Table of contents
- Utility types
-
Partial<T> -
Required<T> -
Readonly<T> -
ReadonlyArray<T> -
Pick<T, K extends keyof T> -
Omit<T, K extends keyof any> -
Record<K extends keyof any, T> -
Exclude<T, U> -
Extract<T, U> -
NonNullable<T> -
Parameters<T extends (...args: any) => any> -
ConstructorParameters<T extends new (...args: any) => any> -
ReturnType<T extends (...args: any) => any> -
InstanceType<T extends new (...args: any) => any>
-
- Combining Built-Ins with other types
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".
Utility Types
Partial<T>
/**
* 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,
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
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>
/**
* 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
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
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>
/**
* 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 which was marked as readonly
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>
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
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 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:
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>
/**
* 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
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>
/**
* Construct a type with a set of properties K of type T
*/
type Record<K extends keyof any, T> = {
[P in K]: T;
};
Explanation:
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>
/**
* 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
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>
/**
* 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>
/**
* 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>
/**
* 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>
/**
* 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:
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>
/**
* 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.
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>
/**
* 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
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.
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