Перейти к содержанию

Exclude, Extract, NonNullable, ReturnType, InstanceType, Omit

Чтобы сэкономить время разработчиков, в систему типов TypeScript были включены несколько часто требующихся условных типов, каждый из которых будет подробно рассмотрен в этой главе.

Exclude (исключает из T признаки присущие U)

В результате разрешения условный тип Exclude<T, U> будет представлять разницу типа T относительно типа U. Параметры типа T и U могут быть представлены как единичным типом, так и множеством union.

1
2
3
// @filename: lib.d.ts

type Exclude<T, U> = T extends U ? never : T;

Простыми словами, из типа T будут исключены признаки (ключи), присущие также и типу U.

1
2
3
let v0: Exclude<number | string, number | boolean>; // let v0: string
let v1: Exclude<number | string, boolean | object>; // let v1: string|number
let v2: Exclude<'a' | 'b', 'a' | 'c'>; // let v2: "b"

В случае, если оба аргумента типа принадлежат к одному и тому же типу данных, Exclude<T, U> будет представлен типом never.

1
let v4: Exclude<number | string, number | string>; // let v4: never

Его реальную пользу лучше всего продемонстрировать на реализации функции, которая на входе получает два разных объекта, а на выходе возвращает новый объект, состоящий из членов, присутствующих в первом объекте, но отсутствующих во втором. Аналог функции difference из широко известной библиотеки lodash.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
declare function difference<T, U>(
  a: T,
  b: U
): Pick<T, Exclude<keyof T, keyof U>>;

interface IA {
  a: number;
  b: string;
}
interface IB {
  a: number;
  c: boolean;
}

let a: IA = { a: 5, b: '' };
let b: IB = { a: 10, c: true };

interface IDifference {
  b: string;
}

let v0: IDifference = difference(a, b); // Ok
// Error -> Property 'a' is missing in type 'Pick<IA, "b">' but required in type 'IA'.
let v1: IA = difference(a, b);
// Error -> Type 'Pick ' is missing the following properties from type 'IB': a, c
let v2: IB = difference(a, b);

Extract (общие для двух типов признаки)

В результате разрешения условный тип Extract<T, U> будет представлять пересечение типа T относительно типа U. Оба параметра типа могут быть представлены как обычным типом, так и union.

1
2
3
// @filename: lib.d.ts

type Extract<T, U> = T extends U ? T : never;

Простыми словами, после разрешения Extract<T, U> будет принадлежать к типу определяемого признаками (ключами), присущими обоим типам. То есть, тип Extract<T, U> является противоположностью типа Exclude<T, U>.

1
2
3
let v0: Extract<number | string, number | string>; // let v0: string | number
let v1: Extract<number | string, number | boolean>; // let v1: number
let v2: Extract<'a' | 'b', 'a' | 'c'>; // let v2: "a"

В случае, когда общие признаки отсутствуют, тип Extract<T, U> будет представлять тип never.

1
let v3: Extract<number | string, boolean | object>; // let v3: never

Условный тип Extract<T, U> стоит рассмотреть на примере реализации функции, принимающей два объекта и возвращающей новый объект, состоящий из членов первого объекта, которые также присутствуют и во втором объекте.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
declare function intersection<T, U>(
  a: T,
  b: U
): Pick<T, Extract<keyof T, keyof U>>;

interface IA {
  a: number;
  b: string;
}
interface IB {
  a: number;
  c: boolean;
}

let a: IA = { a: 5, b: '' };
let b: IB = { a: 10, c: true };

interface IIntersection {
  a: number;
}

let v0: IIntersection = intersection(a, b); // Ok
// Error -> Property 'b' is missing in type 'Pick<IA, "a">' but required in type 'IA'.
let v1: IA = intersection(a, b);
// Error -> Property 'c' is missing in type 'Pick<IA, "a">' but required in type 'IB'.
let v2: IB = intersection(a, b);

NonNullable (удаляет типы null и undefined)

Условный тип NonNullable<T> служит для исключения из типа признаков типов null и undefined. Единственный параметр типа может принадлежать как к обычному типу, так и множеству определяемого тип union.

1
2
3
4
5
// @filename: lib.d.ts

type NonNullable<T> = T extends null | undefined
  ? never
  : T;

Простыми словами, данный тип удаляет из аннотации типа такие типы, как null и undefined.

1
2
3
let v0: NonNullable<string | number | null>; // let v0: string | number
let v1: NonNullable<string | undefined | null>; // let v1: string
let v2: NonNullable<string | number | undefined | null>; // let v2: string | number

В случае, когда тип, выступающий в роли единственного аргумента типа, принадлежит только к типам null и\или undefined, NonNullable<T> представляет тип never.

1
let v3: NonNullable<undefined | null>; // let v3: never

ReturnType (получить тип значения, возвращаемого функцией)

Условный тип ReturnType<T> служит для установления возвращаемого из функции типа. В качестве параметра типа должен обязательно выступать функциональный тип.

1
2
3
4
5
// @filename: lib.d.ts

type ReturnType<
  T extends (...args: any) => any
> = T extends (...args: any) => infer R ? R : any;

На практике очень часто требуется получить тип, к которому принадлежит значение, возвращаемое из функции. Единственное, на что стоит обратить внимание, что в случаях, когда тип возвращаемого из функции значения является параметром типа, у которого отсутствуют хоть какие-то признаки, то тип ReturnType<T> будет представлен пустым объектным типом {}.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let v0: ReturnType<() => void>; // let v0: void
let v1: ReturnType<() => number | string>; // let v1: string|number
let v2: ReturnType<<T>() => T>; // let v2: {}
let v3: ReturnType<<
  T extends U,
  U extends string[]
>() => T>; // let v3: string[]
let v4: ReturnType<any>; // let v4: any
let v5: ReturnType<never>; // let v5: never
let v6: ReturnType<Function>; // Error
let v7: ReturnType<number>; // Error

InstanceType (получить через тип класса тип его экземпляра)

Условный тип InstanceType<T> предназначен для получения типа экземпляра на основе типа, представляющего класс. Параметр типа T должен обязательно принадлежать к типу класса.

1
2
3
4
5
// @filename: lib.d.ts

type InstanceType<
  T extends new (...args: any) => any
> = T extends new (...args: any) => infer R ? R : any;

В большинстве случаев идентификатор класса задействован в приложении в качестве типа его экземпляра.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Animal {
  move(): void {}
}

/**
 * Тип Animal представляет объект класса,
 * то есть его экземпляр, полученный при
 * помощи оператора new.
 */
function f(animal: Animal) {
  type Param = typeof Animal;

  // здесь Param представляет экземпляр типа Animal
}

Но сложные приложения часто требуют динамического создания своих компонентов. В таких случаях фабричные функции работают не с экземплярами классов, а непосредственно с самими классами.

Стоит напомнить, что в JavaScript классы это всего лишь синтаксический сахар над старой доброй функцией конструктором. И, как известно, объект функции конструктора представляет объект класса, содержащего ссылку на прототип, который и представляет экземпляр. Другими словами, в TypeScript идентификатор класса, указанный в аннотации типа, представляет описание прототипа. Чтобы получить тип самого класса, необходимо выполнить над идентификатором класса запрос типа.

1
2
3
4
5
6
7
8
9
class Animal {
  move(): void {}
}

type Instance = Animal;
type Class = typeof Animal;

type MoveFromInstance = Instance['move']; // Ok ->() => void
type MoveFromClass = Class['move']; // Error -> Property 'move' does not exist on type 'typeof Animal'.

Таким образом, грамотно вычислить тип экземпляра в фабричной функции можно при помощи типа InstanceType<T>.

1
2
3
4
5
6
7
8
9
class Animal {
  move(): void {}
}

function factory(Class: typeof Animal) {
  type Instance = InstanceType<Class>;

  let instance: Instance = new Class(); // Ok -> let instance: Animal
}

Хотя можно прибегнуть и к менее декларативному способу - к запросу типа свойства класса prototype.

1
2
3
4
5
function factory(Class: typeof Animal) {
  type Instance = Class['prototype'];

  let instance: Instance = new Class(); // Ok -> let instance: Animal
}

И последнее, о чем стоит упомянуть, что результат получения типа непосредственно через any и never будет представлен ими же. Остальные случаи приведут к возникновению ошибки.

1
2
3
4
5
class Animal {}

let v0: InstanceType<any>; // let v0: any
let v1: InstanceType<never>; // let v1: never
let v2: InstanceType<number>; // Error

Parameters (получить тип размеченного кортежа, описывающий параметры функционального типа)

Расширенный тип Parameters<T> предназначен для получения типов, указанных в аннотации параметров функции. В качестве аргумента типа ожидается функциональный тип, на основе которого будет получен размеченный кортеж, описывающий параметры этого функционального типа.

1
2
3
type Parameters<
  T extends (...args: any[]) => any
> = T extends (...args: infer P) => any ? P : never;

Parameters<T> возвращает типы параметров в виде кортежа.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function f<T>(
  p0: T,
  p1: number,
  p2: string,
  p3?: boolean,
  p4: object = {}
) {}

/**
 * type FunctionParams = [p0: unknown, p1: number, p2: string, p3?: boolean, p4?: object]
 */
type FunctionParams = Parameters<typeof f>;

ConstructorParameters (получить через тип класса размеченный кортеж, описывающий параметры его конструктора)

Расширенный тип ConstructorParameters<T> предназначен для получения типов, указанных в аннотации параметров конструктора.

1
2
3
type ConstructorParameters<
  T extends new (...args: any[]) => any
> = T extends new (...args: infer P) => any ? P : never;

В качестве единственного параметра типа ConstructorParameters<T> ожидает тип самого класса, на основе конструктора которого будет получен размеченный кортеж, описывающий параметры этого конструктора.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Class<T> {
  constructor(
    p0: T,
    p1: number,
    p2: string,
    p3?: boolean,
    p4: object = {}
  ) {}
}

/**
 * type ClassParams = [p0: unknown, p1: number, p2: string, p3?: boolean, p4?: object]
 */
type ClassParams = ConstructorParameters<typeof Class>;

Omit (исключить из T признаки, ассоциированные с ключами, перечисленными множеством K)

Расширенный тип Omit<T, K> предназначен для определения нового типа путем исключения заданных признаков из существующего типа.

1
2
3
4
5
// lib.d.ts

type Omit<T, K extends string | number | symbol> = {
  [P in Exclude<keyof T, K>]: T[P];
};

В качестве первого аргумента типа тип Omit<T, K> ожидает тип данных, из которого будут исключены признаки, связанные с ключами, переданными в качестве второго аргумента типа.

Простыми словами, к помощи Omit<T, K> следует прибегать в случаях необходимости определения типа, представляющего некоторую часть уже существующего типа.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
type Person = {
  firstName: string;
  lastName: string;

  age: number;
};

/**
 * Тип PersonName представляет только часть типа Person
 *
 * type PersonName = {
 *  firstName: string;
 *  lastName: string;
 * }
 */
type PersonName = Omit<Person, 'age'>; // исключение признаков, связанных с полем age, из типа Person

Комментарии