Как использовать типы пересечения в TypeScript: советы от профессионала

Тип в TypeScript можно определять как набор значений. Например, тип number можно представить как набор всех чисел. Из этого следует, что 1,0 и 68 принадлежат этому набору, а "bytefer" не принадлежит ему, поскольку относится к типу string.

Аналогичным образом объектные типы можно воспринимать как наборы объектов. Например, тип Point в следующем фрагменте кода представляет собой набор объектов со свойствами x и y, причем оба типа значений свойств являются числовыми. Тип Named представляет собой набор объектов, содержащих свойство name, а тип значения свойства  —  строковый.

interface Point {
  x: number;
  y: number;
}

interface Named {
  name: string;
}

Согласно теории множеств, если A и B  —  множества, то множество, состоящее из всех элементов, принадлежащих множеству A и множеству B, является пересечением множества A и множества B.

При пересечении типов Point и Named создается новый тип. Объекты, содержащиеся в нем, принадлежат как Point, так и Named.

В TypeScript для реализации операции пересечения нескольких типов предусмотрен оператор &, а полученный новый тип называется типом пересечения.

Оператор & удовлетворяет следующим правилам.

  • Идентичность: A & A эквивалентно A.
  • Коммутативность (независимость от порядка выполнения): A & B эквивалентно B & A (за исключением сигнатур вызова и конструктора, как будет отмечено ниже).
  • Ассоциативность: (A & B) & C эквивалентно A & (B & C).
  • Сокращение супертипов: A & B эквивалентно A, если B является супертипом A.

В приведенном выше коде типы any и never являются специальными. Тип any, пересекающийся с типом any, приводит к типу any (что не происходит при пересечении any с never).

Теперь, когда мы познакомились с оператором &, посмотрим, какой тип получится при пересечения Point и Named.

Созданный тип NamedPoint содержит свойства x, y и name. Что же происходит, когда пересекаются несколько типов объектов, содержащих одинаковые атрибуты, но чьи свойства не являются однотипными?

interface X {
  c: string;
  d: string;
}

interface Y {
  c: number;
  e: string
}

type XY = X & Y;
type YX = Y & X;

В приведенном выше коде interface X и interface Y содержат одно и то же свойство c, но их типы не совпадают. Может ли в этом случае тип атрибута c в типе XY или YX быть строковым или числовым? Проверим это:

Почему после пересечения interface X и interface Y тип свойства c становится never? Потому что тип свойства c после этой операции  —  string & number, то есть он может быть либо строковым, либо числовым. Очевидно, что такого типа не существует, поэтому тип свойства c после операции  —  never.

В предыдущем примере получилось так, что типы свойства c в interface X и interface Y являются примитивными типами данных. Что же произойдет, если разные типы объектов содержат одно и то же свойство, а тип свойства не является примитивным? Рассмотрим конкретный пример:

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

Помимо объектных типов, операции пересечения могут также выполняться с типами функций.

Как видно из приведенного выше кода, только оператор вызова функции f(1, "bytefer") выдает ошибку. Сообщение об ошибке выглядит следующим образом.

No overload matches this call.
Overload 1 of 2, '(a: string, b: string): void', gave the following error.
Argument of type 'number' is not assignable to parameter of type 'string'.
Overload 2 of 2, '(a: number, b: number): void', gave the following error.
Argument of type 'string' is not assignable to parameter of type 'number'.ts(2769)

Ни одна перегрузка не соответствует этому вызову.

Перегрузка 1 из 2, (a: string, b: string): void, выдала следующую ошибку.

Аргумент типа number не может быть присвоен параметру типа string.

Перегрузка 2 из 2, (a: число, b: число): void, привела к следующей ошибке.

Аргумент типа string не может быть присвоен параметру типа ‘number’.ts(2769).

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

Освоение типов пересечения в сочетании со знанием отображенных типов позволит реализовывать некоторые пользовательские типы утилит в соответствии с конкретными требованиями. Например, вы можете реализовать тип утилиты PartialByKeys, чтобы сделать опциональными значения ключей, указанные в объектном типе.

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Bytefer: How to Use TypeScript Intersection Types Like a Pro

Предыдущая статьяСоздание анимированной пузырьковой диаграммы Ханса Рослинга на языке R
Следующая статья5 секретов продуктивной работы в VS Code