TypeScript: Understand Built-In/Utility Types

Oleh Baranovskyi
10 min readAug 9, 2019

Nowadays TypeScript becomes more and more popular. Many people are using it for different purposes and not only for frontend but for the backend as well. Good examples are NestJS, TypeORM, and many, many others.
Not a secret that bringing type system into javascript is the main point of TypeScript. But understanding advanced types is not that easy. In this article, we are going to talk about one of the main topics “Built-in/Utility types”.

We’ll cover the following built-in/utility types:
1. Partial
2. Required
3. Readonly
4. ReadonlyArray
5. Pick
6. Omit
7. Record
8. Exclude
9. Extract
10. NonNulable
11. Parameters
12. ConstructorParameters
13. ReturnType
14. InstanceType

Partial<T>

From documentation:

/**
* Make all properties in T optional
*/
type Partial<T> = {
[P in keyof T]?: T[P];
};

Explanation:

When we have an interface or type with required fields, Partial built-in type could help to create a new type from already existing with partial implementation. This means that object which previously had required fields now could be created without those fields.

It also means that we could assign an empty object. But the good side of this is that we could create an empty object but we are not able to add fields which are not in the scope of the partial model.

By example:

Suppose we have a situation when the object could be filled from multiple places and at this point, we are able to initialize it partially, only with one property:

interface User {
name: string;
age: number;
}
let user1: User = { name: 'aaa', age: 23 };
let user2: User = { age: 23 }; // Property 'name' is missing in type '{ age: number; }' but required in type 'User'.
let user3: User = { name: 'ccc' }; // Property 'age' is missing in type '{ name: string; }' but required in type 'User'.

TypeScript will show errors because User interface requires more than one field. However, we could use in this case Partial built-in type. So if we change types for user2 and user3 to Partial type, we would be able to fill user object partially:

let user1: Partial<User> = { name: 'aaa', age: 23 };
let user2: Partial<User> = { age: 23 };
let user3: Partial<User> = { name: 'ccc' };

we could create own type if the partial user has to be used in multiple places:
NOTE: Same is for all other types.

type PartialUser = Partial<User>;let user1: PartialUser = { name: 'aaa', age: 23 };
let user2: PartialUser = { age: 23 };
let user3: PartialUser = { name: 'ccc' };

Required<T>

From documentation:

/**
* Make all properties in T required
*/
type Required<T> = {
[P in keyof T]-?: T[P];
};

Explanation:

If the model contains fields which are not required and there is a need in a new model with all required fields Required type could help in this case.

By Example:

interface User {
name: string;
age?: number;
}
const user1: User = { name: 'User1', age: 23 };
const user2: User = { name: 'User2' }; // no error here

At this point, the User model has field age which is not required so user object could exist without age, but what we need is to create a new User model with required age field.
It can be easily done with Required built-in type.

type RequiredUser = Required<User>;const user1: RequiredUser = { name: 'User1', age: 23 };
const user2: RequiredUser = { name: 'User2' }; // Property 'age' is missing in type '{ name: string; }' but required in type 'Required<User>'.

Readonly<T>

From documentation:

/**
* Make all properties in T readonly
*/
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};

Explanation:

This built-in type makes all properties in the model readonly. Field wich was marked as reaonly could not be changed after initialization. It could be really handy in a situation where you want to have immutable structures or just if you want to prevent some properties from changes.

By Example:

interface User {
name: string;
age: number
}
type ReadonlyUser = Readonly<User>;let readonlyUser: ReadonlyUser = { name: 'Joe', age: 34 };
readonlyUser.age = 3; // Cannot assign to 'age' because it is a read-only property.

ReadonlyArray<T>

From documentation:

interface ReadonlyArray<T> {
/** Iterator of values in the array. */
[Symbol.iterator](): IterableIterator<T>;
/**
* Returns an iterable of key, value pairs for every entry in the array
*/
entries(): IterableIterator<[number, T]>;
/**
* Returns an iterable of keys in the array
*/
keys(): IterableIterator<number>;
/**
* Returns an iterable of values in the array
*/
values(): IterableIterator<T>;
}

Explanation:

Prevents array from changes. In the source code, you can see that there is no method which could modify array with ReadonlyArray type.

By example:

let readonlyArray: ReadonlyArray<number> = [1, 2, 3];
// readonlyArray.push(4); // Because: Property 'push' does not exist on type 'readonly number[]'

Pick<T, K extends keyof T>

From documentation:

/**
* From T, pick a set of properties whose keys are in the union K
*/
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};

Explanation:

Pick utility type gives the ability to create a new type by picking properties from the already existing model.

By Example:

interface User {
name: string;
age: number;
friends: string[];
}
type UserNameAndAge = Pick<User, 'name' | 'age'>let person: UserNameAndAge = {
name: 'hello',
age: 23,
// friends: [] // Because: Type '{ name: string; age: number; friends: undefined[]; }' is not assignable to type 'Pick<User, "name" | "age">'.
Object literal may only specify known properties, and 'friends' does not exist in type 'Pick<User, "name" | "age">'
}

Omit<T, K extends keyof any>

From documentation:

/**
* Construct a type with the properties of T except for those in type K.
*/
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

Explanation:

If Pick built-in type gives you the ability to pick properties from an already existing and thus build new type then Omit is the complete opposite. It lets to omit properties.

By Example:

interface Test {
a: string;
b: number;
c: boolean;
d: number
}
type TestCD = Omit<Test, "a" | "b">;let cAndD: TestCD = {
c: true,
d: 34,
// a: 'test' // Because: Type '{ c: true; d: number; a: string; }' is not assignable to type 'Pick<Test, "c" | "d">'. Object literal may only specify known properties, and 'a' does not exist in type 'Pick<Test, "c" | "d">'.
}

Record<K extends keyof any, T>

From documentation:

/**
* Construct a type with a set of properties K of type T
*/
type Record<K extends keyof any, T> = {
[P in K]: T;
};

Explanation:

Record type gives an ability to describe type that has some amount of keys with the same value type.

By Example:

type AlertType = 'warning' | 'error' | 'success';enum AlertColors {
Red = 1,
Green = 2,
Yellow = 3
}
enum AlertIcon {
Star = 1,
Close = 2,
Alert = 3
}
type AlertRecord = Record<AlertType, { color: AlertColors, icon: AlertIcon }>;const alerts: AlertRecord = {
warning: { color: AlertColors.Yellow, icon: AlertIcon.Alert },
error: { color: AlertColors.Red, icon: AlertIcon.Close },
success: { color: AlertColors.Green, icon: AlertIcon.Star },
}

Exclude<T, U>

From documentation:

/**
* Exclude from T those types that are assignable to U
*/
type Exclude<T, U> = T extends U ? never : T;

Explanation:

It lets to exclude some keys from an already defined set of keys and to create a kind of string union type. The best example is Omit built-in type.

By Example:

export interface User {
firstname: string;
lastname: string;
age: number;
}
export type UserKeysWithoutLastName = Exclude<keyof User, 'lastname'>let user9: UserKeysWithoutLastName = 'age';
let user10: UserKeysWithoutLastName = 'firstname';
// let user11: UserKeysWithoutLastName = 'lastname'; // Because: Type '"lastname"' is not assignable to type '"firstname" | "age"'.

Extract<T, U>

From documentation:

/**
* Extract from T those types that are assignable to U
*/
type Extract<T, U> = T extends U ? T : never;

Explanation:

Extract type picks properties from the two types which are present in both.

By example:

export interface User {
id: number;
username: string;
city: string;
password: string;
}
export interface UserToSave {
username: string;
city: string;
password: string;
passwordConfirmation: string;
}
export type UserDetails = Extract<keyof User, keyof UserToSave>let userDetails1: UserDetails = 'username';
// let userDetails2: UserDetails = 'id'; // Because: Type '"id"' is not assignable to type '"username" | "city" | "password"'.
let userDetails3: UserDetails = 'city';
let userDetails4: UserDetails = 'password';
// let userDetails5: UserDetails = 'passwordConfirmation'; // Because: Type '"passwordConfirmation"' is not assignable to type '"username" | "city" | "password"'.

NonNullable<T>

From documentation:

/**
* Exclude null and undefined from T
*/
type NonNullable<T> = T extends null | undefined ? never : T;

Explanation:

Gives the ability to describe brand new type from already existing, which will prevent null values for all fields.

By example:

interface User {
name: string;
}
type NonNullableUser = NonNullable<User>;let user1: NonNullableUser = { name: 'John' };
// let user2: NonNullableUser = { name: null }; // Because: Type 'null' is not assignable to type 'string'
// let user3: NonNullableUser = { name: undefined }; // Because: Type 'undefined' is not assignable to type 'string'.

Parameters<T extends (...args: any) => any>

From documentation:

/**
* Obtain the parameters of a function type in a tuple
*/
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

Explanation:

Creates an array of types from the existing function which will have the same list of types as function arguments.

By example:

function a1(x: number, y: number, flag: boolean): void {
// ...
}
type aType = Parameters<typeof a1>;
let aValue: aType = [1, 2, false];
// let aValue2: aType = [1, 'as string', false]; // Because: Type 'string' is not assignable to type 'number'.

ConstructorParameters<T extends new (...args: any) => any>

From documentation:

/**
* Obtain the parameters of a constructor function type in a tuple
*/
type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) => any ? P : never;

Explanation:

ConsctructorParameters built-in type is pretty similar to Parameters type except that it collects property types from the constructor.

By example:

class Hero {
private _firstname: string;
private _lastname: string;
constructor(firstname: string, lastname: string) {
this._firstname = firstname;
this._lastname = lastname;
}
}
type HeroConstructorArgsType = ConstructorParameters<typeof Hero>;let heroConstructorArgs: HeroConstructorArgsType = ['first', 'last'];
// let heroConstructorArgs2: HeroConstructorArgsType = ['first', false]; // Because: Type 'false' is not assignable to type 'string'.

ReturnType<T extends (...args: any) => any>

From documentation:

/**
* Obtain the return type of a function type
*/
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

Explanation:
If we want to extract type which will be returned by the function. ReturnType is exactly we are looking for.

By example:

let f1 = () => ({ a: 23, b: 33 });type T0 = ReturnType<() => string>;  // string
type T1 = ReturnType<(s: string) => void>; // void
type T2 = ReturnType<(<T>() => T)>; // {}
type T3 = ReturnType<(<T extends U, U extends number[]>() => T)>; // number[]
type T4 = ReturnType<typeof f1>; // { a: number, b: string }
type T5 = ReturnType<any>; // any
type T6 = ReturnType<never>; // any
// type T7 = ReturnType<string>; // Because: Type 'string' does not satisfy the constraint '(...args: any) => any'.
// type T8 = ReturnType<Function>; // Because: Type 'Function' does not satisfy the constraint '(...args: any) => any'. Type 'Function' provides no match for the signature '(...args: any): any'.

InstanceType<T extends new (...args: any) => any>

From documentation:

/**
* Obtain the return type of a constructor function type
*/
type InstanceType<T extends new (...args: any) => any> = T extends new (...args: any) => infer R ? R : any;

Explanation:

Constructs a type consisting of the instance type of a constructor function type T.
Note: This built-in type is a bit complicated in understanding here: https://github.com/Microsoft/TypeScript/issues/25998 you can find more information about it.

By example:

class C {
x = 0;
y = 0;
}
type T0 = InstanceType<typeof C>; // C
type T1 = InstanceType<any>; // any
type T2 = InstanceType<never>; // any
// type T3 = InstanceType<string>; // Because: Type 'string' does not satisfy the constraint 'new (...args: any) => any'.
// type T4 = InstanceType<Function>; // Because: Type 'Function' does not satisfy the constraint 'new (...args: any) => any'. Type 'Function' provides no match for the signature 'new (...args: any): any'.

Combining Built-Ins with other types:

What I really like in TypeScript is the fact that it is so flexible in the creation of new types. Required, Partial, NonNullable, etc. could be easily combined with most of the other types, for instance with intersection or unio type. Let us take a look for the following example:

interface User {
name: string;
profession?: string;
}
interface Credentials {
password: string;
confirm: string;
}
type UserMaybeWithCredentials = Required<User> & Partial<Credentials>;// OK
const u2: UserMaybeWithCredentials = {
name: 'John',
profession: 'Developer',
password: 'pass'
}
// OK
const u3: UserMaybeWithCredentials = {
name: 'John',
profession: 'Developer',
password: 'pass',
confirm: 'pass'
}
// ERROR
const u4: UserMaybeWithCredentials = {
profession: 'Developer',
password: 'pass',
confirm: 'pass'
}
// ERROR
const u5: UserMaybeWithCredentials = {
name: 'John',
password: 'pass',
confirm: 'pass'
}

Here we have a User model with not required field and Credentials with all required fields. But what if we want a combined object with all fields from the User model and maybe with some fields from Credentials model. The example above demonstrates how easy this target could be achieved with TypeScript utility and intersection types. That are big opportunities for us, JS developers.

Conclusion

Thank you guys for reading. I hope you enjoyed it and learned some new stuff related to JavaScript. Please subscribe and press ‘Clap’ button if you like this article.

--

--