Derive union of string literal types with lookup types in Typescript

remshams

Mathias Remshardt

Posted on September 4, 2021

Derive union of string literal types with lookup types in Typescript

Recently I started playing with unions of string literals as an alternative to enums. I did not experience any issues while using enums. I just prefer the string literals as these, in contrast to enums, do not lead additional code generated during compilation.

One thing that has bothered me so far is that I could not find an easy way to store all strings of a string literal union in an array. This can be useful if you want to e.g. randomly select one of the string literals in the union as part of creating mock/fixture data.

Of course you can define both the unions of string literals and an array with the same strings:

type PostType = 'article' | 'podcast';
const postTypes = ['article', 'podcast'];
Enter fullscreen mode Exit fullscreen mode

This duplication is error prone in case a new option needs to be added, removed and the like. So I was searching for a means to either derive the array from the type or the type from the array.

With lookup types this is exactly possible:

const postTypes = ['article', 'podcast'] as const;
type PostTypes = typeof postTypes[number];
Enter fullscreen mode Exit fullscreen mode

Screenshot of derived post types in VSCode

First typeof in combination with as const is used to infer the type of the defined array. as const is important here as Typescript otherwise defines the type as Array<string> instead of an array of string literals.

Using indexed access types/lookup types gives the union of string literals. This is somehow equivalent to using an index in an "normal" Javascript/Typescript to get a specific element like list[0].

Lookup types can be used for more sophisticated use case e.g. like deriving all properties in an object the values of which are e.g. a string:

type Author = {
  firstName: string;
  lastName: string;
};
type Post = {
  title: string;
  description: string;
  views: number;
  author: Author;
};

type PostStringKeys = {
  [P in keyof Post]: Post[P] extends string ? P : never;
}[keyof Post];
Enter fullscreen mode Exit fullscreen mode

Screenshot of extracted string property names in VSCode

Let's quickly break this down:

  • P keyof Post gives all keys of Post (title and description).
  • Post[P] extends string ? P : never checks if the value of property P in Post is of type string. If true the property name is set as value otherwise the property is not included in the newly created type.
  • With the help of lookup types the union of property names/string literals is derived using keyof Post.
    • The set of keys in Post is a superset of the keys of the derived type and can therefor be used as an index

This can be made generic like so:

type KeysOfType<T, K> = { [P in keyof T]: T[P] extends K ? P : never }[keyof T];

type PostStringKeys = KeysOfType<Post, string>;
type PostNumberKeys = KeysOfType<Post, number>;
Enter fullscreen mode Exit fullscreen mode

Compare to the previous example T == Post and K == string. This provides the additional possibility to include properties with different value types like string and Author using unions.

type PostStringAndAuthorKeys = KeysOfType<Post, number | Author>;
Enter fullscreen mode Exit fullscreen mode

Screenshot of extracted property names with type number and author

The code snippets can be found here.

That's it and as always, thanks for reading.

đź’– đź’Ş đź™… đźš©
remshams
Mathias Remshardt

Posted on September 4, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related