TypeScript's type system makes it very easy to add types as per our requirements. But most of the time, we may need to perform some transformations on existing types to get resultant type as per our requirements instead of adding a new one.
Some common type transformations may include:

  • Extraction of types
  • Exclusion/Inclusion of types
  • Setting types to required/optional

Utility Types provided by TypeScript help in performing such type transformations. Utility Types are built-in and globally accessible functions available in TypeScript. Under the hood, they make use of Generics extensively.

Some frequently used Utility Types are as follows:

Pick:

Pick<Type, Keys> utility constructs a new type by selecting the specified Keys(string literal or union of string literals) from the given Type.

// Consider PersonInfo type having keys `name` as `string` and `age` as `number`
type PersonInfo = {
  name: string;
  age: number;
};
// PersonName is new type created by Pick<T, Key> by extracting `name` from `PersonInfo`
type PersonName = Pick<PersonInfo, "name">;
// Output:
// PersonName = { name:string }
const person: PersonName = { name: "John Doe" };

ReturnType:

ReturnType<Type> utility creates a new Type from the return type of the function.

// Function `createPerson()` returns a PersonInfo object having keys `name` of type `string` and `age` of type `number`
function createPerson(): PersonInfo {
  return {
    name: "John Doe",
    age: 30,
  };
}

// T1 is the new type created by ReturnType<typeof createPerson>
type T1 = ReturnType<typeof createPerson>;

// Output:
// T1 = {
//  name: string,
//  age: number
// }

A list of built-in Utility Types can be found from the Official TypeScript Docs which can be useful for type transformations.

Along with built-in Utility Types, TypeScript is flexible in offering us to create custom Utility Types as per our requirements. Creating custom Utility Types require a basic understanding of the following as pre-requisites:

Let’s understand how we can create a custom utility FilterKeysOfType<Type, Key> which will filter all keys of type Key present in type Type.

FilterKeysOfType<Type, Key>:

// Consider PersonInfo type from above having keys `name` as `string` and `age` as `number`
type PersonInfo = {
  name: string;
  age: number;
};


type FilterKeysOfType<Type, Key> = {
  [key in keyof Type as Type[key] extends Key ? never : key]: Type[key];
};

// `FilterKeysOfType<PersonInfo, number>` filters a Key of type `number` from Type PersonInfo
type FilteredType = FilterKeysOfType<PersonInfo, number>;
// Output:
// FilteredType = { name: string }

The example looks too complex at first. Let's break down the logic and try understanding how it works:-

  • type FilterKeysOfType<Type, Key> - We have defined a generic utility filter FilterKeysOfType<Type, Key> that accepts two type parameters, Type - on which we want to apply filter and Key - the type of keys we want to filter out of type Type.

  • [key in keyof Type as Type[key] extends Key ? never : key] : Type[key]:

    • We are using keyof operator within square brackets. keyof Type represents all keys of type PersonInfo as a union of string literal types i.e. "name"|"age".
    • The key in keyof Type keyword allows us to loop through all the keys in type Type.
    • Type[key] is a lookup type that denotes the type of the key. Here, for Type - PersonInfo it transpiles to
      • PersonInfo["name"] - string
      • PersonInfo["age"] - number
    • Type[key] extends Key checks if a given key in PersonInfo is of type Key. Here key age is of the type number which is passed as Key to FilterKeysOfType.
    • Thus, the entire expression key in keyof Type as Type[key] extends Key checks whether key is of type number. If true, returns never which filters out the key. If false, it adds the key to be used in the new type generated after applying filter.
  • Finally, the entire expression filters out keys of the type number which we have passed as the second parameter to PersonInfo, and, returns the resultant type without any key of type number.

With TypeScript's flexibility and the knowledge of Generics, Mapped Types, and keyof operator, we can create our custom Utility Types enabling easy and efficient transformation of types.

References