DEV Community

Danil Kamyshov
Danil Kamyshov

Posted on • Originally published at kamyshov.info

Constraining literal types with generics in TypeScript

Let's say you need an object with the shape of Record<string, { s: string } | { n: number } | { b: boolean }>. You fire up your IDE and write down something like this:

type A = { s: string };
type B = { n: number };
type C = { b: boolean };

type MyObject = Record<string, A | B | C>;
Enter fullscreen mode Exit fullscreen mode

Great, you can now type check your object literals:

const o1: MyObject = {
  a: { s: 1 }, // error: Type 'number' is not assignable to type 'string'.
};

// all good
const o2: MyObject = {
  a: { s: "str" },
};
Enter fullscreen mode Exit fullscreen mode

Open in TS playground →

Some time later you decide that you need to know the type of o.a, but it can't be inferred! Due to the MyObject type reference, the object literal type information is lost and all you are left with is A | B | C:

type T = typeof o2.a; // => A | B | C
Enter fullscreen mode Exit fullscreen mode

Open in TS playground →

Moreover, because string is used as an indexed access type of Record, TS will not warn you about the non-existent property access:

// TS guess:      `A | B | C`
// Harsh reality: `undefined`
const value = o2.j;
Enter fullscreen mode Exit fullscreen mode

Open in TS playground →

The autocomplete is also not available in this case.

Fortunately, we can leverage the power of generics to both type check the object literal and preserve type information:

type Constraint = Record<string, A | B | C>;

function identity<T extends Constraint>(x: T): T {
  return x;
}

const o = identity({
  a: { s: "a" },
});

type T = typeof o.a; // => { s: string }
Enter fullscreen mode Exit fullscreen mode

Open in TS playground →

The extends clause of the type parameter enforces the correct type of the object:

const o = identity({
  a: { s: 1 }, // error: Type 'number' is not assignable to type 'string'.
});
Enter fullscreen mode Exit fullscreen mode

Open in TS playground →

At the same time, the literal type information is preserved because the identity function returns exactly what it have received: T, which is the (literal) type of the object literal.

Read more on generic constraints in the TS handbook: https://www.typescriptlang.org/docs/handbook/2/generics.html#generic-constraints

Top comments (0)