Um intersection type, escrito A & B, descreve um valor que é um A e um B ao mesmo tempo. Para tipos de objeto, isso significa que o valor tem todas as propriedades dos dois. É assim que você combina tipos existentes sem reescrever as propriedades.
O segundo objeto deixa de fora team, então gera o erro de compilação TS2322, e a linha seguinte da mensagem diz Property 'team' is missing ... but required in type 'Employee'. Um valor Staff pode ser passado em qualquer lugar em que um Person ou um Employee é esperado.
Combinando tipos de objeto
O & funciona com qualquer mistura de type aliases, interfaces e tipos de objeto inline, e com type parameters genéricos. Esse último caso é onde ele é difícil de substituir: uma função que acrescenta propriedades a qualquer objeto que receba consegue dizer isso com precisão.
Quem chama mantém o tipo exato do que passou (title, words) mais as duas propriedades acrescentadas. Um interface ... extends não consegue expressar "seja o que for T, mais isto", porque uma interface não pode estender um type parameter.
Propriedades conflitantes viram never
Quando os dois lados declaram a mesma propriedade, o tipo dela é a intersection dos dois. Se esses tipos não têm nenhum valor em comum, a propriedade vira never, e o compilador não diz nada até você tentar criar um valor:
index.ts(7,22): error TS2322: Type 'string' is not assignable to type 'never'.
O erro aponta para o objeto, não para o tipo que o causou, e por isso esses bugs são lentos de rastrear. Passar o mouse sobre r.id no editor mostra o tipo dele: never. Quando a propriedade conflitante é uma tag literal, como em type Shape = { kind: "circle" } & { kind: "square" }, o TypeScript vai além e reduz a intersection inteira a never. Ler uma propriedade desse valor então explica o motivo: 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 pega o mesmo conflito já na declaração, com o erro TS2430. Essa é a principal diferença prática entre os dois; a página sobre interface vs type os compara lado a lado.
Sobreposições compatíveis estreitam a propriedade
Se os dois tipos da propriedade se sobrepõem, o resultado é a sobreposição. Isso é útil, não um erro:
A segunda metade mostra o que o & faz com unions: mantém os membros que os dois lados têm em comum. Pensar nos tipos como conjuntos de valores torna isso previsível. A | B é a união dos dois conjuntos, A & B é a sobreposição deles, e uma sobreposição vazia é never.
Intersection vs union
Os nomes vêm da teoria dos conjuntos, e parecem invertidos quando aplicados a propriedades de objeto:
A | B (union) | A & B (intersection) | |
|---|---|---|
| Um valor é | um A ou um B | um A e um B |
| Conjunto de valores permitidos | maior | menor |
| Propriedades que você pode usar | só as que estão nos dois | todas de qualquer um |
string com number | string | number | never |
"a" | "b" com "b" | "c" | "a" | "b" | "c" | "b" |
Uma intersection de tipos de objeto tem mais propriedades justamente porque permite menos valores: só objetos que têm tudo.
Intersection vs extends
type C = A & B | interface C extends A, B | |
|---|---|---|
| Funciona com | qualquer tipo, incluindo unions e type parameters | tipos de objeto com membros conhecidos estaticamente |
| Propriedade conflitante | vira never em silêncio | erro TS2430 ou TS2320 na declaração |
| Resultado | uma intersection, verificada componente por componente | um único tipo plano com nome, com as relações em cache |
| Composições grandes | podem deixar a verificação de tipos lenta | preferido pela página Performance da wiki do TypeScript |
Para combinar alguns type aliases de objeto, o & é idiomático e funciona bem. Para um tipo montado a partir de muitas partes, ou um tipo de API pública, o extends dá erros mais cedo e verificações de tipo mais baratas.
Intersections com primitivos: branding
Fazer a intersection de um primitivo com um tipo de objeto não produz never: string & { readonly __brand: "UserId" } é uma string que carrega uma marca extra, que só existe em tempo de compilação. Nenhuma string real tem essa propriedade, e é exatamente essa a ideia: só código que afirma a marca de propósito pode criar uma, então uma string simples ou um OrderId não podem mais ser passados onde um UserId é esperado. Essa técnica tem uma página própria, branded types.
Perguntas frequentes
O que é um intersection type no TypeScript?
Um tipo escrito A & B cujos valores precisam satisfazer A e B ao mesmo tempo. Para tipos de objeto, isso significa que o valor tem todas as propriedades de A e todas as propriedades de B. É o jeito usual de combinar dois type aliases em um.
Qual é a diferença entre uma union e uma intersection?
Uma union A | B significa "um ou outro": o valor pode ser um A ou um B, e você só pode usar o que eles compartilham até fazer o narrowing. Uma intersection A & B significa "os dois": o valor tem tudo dos dois. Com tipos de objeto, a union aceita mais valores e a intersection tem mais propriedades.
Por que meu intersection type virou never?
Porque nenhum valor consegue satisfazer os dois lados. string & number é never, e { id: string } & { id: number } faz de id um string & number, então a propriedade é never e nenhum objeto pode ser criado. Se dois tipos de objeto têm a mesma tag literal com valores diferentes (kind: "circle" e kind: "square"), a intersection inteira se reduz a never.
Devo usar uma intersection ou extends?
Os dois combinam tipos de objeto. interface X extends A, B aponta propriedades conflitantes na declaração e é recomendado pelo time do TypeScript para compor tipos de objeto grandes. O & funciona com qualquer tipo, incluindo unions e type parameters genéricos, que o extends não consegue combinar. Use & para type aliases e helpers genéricos, e extends ao montar interfaces.
Como juntar dois tipos de objeto no TypeScript?
Escreva type Merged = A & B. Para o valor em tempo de execução, espalhe os dois objetos: const merged: A & B = { ...a, ...b }. Se A e B compartilham uma propriedade com tipos diferentes, o tipo vira never para essa propriedade; use Omit<A, keyof B> & B quando as propriedades do segundo objeto devem substituir as do primeiro.