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 filterFilterKeysOfType<Type, Key>
that accepts two type parameters,Type
- on which we want to apply filter andKey
- the type of keys we want to filter out of typeType
. -
[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 typePersonInfo
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 typeType
. Type[key]
is a lookup type that denotes the type of the key. Here, forType - PersonInfo
it transpiles toPersonInfo["name"] - string
PersonInfo["age"] - number
Type[key] extends Key
checks if a given key inPersonInfo
is of typeKey
. Here keyage
is of the typenumber
which is passed asKey
toFilterKeysOfType
.- Thus, the entire expression
key in keyof Type as Type[key] extends Key
checks whetherkey
is of typenumber
. If true, returnsnever
which filters out thekey
. If false, it adds thekey
to be used in the new type generated after applying filter.
- We are using
-
Finally, the entire expression filters out keys of the type
number
which we have passed as the second parameter toPersonInfo
, and, returns the resultant type without any key of typenumber
.
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.