You should overload your function:
const lookupTable = {
A: () => "foo",
B: () => 1,
} as const;
function findInTable<T extends keyof typeof lookupTable>(
p: T
):ReturnType<typeof lookupTable[T]>
function findInTable<T extends keyof typeof lookupTable>(
p: T
) {
return lookupTable[p]();
}
const isString = findInTable("A"); // string
const isNumber = findInTable("B"); // number
Playground
Try to avoid declaring explicit types like here const isString: string
Most of the time TS should do the work for you.
AFAIK, function overloads are bivariant. It is mean that they are not so strict.
TS does not check function inner implementation with overloaded implementation.
Because T extends "A" | "B"
, this code is valid:
const lookupTable = {
A: () => "foo",
B: () => 1,
};
function findInTable<T extends "A" | "B">(
p: T
): ReturnType<typeof lookupTable[T]> {
return lookupTable[p](); // Can't assign string | number to ReturnType<{ A: () => string; B: () => number; }[T]>
}
const isString = findInTable<'B' | 'A'>("A");
const isNumber = findInTable<'B' | 'A'>("B");
And this is why TS cant figure out what function will return. In any moment it can be either A or B
If you want to be super safe, consider this example:
const lookupTable = {
A: () => "foo",
B: () => 1,
} as const;
// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never;
// credits https://stackoverflow.com/users/125734/titian-cernicova-dragomir
type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true;
function findInTable<T extends keyof typeof lookupTable>(
p: IsUnion<T> extends true ? never : T
): ReturnType<typeof lookupTable[T]>
function findInTable<T extends keyof typeof lookupTable>(
p: T
) {
return lookupTable[p]();
}
const isString = findInTable<'A' | 'B'>("A"); // error
As you might have noticed you are not allowed to use findInTable<'A' | 'B'>("A")
explicit union generic.