Constraining literal types with generics in TypeScript
Tags: typescript
Published: 2021-06-30 11:30:07


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:

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

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

Great, you can now type check your object literals:

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

// all good
const o2: MyObject = {
  a: { s: "str" },
};
```

[Open in TS playground →](https://www.typescriptlang.org/play?#code/C4TwDgpgBAglC8UDeUDOAuNwBOBLAdgOZQC+A3AFCiRQBCCyU+m+ArgLYBGE2pl10AMIMUnTJwD2EgDYQAhvj4Uq4aAFkQAeU4ArCAGNgDAEoGJ2ACYAeVDgKEANLCgAfOq6iCAfJQr6J+LZQEgCMmBraeoYiFFBQcpgoGFAhpE4A9OlQPNjmmAAqqlAA5GxcPMVQuKhMEkZyqKi4hPhynLJQwBKdRcW2eETFAHQU5MqZ8dLSUIRSFn4BQRIATOFaugZGiEix8YlomABE-Ydpo5RAA)

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`:

```ts
type T = typeof o2.a; // => A | B | C
```

[Open in TS playground →](https://www.typescriptlang.org/play?#code/C4TwDgpgBAglC8UDeUDOAuNwBOBLAdgOZQC+A3AFCiRQBCCyU+m+ArgLYBGE2pl10AMIMUnTJwD2EgDYQAhvj4Uq4aAFkQAeU4ArCAGNgDAEoGJ2ACYAeVDgKEANLCgAfOq6iCAfJQoB6Pyg5aWkoQikLCn0JfFsoCQAmTA1tPUMRCiggzBQMKAAiW2x80gcKcmUBKAAVBgEJADN4hIA6OTIoAIQvZzd6N0EKIA)

Moreover, because `string` is used as an _[indexed access type](https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html)_ of `Record`, TS will not warn you about the non-existent property access:

```ts
// TS guess:      `A | B | C`
// Harsh reality: `undefined`
const value = o2.j;
```

[Open in TS playground →](https://www.typescriptlang.org/play?#code/C4TwDgpgBAglC8UDeUDOAuNwBOBLAdgOZQC+A3AFCiRQBCCyU+m+ArgLYBGE2pl10AMIMUnTJwD2EgDYQAhvj4Uq4aAFkQAeU4ArCAGNgDAEoGJ2ACYAeVDgKEANLCgAfOq6iCAfJQoB6Pyg5aWkoQikLCn0JfFsoCQAmTA1tPUMRCiggzBQMKAAiW2x80gcKcmVo2KMAN2DWaEREgDodSiA)

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:

```ts
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 }
```

[Open in TS playground →](https://www.typescriptlang.org/play?#code/C4TwDgpgBAglC8UDeUDOAuNwBOBLAdgOZQC+A3AFCiRQBCCyU+m+ArgLYBGE2pl10AMIMUnTJwD2EgDYQAhvj4Uq4IRPyoccgsAYAlCAGMJ2ACYAeTXiIAaWFAA+dR1EEA+ShQBmrfIeC46lC4phD4AaDmACpQEAAewGGmqK7qVtrhbgAUcZhRAJR5yBRQUNgQwKzYinGUJMrGGroSDCFhESBZSCVQcpgoGFAARHJDpDYUJPmeAlAxiAISXlASAHRyZFAA9FsIboyDVgTE9UA)

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

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

[Open in TS playground →](https://www.typescriptlang.org/play?#code/C4TwDgpgBAglC8UDeUDOAuNwBOBLAdgOZQC+A3AFCiRQBCCyU+m+ArgLYBGE2pl10AMIMUnTJwD2EgDYQAhvj4Uq4IRPyoccgsAYAlCAGMJ2ACYAeTXiIAaWFAA+dR1EEA+ShQBmrfIeC46lC4phD4AaDmACpQEAAewGGmqK7qVtrhbgAUcZhRAJR5yBRQUNgQwKzYinGUJMrGGroSDCFhESBZSCVQcpgoGFAAjKR2APRjsdjYJnmqUADkbFw8C8Ep+BK6cqiouIT4cpyyUMAtAotWBIQLAHQUJPmUQA)

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