Union types e intersection types no TypeScript
1. Introdução aos Union Types
Union types permitem que uma variável, parâmetro ou retorno de função aceite mais de um tipo. A sintaxe usa o operador pipe (|) para declarar que o valor pode ser de qualquer tipo listado.
// Tipo que pode ser string ou number
type ID = string | number;
// Função que aceita múltiplos tipos
function formatInput(input: string | number): string {
return `Valor: ${input}`;
}
console.log(formatInput("abc")); // Funciona
console.log(formatInput(123)); // Funciona
// console.log(formatInput(true)); // Erro! boolean não está no union
Union types são ideais para criar APIs flexíveis, onde um parâmetro pode receber diferentes formatos de dados.
function setPadding(padding: number | string): void {
if (typeof padding === "number") {
console.log(`Padding: ${padding}px`);
} else {
console.log(`Padding: ${padding}`);
}
}
setPadding(10); // "Padding: 10px"
setPadding("2rem"); // "Padding: 2rem"
2. Trabalhando com Union Types na Prática
Para manipular valores de union types, precisamos usar type narrowing — reduzir o tipo para um mais específico.
Narrowing com typeof
function processValue(value: string | number | boolean): void {
if (typeof value === "string") {
// Aqui value é string
console.log(value.toUpperCase());
} else if (typeof value === "number") {
// Aqui value é number
console.log(value.toFixed(2));
} else {
// Aqui value é boolean
console.log(value ? "Verdadeiro" : "Falso");
}
}
Narrowing com in operator
type Cat = { meow: () => void };
type Dog = { bark: () => void };
function makeSound(animal: Cat | Dog): void {
if ("meow" in animal) {
animal.meow(); // TypeScript sabe que é Cat
} else {
animal.bark(); // TypeScript sabe que é Dog
}
}
Type guards personalizados
interface Fish {
swim: () => void;
layEggs: () => void;
}
interface Bird {
fly: () => void;
layEggs: () => void;
}
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
function move(pet: Fish | Bird): void {
if (isFish(pet)) {
pet.swim(); // TypeScript infere que é Fish
} else {
pet.fly(); // TypeScript infere que é Bird
}
}
3. Intersection Types: Combinando Tipos
Intersection types usam o operador & para combinar todos os membros de cada tipo em um único tipo. É o equivalente ao "E" lógico.
interface Person {
name: string;
age: number;
}
interface Employee {
employeeId: number;
department: string;
}
// Intersection: deve ter TODAS as propriedades
type StaffMember = Person & Employee;
const staff: StaffMember = {
name: "Alice",
age: 30,
employeeId: 12345,
department: "Engineering"
};
A diferença fundamental: union é "ou" (pode ser um tipo ou outro), intersection é "e" (deve ser todos os tipos simultaneamente).
// Union: ou um ou outro
type Status = "active" | "inactive"; // Pode ser apenas um valor
// Intersection: combina todos
type Complete = { a: number } & { b: string }; // Deve ter a E b
4. Intersection Types com Interfaces e Type Aliases
Podemos combinar interfaces e type aliases existentes para criar tipos mais específicos.
interface HasName {
name: string;
}
interface HasAge {
age: number;
}
interface HasEmail {
email: string;
}
// Combinando três interfaces
type User = HasName & HasAge & HasEmail;
const user: User = {
name: "Bob",
age: 25,
email: "bob@example.com"
};
Cuidados com conflitos de propriedades
Quando dois tipos no intersection têm a mesma propriedade com tipos diferentes, o resultado é um never (impossível de satisfazer).
type A = { value: string };
type B = { value: number };
// value é string & number = never
type Conflict = A & B;
// const x: Conflict = { value: "test" }; // Erro!
// const x: Conflict = { value: 123 }; // Erro!
5. Union Types com Objetos e Discriminated Unions
Discriminated unions usam uma propriedade literal comum (como type ou kind) para diferenciar objetos em um union.
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
interface Triangle {
kind: "triangle";
base: number;
height: number;
}
type Shape = Circle | Square | Triangle;
function calculateArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
case "triangle":
return (shape.base * shape.height) / 2;
default:
// Exaustividade: se adicionarmos nova forma, o TypeScript avisará
const _exhaustive: never = shape;
return _exhaustive;
}
}
Essa técnica é poderosa para pattern matching e garante que todos os casos sejam tratados.
6. Técnicas Avançadas de Combinação
Union com tipos literais
type Status = "pending" | "processing" | "completed" | "failed";
type HTTPCode = 200 | 201 | 400 | 401 | 500;
function getMessage(code: HTTPCode): string {
const messages: Record<HTTPCode, string> = {
200: "OK",
201: "Created",
400: "Bad Request",
401: "Unauthorized",
500: "Internal Server Error"
};
return messages[code];
}
Intersection com tipos genéricos
interface WithId {
id: string;
}
interface WithTimestamp {
createdAt: Date;
updatedAt: Date;
}
type Entity<T> = T & WithId & WithTimestamp;
interface Product {
name: string;
price: number;
}
type ProductEntity = Entity<Product>;
const product: ProductEntity = {
id: "abc-123",
name: "Laptop",
price: 1500,
createdAt: new Date(),
updatedAt: new Date()
};
Manipulando unions com Exclude e Extract
type AllTypes = string | number | boolean | null | undefined;
// Exclude remove tipos específicos
type NonNullableTypes = Exclude<AllTypes, null | undefined>;
// Resultado: string | number | boolean
// Extract mantém apenas os tipos especificados
type PrimitiveTypes = Extract<AllTypes, string | number | boolean>;
// Resultado: string | number | boolean
// Aplicação prática
function processValue(value: Exclude<AllTypes, null | undefined>): void {
// value nunca será null ou undefined
console.log(value.toString());
}
7. Boas Práticas e Armadilhas Comuns
Quando preferir union vs intersection
- Use union quando o valor pode ser um entre vários tipos (parâmetros flexíveis, estados alternativos)
- Use intersection quando precisa combinar todas as características de múltiplos tipos (composição de objetos)
Evitando unions muito grandes
// Ruim: union grande e difícil de manter
type Status = "a" | "b" | "c" | "d" | "e" | "f" | "g";
// Bom: agrupe por contexto
type LoadingStatus = "idle" | "loading" | "success" | "error";
type FormStatus = "draft" | "submitted" | "approved" | "rejected";
Problemas com any e unknown em combinações
// any anula o tipo seguro
type Unsafe = string | any; // Resultado: any
// unknown em intersection força verificação
type Safe = string & unknown; // Resultado: string (unknown é ignorado)
type Unsafe2 = string & any; // Resultado: any (any prevalece)
Dicas de legibilidade com type aliases nomeados
// Ruim: difícil de ler
function process(data: string | number | boolean | null): void {}
// Bom: use aliases descritivos
type InputValue = string | number | boolean | null;
function process(data: InputValue): void {}
Referências
- TypeScript Handbook: Union Types — Documentação oficial explicando union types com exemplos práticos
- TypeScript Handbook: Intersection Types — Seção oficial sobre intersection types e combinação de objetos
- TypeScript Deep Dive: Union and Intersection Types — Guia aprofundado com exemplos avançados de manipulação de tipos
- TypeScript Playground: Discriminated Unions — Exemplo interativo oficial de discriminated unions no playground
- Mariuss Schulz Blog: Union Types in TypeScript — Artigo técnico detalhado sobre union types com type guards e narrowing
- dev.to: Understanding Intersection Types in TypeScript — Tutorial prático sobre intersection types com comparações visuais