2

I'm trying to type the return of a lookup table with functions.

While it works great with a simple lookup table:

const lookupTable = {
  A: "foo",
  B: 1,
};

function findInTable<T extends "A" | "B">(p: T): typeof lookupTable[T] {
  return lookupTable[p];
}

const isString: string = findInTable("A");
const isNumber: number = findInTable("B");

I can't seem to make it work with the return type of the function

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: string = findInTable("A");
const isNumber: number = findInTable("B");

How can I write it to make it build ?

Quadear
  • 467
  • 2
  • 10

1 Answers1

2

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.