O que são tipos avançados de TypeScript?
Tipos avançados são as ferramentas que permitem transformar, compor e manipular tipos no nível de tipagem. Em vez de definir manualmente tipos paralelos para cada variação, você os deriva automaticamente de tipos já existentes.
Se o básico fornece os tipos e os generics permitem escrever código reutilizável, os tipos avançados permitem construir um sistema de tipos que descreve seus dados com precisão — capturando erros que abordagens mais simples deixariam passar.
Os três pilares são utility types (transformações nativas), conditional types (lógica no nível de tipo) e mapped types (transformação de propriedades). Juntos, eles formam uma pequena linguagem para trabalhar com tipos.
Utility types
O TypeScript vem com tipos integrados que modificam outros tipos:
Partial e Required
interface User {
id: number;
name: string;
email: string;
}
// All properties optional
type UserUpdate = Partial<User>;
// { id?: number; name?: string; email?: string; }
// All properties required
type StrictUser = Required<Partial<User>>;
// { id: number; name: string; email: string; }
Pick e Omit
// Extract specific properties
type UserPreview = Pick<User, "id" | "name">;
// { id: number; name: string; }
// Remove specific properties
type UserWithoutEmail = Omit<User, "email">;
// { id: number; name: string; }
Record
// Create object type with specific keys and values
type Scores = Record<string, number>;
// { [key: string]: number }
const scores: Scores = { math: 95, science: 87 };
// With union keys
type StatusMap = Record<"idle" | "loading" | "done", string>;
Extract e Exclude
type Status = "idle" | "loading" | "success" | "error";
// Keep only matching members
type ActiveStatus = Extract<Status, "loading" | "success">;
// "loading" | "success"
// Remove matching members
type InactiveStatus = Exclude<Status, "loading" | "success">;
// "idle" | "error"
ReturnType e Parameters
function createUser(name: string, age: number) {
return { name, age, id: Date.now() };
}
type User = ReturnType<typeof createUser>;
// { name: string; age: number; id: number }
type Args = Parameters<typeof createUser>;
// [string, number]
NonNullable
type Maybe = string | null | undefined;
type Definite = NonNullable<Maybe>;
// string
Mapped types
Mapped types iteram sobre as chaves de um tipo existente e transformam cada propriedade:
type ReadOnly<T> = {
readonly [K in keyof T]: T[K];
};
type MutableUser = ReadOnly<User>;
// All properties are readonly
Adicionando modificadores
// Add optional
type Optional<T> = {
[K in keyof T]?: T[K];
};
// Remove optional
type Concrete<T> = {
[K in keyof T]-?: T[K];
};
// Remove readonly
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
Os operadores - e + removem ou adicionam modificadores durante o mapeamento.
Remapeamento de chaves com as
O TypeScript 4.1+ permite que você remapeie chaves durante o mapeamento:
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<User>;
// { getId: () => number; getName: () => string; getEmail: () => string; }
Tipos condicionais
Tipos condicionais expressam lógica no nível de tipos:
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>; // false
A palavra-chave infer
infer extrai um tipo de dentro de outro tipo:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type Fn = () => string;
type Result = ReturnType<Fn>; // string
Exemplos práticos
// Extract array element type
type ElementOf<T> = T extends (infer E)[] ? E : never;
type Num = ElementOf<number[]>; // number
// Unwrap promises
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
type Deep = Awaited<Promise<Promise<string>>>; // string
// Extract object property types
type ValuesOf<T> = T[keyof T];
type UserValues = ValuesOf<User>; // number | string
Tipos condicionais distributivos
Quando um tipo condicional é distribuído sobre uma union, cada membro é verificado separadamente:
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>;
// string[] | number[] — not (string | number)[]
Isso é útil para tipos como Extract e Exclude que filtram membros de uma union.
Template literal types
Template literal types manipulam strings no nível de tipo:
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<"click">; // "onClick"
type FocusEvent = EventName<"focus">; // "onFocus"
Com mapped types
type EventHandlers<T extends string> = {
[K in T as `on${Capitalize<K>}`]: () => void;
};
type MouseEvents = EventHandlers<"click" | "hover" | "focus">;
// { onClick: () => void; onHover: () => void; onFocus: () => void; }
String pattern matching
type CSSProperty = `${string}-${string}`;
type ValidCSS = CSSProperty; // any string with a hyphen
Discriminated unions
Discriminated unions são o padrão avançado mais importante para a modelagem de estado:
type RequestState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: string };
function handleState(state: RequestState<User>) {
switch (state.status) {
case "idle":
return "Ready";
case "loading":
return "Loading...";
case "success":
return state.data.name; // TypeScript knows data exists
case "error":
return state.error; // TypeScript knows error exists
}
}
A propriedade status é o discriminante. O TypeScript faz o narrowing do tipo automaticamente em cada ramificação.
Branded types
Branded types evitam a mistura de tipos que possuem a mesma estrutura:
type UserId = string & { readonly __brand: unique symbol };
type OrderId = string & { readonly __brand: unique symbol };
function branded<T extends string>(value: T): T & { readonly __brand: unique symbol } {
return value as any;
}
const userId = branded<UserId>("user-123");
const orderId = branded<OrderId>("order-456");
function getUser(id: UserId) { /* ... */ }
getUser(userId); // OK
getUser(orderId); // Error: OrderId is not assignable to UserId
Melhores práticas
- Use utility types em vez de duplicar definições de tipos manualmente.
- Prefira discriminated unions em vez de type assertions para a modelagem de estados.
- Construa tipos a partir de peças pequenas e compostíveis, em vez de definições monolíticas e extensas.
- Use conditional types com moderação — se um tipo estiver difícil de ler, simplifique a API.
- Use
inferpara extrair tipos de bibliotecas em vez de tipar manualmente os valores de retorno. - Mantenha a fonte da verdade em um único lugar e derive todo o restante.
- Use branded types quando precisar de nominal typing para IDs ou valores semelhantes.
- Trate tipos complexos como último recurso — tipos simples são mais fáceis de manter.
Erros comuns
- Over-engineering de tipos quando tipos mais simples resolveriam.
- Criar tipos condicionais profundamente aninhados que são impossíveis de depurar.
- Usar asserções
asem vez de um type narrowing adequado. - Não utilizar utility types, resultando em definições de tipos duplicadas.
- Tornar os tipos complexos demais para o benefício que proporcionam.
- Esquecer que os tipos são removidos no runtime — não há verificação de tipos em tempo de execução.
- Não documentar tipos complexos com comentários que expliquem a intenção.
O que aprender a seguir
Agora você compreende tipos avançados: utility types, conditional types, mapped types e template literals. A partir daqui, o próximo passo natural é TypeScript para os fundamentos — annotations, inference, interfaces e generics. Escolha um projeto, refatore alguns tipos e deixe a prática acumular.