Type narrowing com switch(true) e pattern matching proposals
1. Introdução ao Type Narrowing em TypeScript
Type narrowing é o processo pelo qual o TypeScript reduz um tipo amplo para um tipo mais específico dentro de um bloco de código. É essencial para escrever código type-safe, permitindo que o compilador entenda exatamente qual tipo está sendo manipulado em cada contexto.
As técnicas tradicionais incluem:
// typeof narrowing
function process(value: string | number) {
if (typeof value === "string") {
return value.toUpperCase(); // TypeScript sabe que é string
}
return value.toFixed(2); // TypeScript sabe que é number
}
// instanceof narrowing
class Dog { bark() {} }
class Cat { meow() {} }
function makeSound(animal: Dog | Cat) {
if (animal instanceof Dog) {
animal.bark();
} else {
animal.meow();
}
}
// Discriminated unions
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number };
function area(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2;
}
return shape.side ** 2;
}
No entanto, essas técnicas mostram limitações quando enfrentamos condições complexas que não se encaixam perfeitamente em typeof, instanceof ou discriminated unions.
2. Switch(true) como Padrão de Narrowing Avançado
O padrão switch(true) explora a capacidade do TypeScript de realizar narrowing dentro de blocos case que contêm expressões booleanas:
type ApiResponse =
| { status: "success"; data: unknown }
| { status: "error"; message: string; code: number }
| { status: "loading"; progress: number }
| null
| undefined;
function handleResponse(response: ApiResponse): string {
switch (true) {
case response === null:
return "Response is null";
case response === undefined:
return "Response is undefined";
case response.status === "success":
return `Data: ${JSON.stringify(response.data)}`;
case response.status === "error":
return `Error ${response.code}: ${response.message}`;
case response.status === "loading":
return `Loading... ${response.progress}%`;
default:
return "Unknown response state";
}
}
O TypeScript infere corretamente o tipo em cada case porque a expressão booleana atua como um type guard, estreitando o tipo da variável no escopo do bloco.
3. Vantagens e Desvantagens do Switch(true)
Vantagens:
// Legibilidade superior vs if-else aninhados
type UserAction =
| { type: "view"; id: number }
| { type: "edit"; id: number; changes: Record<string, unknown> }
| { type: "delete"; id: number; confirm: boolean }
| { type: "create"; data: Record<string, unknown> };
// Com switch(true)
function executeAction(action: UserAction): void {
switch (true) {
case action.type === "view":
console.log(`Viewing item ${action.id}`);
break;
case action.type === "edit":
console.log(`Editing item ${action.id} with changes`);
break;
case action.type === "delete" && action.confirm:
console.log(`Deleting item ${action.id}`);
break;
case action.type === "create":
console.log("Creating new item");
break;
default:
console.log("Unknown action");
}
}
Desvantagens:
- Perda de escopo de narrowing em alguns casos complexos
- Verbosidade para padrões simples
- Necessidade de
breakexplícito (ao contrário de early returns)
// Comparação com if-else e early returns
function executeActionWithEarlyReturn(action: UserAction): void {
if (action.type === "view") {
return console.log(`Viewing item ${action.id}`);
}
if (action.type === "edit") {
return console.log(`Editing item ${action.id}`);
}
// ...
}
4. Pattern Matching Proposals no TypeScript
O repositório do TypeScript possui várias propostas relacionadas a pattern matching. A mais notável é a proposta de when clauses:
// Sintaxe proposta (não implementada)
type Result<T> =
| { ok: true; value: T }
| { ok: false; error: Error };
function handleResult<T>(result: Result<T>) {
match (result) {
when { ok: true, value: v } => console.log(v);
when { ok: false, error: e } => console.error(e);
}
}
Outras propostas incluem case patterns em switch statements e expressões match com suporte a destructuring. Atualmente, nenhuma dessas propostas foi implementada na linguagem principal.
5. Simulando Pattern Matching com Type Guards e Switch(true)
Podemos criar type guards customizados para simular pattern matching:
// Type guards customizados
function isSuccessResponse<T>(response: ApiResponse): response is { status: "success"; data: T } {
return response !== null && response !== undefined && response.status === "success";
}
function isErrorResponse(response: ApiResponse): response is { status: "error"; message: string; code: number } {
return response !== null && response !== undefined && response.status === "error";
}
// Combinando com switch(true)
type PaymentPayload =
| { method: "credit"; cardNumber: string; cvv: string }
| { method: "pix"; key: string }
| { method: "boleto"; dueDate: Date };
function validatePayment(payload: PaymentPayload): boolean {
switch (true) {
case payload.method === "credit":
return payload.cardNumber.length === 16 && payload.cvv.length === 3;
case payload.method === "pix":
return payload.key.includes("@") || payload.key.length === 11;
case payload.method === "boleto":
return payload.dueDate > new Date();
default:
return false;
}
}
6. Exaustividade e Type Safety com Switch(true)
Para garantir que todos os casos sejam cobertos, utilizamos o tipo never:
function assertNever(value: never): never {
throw new Error(`Unexpected value: ${value}`);
}
type Direction = "up" | "down" | "left" | "right";
function move(direction: Direction): string {
switch (true) {
case direction === "up":
return "Moving up";
case direction === "down":
return "Moving down";
case direction === "left":
return "Moving left";
case direction === "right":
return "Moving right";
default:
return assertNever(direction); // Erro em tempo de compilação se não exaustivo
}
}
Se adicionarmos um novo membro ao tipo Direction sem atualizar o switch, o TypeScript apontará erro no assertNever.
7. Padrões Emergentes e Boas Práticas
Quando preferir switch(true):
- Múltiplas condições que não são mutuamente exclusivas via discriminated unions
- Validação de payloads com formatos variados
- Narrowing baseado em múltiplas propriedades
Organização de código:
// Extraindo condições em funções auxiliares
function isPositiveNumber(value: unknown): value is number {
return typeof value === "number" && value > 0;
}
function isValidString(value: unknown): value is string {
return typeof value === "string" && value.length > 0;
}
function processInput(input: unknown): string {
switch (true) {
case isPositiveNumber(input):
return `Number: ${input * 2}`;
case isValidString(input):
return `String: ${input.toUpperCase()}`;
default:
return "Invalid input";
}
}
Cuidados com desempenho: Em loops críticos, switch(true) pode ser ligeiramente mais lento que if-else devido à avaliação de expressões booleanas. Para a maioria dos casos, porém, a diferença é insignificante.
8. Conclusão e Próximos Passos
O padrão switch(true) oferece uma maneira elegante de realizar type narrowing em cenários complexos, especialmente quando combinado com type guards customizados. Embora não substitua completamente o pattern matching nativo que pode vir no futuro, ele preenche uma lacuna importante na expressividade do TypeScript atual.
Para se aprofundar, explore temas vizinhos como o operador satisfies, type guards com predicados de tipo, e const type parameters, que expandem ainda mais as capacidades de type safety da linguagem.
Referências
- TypeScript Handbook: Narrowing — Documentação oficial sobre todas as técnicas de type narrowing em TypeScript
- TypeScript Issue #165: Pattern Matching Proposal — Proposta oficial de pattern matching no repositório do TypeScript
- TypeScript Deep Dive: Type Guards — Guia abrangente sobre type guards e narrowing por Basarat Ali Syed
- Pattern Matching in TypeScript with switch(true) — Artigo técnico de Ben Ilegbodu demonstrando o padrão switch(true)
- TypeScript 5.0: const Type Parameters — Anúncio oficial do TypeScript 5.0 com const type parameters, tema relacionado
- Effective TypeScript: Item 22 - Narrowing — Dicas práticas de narrowing por Dan Vanderkam, autor do livro "Effective TypeScript"