Photo by Dan Cristian Pădureț on Unsplash

Uma palhinha sobre Union/Intersection Types em TypeScript

Mateus M.
4 min readApr 26, 2023

--

Ah, o tal do TypeScript. Ame ou odeie, é inegável que ele está cada vez mais presente em projetos de todo porte, e ainda que baste um pouco de vivência e mão na massa, muita gente (eu incluso) acaba confundindo alguns conceitos por terem sintaxes diferentes às daquelas ensinadas na faculdade ou mesmo usadas em outras linguagens. Quer um exemplo? Como você leria o trecho abaixo?

const fn = (x: number | string)...

Se você leu que o parâmetro “x pode ser number ou string”, não deixa de estar certo, mas pode ser um tiro no pé. Matematicamente falando, isso na verdade (🤓) é uma união entre os tipos number e string , o que significa que algumas coisas não vão funcionar como você imagina e isso não é exatamente um operador lógico como o nosso querido ||. Pra piorar, na matemática o | significa “de forma que”, e geralmente precede um predicado (ex: x.valor | x < 5). Ter uma base de set theory ajuda muito ao lidar com linguagens tipadas e conjuntos de tipos, então sim, isso significa estudar um pouquinho de álgebra. Um dia faço um texto sobre como não se frustrar tanto estudando conceitos teóricos e abstratos, porque é frustrante e vai fazer você se sentir como no primeiro dia que começou a programar, principalmente os autodidatas como eu. Enfim, divagando. Vou usar termos mais apropriados ao contexto da programação ao invés dos matemáticos.

Union Types

Uma união é um subtipo que compreende apenas os elementos em comum entre cada tipo. O próprio handbook tem uma excelente analogia sobre isso:

For example, if we had a room of tall people wearing hats, and another room of Spanish speakers wearing hats, after combining those rooms, the only thing we know about every person is that they must be wearing a hat.

Ou seja, se você pedir pra alguém de chapéu falar em espanhol, a pessoa pode dar um erro (???), mas se pedir para qualquer um ali tirar o chapéu (de alguma forma que todos entendam), vai funcionar!

Vamos voltar ao number | string . O que você acha que vai acontecer se definirmos a função lá de cima assim?

const fn = (x: number | string) => {
return x.toUpperCase()
}

Sim, o TypeScript não vai gostar da ideia. Já no próprio editor de texto ele deve reclamar algo como: “Property ‘toUpperCase’ does not exist on type ‘string | number’”. Isso porque o seu parâmetro x só “entende” métodos em comum entre números e strings (spoiler: não existe). O tipo number não tem um toUpperCase, assim como o tipo string não tem um toFixed. Esse problema é extremamente comum e em types de outras bibliotecas/frameworks pode resultar em comportamentos confusos. O erro semelhante de array por exemplo costuma te dizer que tem umas 900 propriedades faltando — isso se dá porque o outro tipo provavelmente não é um array!

Tá, e como eu boto essa função boba aí pra funfar?

Com uma técnica sugerida pelo próprio handbook, chamada narrowing. Consiste em checar o tipo do argumento passado à função em sua chamada com a keyword typeof e então executar o fluxo desejado. Não é muito elegante mas se tratando de tipos primitivos é o que tem pra hoje.

const fn = (x: number | string) => {
if (typeof x === "number") return x
else return x.toUpperCase()
}

Intersection Types

A título de curiosidade, também temos o operador & , que nos dá a intersecção entre tipos, ou seja: number & string resultaria em um tipo que simultaneamente satisfaz todas propriedades de ambos os primitivos. E isso é impossível! Lembra que não existem elementos em comum entre os dois? Quando uma intersecção é impossível, o TS automaticamente o transforma em um tipo never , que na matemática seria o símbolo ∅, que significa um conjunto vazio. Você pode ver esse cara com frequência caso configure seu TS para não permitir any .

Agora se nós temos o seguinte tipo:

interface Person {
name: string;
age: number;
greet(): string;
}

interface Skill {
name: string;
level: number;
showOff(): void;
}

type SkilledPerson = Person & Skill

Significa que ao declarar um objeto com o tipo SkilledPerson , ele precisa implementar todas as propriedades e métodos de ambas as interfaces Person e Skill. Uma SkilledPerson não pode ser nem apenas Skill nem apenas Person , nem parcialmente os dois.

Agora, e se SkilledPerson fosse um union type?

type SkilledPerson = Person | Skill

Podemos declarar um objeto do tipo facilmente:

const sinucaGod: SkilledPerson = {
name: "Baianinho de Mauá",
level: 999,
showOff() {
// return ...
},
greet() {
// return ...
}
}

Mas ao tentar acessar qualquer uma de suas propriedades teremos uma surpresa: apenas name estará disponível! Isso porque todos os outros elementos são distintos entre as interfaces, portanto não passam na união. Nesse caso o que precisamos não é uma união de tipos, mas uma extensão entre interfaces. 😉

E com isso aprendemos que os operadores |e & no contexto de TypeScript não funcionam igual os operadores lógicos, mas na verdade declaram novos tipos, os quais possuem algumas particularidades. Vou deixar também alguns links de interesse, já que esse assunto demanda um tempo pra entrar na cabeça (ainda estou absorvendo por exemplo), então paciência acima de tudo!

Refs:

--

--

Mateus M.

He/They. Web developer, writes and overshares mostly about mental health. Tech, cats and cute things. May write in EN or PT-BR.