Narrowing: refinamento de tipos com type guards
1. O que é Narrowing e por que precisamos dele?
Narrowing é o processo pelo qual o TypeScript reduz um tipo de união para um tipo mais específico dentro de um bloco de código. Quando declaramos uma variável como string | null, o TypeScript não nos permite acessar métodos específicos de string sem antes verificar se o valor não é nulo.
function processName(name: string | null) {
// Erro: Object is possibly 'null'
// return name.toUpperCase();
// Com narrowing, o TypeScript entende que name é string
if (name !== null) {
return name.toUpperCase(); // ✅ name é string aqui
}
return "default";
}
Diferente do type casting com as, que força um tipo sem verificação em tempo de execução, o narrowing oferece segurança:
const value: unknown = "Hello";
// Type casting (inseguro)
const length1 = (value as string).length; // Pode quebrar em runtime
// Narrowing (seguro)
if (typeof value === "string") {
const length2 = value.length; // ✅ TypeScript sabe que é string
}
2. Type Guards nativos do JavaScript
O typeof é o type guard mais básico e funciona para tipos primitivos:
function formatValue(value: string | number | boolean) {
if (typeof value === "string") {
return value.toUpperCase(); // ✅ value é string
}
if (typeof value === "number") {
return value.toFixed(2); // ✅ value é number
}
return value ? "true" : "false"; // ✅ value é boolean
}
O instanceof verifica se um objeto é instância de uma classe:
class ApiError {
constructor(public statusCode: number) {}
}
class ValidationError {
constructor(public field: string) {}
}
function handleError(error: ApiError | ValidationError) {
if (error instanceof ApiError) {
console.log(`Status: ${error.statusCode}`); // ✅
} else {
console.log(`Campo inválido: ${error.field}`); // ✅
}
}
Limitações importantes: typeof null retorna "object", e typeof não diferencia objetos personalizados.
3. Type Guards baseados em igualdade e truthiness
Operadores de igualdade refinam tipos comparando valores específicos:
function setTheme(theme: "light" | "dark" | "system") {
if (theme === "light") {
document.documentElement.setAttribute("data-theme", "light"); // ✅
} else if (theme === "dark") {
document.documentElement.setAttribute("data-theme", "dark"); // ✅
}
// theme é "system" aqui
}
Truthiness elimina valores falsy como null, undefined, 0, "":
function getLength(value: string | null | undefined) {
if (value) {
return value.length; // ✅ value é string (elimina null e undefined)
}
return 0;
}
O operador in verifica propriedades em objetos:
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(pet: Fish | Bird) {
if ("swim" in pet) {
pet.swim(); // ✅ pet é Fish
} else {
pet.fly(); // ✅ pet é Bird
}
}
4. Type Guards definidos pelo usuário (Type Predicates)
Criamos funções guarda personalizadas usando value is Type:
interface User {
name: string;
email: string;
}
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"name" in value &&
"email" in value
);
}
function processData(data: unknown) {
if (isUser(data)) {
console.log(data.name); // ✅ TypeScript sabe que é User
console.log(data.email); // ✅
}
}
Boas práticas: a função deve retornar booleano e a lógica deve ser consistente com a assinatura de tipo.
5. Narrowing com discriminated unions (uniões discriminadas)
Uniões discriminadas usam uma propriedade literal comum para identificar cada tipo:
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number }
| { kind: "triangle"; base: number; height: number };
function calculateArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2; // ✅ shape é circle
case "square":
return shape.side ** 2; // ✅ shape é square
case "triangle":
return (shape.base * shape.height) / 2; // ✅ shape é triangle
default:
// Exaustividade com never
const _exhaustive: never = shape;
return _exhaustive;
}
}
O tipo never no default garante que todos os casos foram tratados. Se adicionarmos um novo tipo sem tratá-lo, o TypeScript apontará erro.
6. Narrowing com never e assertion functions
O tipo never representa valores que nunca ocorrem:
function assertNever(value: never): never {
throw new Error(`Valor inesperado: ${value}`);
}
type Status = "pending" | "approved" | "rejected";
function processStatus(status: Status) {
switch (status) {
case "pending":
return "Aguardando...";
case "approved":
return "Aprovado!";
default:
return assertNever(status); // Erro se adicionar novo status sem tratá-lo
}
}
Assertion functions refinam tipos lançando erros:
function assertIsDefined<T>(value: T): asserts value is NonNullable<T> {
if (value === null || value === undefined) {
throw new Error("Valor não definido");
}
}
function processUser(user: User | null) {
assertIsDefined(user);
console.log(user.name); // ✅ user é User (não é null)
}
7. Narrowing avançado: type guards com genéricos e funções de alta ordem
Type guards genéricos criam funções reutilizáveis:
function hasProperty<T extends object, K extends keyof T>(
obj: T,
prop: K
): obj is T & Record<K, unknown> {
return prop in obj;
}
interface Car {
brand: string;
year: number;
}
const vehicle: unknown = { brand: "Toyota", year: 2023 };
if (hasProperty(vehicle, "brand")) {
console.log(vehicle.brand); // ✅ vehicle tem brand
}
Narrowing em arrays com Array.filter e type predicates:
function isString(value: unknown): value is string {
return typeof value === "string";
}
const mixed: (string | number)[] = ["hello", 42, "world", 100];
const strings: string[] = mixed.filter(isString); // ✅ ["hello", "world"]
Cuidados com narrowing em callbacks:
const items: (string | null)[] = ["a", null, "b"];
// ❌ Erro: filter não refina automaticamente
// const filtered: string[] = items.filter(item => item !== null);
// ✅ Correto: usar type predicate
const filtered: string[] = items.filter((item): item is string => item !== null);
Referências
- TypeScript Handbook: Narrowing — Documentação oficial completa sobre narrowing, type guards e discriminated unions
- TypeScript Deep Dive: Type Guards — Guia aprofundado com exemplos práticos de type guards personalizados
- TypeScript: Assertion Functions — Documentação oficial sobre assertion functions introduzidas no TypeScript 3.7
- TypeScript: Discriminated Unions — Explicação concisa sobre uniões discriminadas com exemplos
- TypeScript: Type Predicates — Seção do handbook sobre type predicates e funções guarda definidas pelo usuário