Menu

Intersection types em TypeScript: combinar tipos com &

Um intersection type A & B descreve um valor que é A e B ao mesmo tempo, então tem todos os membros dos dois. Veja como combinar tipos de objeto com &, por que propriedades conflitantes viram never, como intersections de unions mantêm só os membros em comum e quando usar extends no lugar.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

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 Bum A e um B
Conjunto de valores permitidosmaiormenor
Propriedades que você pode usarsó as que estão nos doistodas de qualquer um
string com numberstring | numbernever
"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 & Binterface C extends A, B
Funciona comqualquer tipo, incluindo unions e type parameterstipos de objeto com membros conhecidos estaticamente
Propriedade conflitantevira never em silêncioerro TS2430 ou TS2320 na declaração
Resultadouma intersection, verificada componente por componenteum único tipo plano com nome, com as relações em cache
Composições grandespodem deixar a verificação de tipos lentapreferido 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.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR