Тип пересечения, записываемый как A & B, описывает значение, которое одновременно является A и B. Для объектных типов это значит, что у значения есть все свойства обоих. Так объединяют существующие типы, не выписывая свойства заново.
Во втором объекте нет team, поэтому это ошибка компиляции TS2322, а следующая строка сообщения говорит Property 'team' is missing ... but required in type 'Employee'. Значение Staff можно передать везде, где ожидается Person или Employee.
Объединение объектных типов
& работает с любым сочетанием псевдонимов типов, интерфейсов и встроенных объектных типов, а также с параметрами типа дженериков. В последнем случае его трудно заменить: функция, которая добавляет свойства к любому полученному объекту, может точно это выразить.
Вызывающий код сохраняет точный тип того, что передал (title, words), плюс два добавленных свойства. interface ... extends не может выразить «каким бы ни был T, плюс это», потому что интерфейс не может расширять параметр типа.
Конфликтующие свойства становятся never
Когда обе стороны объявляют одно и то же свойство, его тип это пересечение двух типов. Если у этих типов нет общих значений, свойство становится never, и компилятор молчит, пока вы не попробуете создать значение:
index.ts(7,22): error TS2322: Type 'string' is not assignable to type 'never'.
Ошибка указывает на объект, а не на тип, который её вызвал, поэтому такие ошибки долго ищут. Если навести курсор на r.id в редакторе, виден его тип: never. Когда конфликтующее свойство это литеральный тег, как в type Shape = { kind: "circle" } & { kind: "square" }, TypeScript идёт дальше и сводит всё пересечение к never. Тогда чтение свойства такого значения объясняет причину: Property 'kind' does not exist on type 'never'. The intersection 'Shape' was reduced to 'never' because property 'kind' has conflicting types in some constituents.
interface ... extends ловит тот же конфликт сразу в объявлении, с ошибкой TS2430. Это главное практическое различие между ними; на странице interface или type они сравниваются бок о бок.
Совместимое пересечение сужает свойство
Если типы двух свойств пересекаются, результатом будет их общая часть. Это полезно, а не ошибка:
Вторая половина показывает, что & делает с объединениями: он оставляет члены, общие для обеих сторон. Если думать о типах как о множествах значений, это становится предсказуемым. A | B это объединение двух множеств, A & B их пересечение, а пустое пересечение это never.
Пересечение и объединение
Названия взяты из теории множеств, и в применении к свойствам объектов они кажутся перевёрнутыми:
A | B (объединение) | A & B (пересечение) | |
|---|---|---|
| Значение это | A или B | A и B |
| Множество допустимых значений | больше | меньше |
| Какие свойства можно использовать | только те, что есть в обоих | все из любого |
string с number | string | number | never |
"a" | "b" с "b" | "c" | "a" | "b" | "c" | "b" |
У пересечения объектных типов больше свойств именно потому, что оно допускает меньше значений: только объекты, у которых есть всё.
Пересечение и extends
type C = A & B | interface C extends A, B | |
|---|---|---|
| Работает с | любыми типами, включая объединения и параметры типа | объектными типами со статически известными членами |
| Конфликтующее свойство | молча становится never | ошибка TS2430 или TS2320 в объявлении |
| Результат | пересечение, проверяемое по каждой составляющей | один плоский именованный тип, отношения которого кешируются |
| Большие составные типы | могут замедлять проверку типов | рекомендуются на странице Performance вики TypeScript |
Для объединения нескольких псевдонимов объектных типов & идиоматичен и вполне годится. Для типа из многих частей или типа публичного API extends даёт более ранние ошибки и более дешёвую проверку типов.
Пересечения с примитивами: брендирование
Пересечение примитива с объектным типом не даёт never: string & { readonly __brand: "UserId" } это строка с дополнительной меткой, существующей только при компиляции. Ни у одной настоящей строки нет такого свойства, и в этом весь смысл: создать такое значение может только код, который намеренно утверждает бренд, поэтому обычный string или OrderId больше нельзя передать туда, где ожидается UserId. Этому приёму посвящена отдельная страница, брендированные типы.
Часто задаваемые вопросы
Что такое тип пересечения в TypeScript?
Тип вида A & B, значения которого должны одновременно соответствовать и A, и B. Для объектных типов это значит, что у значения есть каждое свойство A и каждое свойство B. Это обычный способ объединить два псевдонима типов в один.
Чем объединение отличается от пересечения?
Объединение A | B означает «одно из двух»: значение может быть A или B, и до сужения можно использовать только общее. Пересечение A & B означает «оба»: у значения есть всё от обоих. Для объектных типов объединение принимает больше значений, а у пересечения больше свойств.
Почему мой тип пересечения стал never?
Потому что никакое значение не удовлетворяет обеим сторонам. string & number это never, а { id: string } & { id: number } делает id типом string & number, поэтому свойство становится never, и создать объект нельзя. Если у двух объектных типов один литеральный тег с разными значениями (kind: "circle" и kind: "square"), всё пересечение сводится к never.
Что использовать: пересечение или extends?
Оба объединяют объектные типы. interface X extends A, B сообщает о конфликтующих свойствах в объявлении, и команда TypeScript рекомендует его для составления больших объектных типов. & работает с любым типом, включая объединения и параметры дженериков, которые extends объединить не может. Используйте & для псевдонимов типов и обобщённых вспомогательных типов, а extends при построении интерфейсов.
Как слить два объектных типа в TypeScript?
Напишите type Merged = A & B. Для значения во время выполнения разверните оба объекта: const merged: A & B = { ...a, ...b }. Если у A и B есть общее свойство с разными типами, тип этого свойства станет never; используйте Omit<A, keyof B> & B, если свойства второго объекта должны заменять свойства первого.