TypeScript Patterns que Uso Todo Dia
Patterns e técnicas de TypeScript que melhoram a legibilidade e segurança do código.
- TypeScript
- Patterns
Depois de alguns anos escrevendo TypeScript em produção, alguns patterns se tornaram automáticos. Não são truques obscuros - são técnicas práticas que melhoram a qualidade do código no dia a dia.
Discriminated Unions para estados
Esse é provavelmente o pattern mais útil. Em vez de flags booleanas espalhadas, use union types com um campo discriminador.
// Ruim: múltiplas flags, estados impossíveis são possíveis
interface RequestState {
isLoading: boolean;
isError: boolean;
data: User | null;
error: Error | null;
}
// Problema: isLoading: true E isError: true é válido?
// Bom: estados são mutuamente exclusivos
type RequestState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: User }
| { status: 'error'; error: Error };
function UserProfile({ state }: { state: RequestState }) {
switch (state.status) {
case 'idle':
return <Empty />;
case 'loading':
return <Spinner />;
case 'success':
return <Profile user={state.data} />; // data existe aqui
case 'error':
return <Error message={state.error.message} />; // error existe aqui
}
}
O TypeScript garante que você trate todos os casos e que os dados certos existam em cada estado.
Branded Types para IDs
IDs são strings, mas nem toda string é um ID válido. Branded types previnem que você passe o ID errado.
// Cria um "brand" único para cada tipo de ID
type UserId = string & { readonly __brand: 'UserId' };
type OrderId = string & { readonly __brand: 'OrderId' };
// Funções helper para criar IDs
function createUserId(id: string): UserId {
return id as UserId;
}
function createOrderId(id: string): OrderId {
return id as OrderId;
}
// Agora o TypeScript impede erros
function getUser(id: UserId) { /* ... */ }
function getOrder(id: OrderId) { /* ... */ }
const userId = createUserId('user_123');
const orderId = createOrderId('order_456');
getUser(userId); // OK
getUser(orderId); // Erro de tipo!
satisfies para validação sem perder inferência
O operador satisfies valida que um valor corresponde a um tipo sem alterar a inferência.
// Sem satisfies: perdemos os valores literais
const config: Record<string, string> = {
apiUrl: 'https://api.example.com',
timeout: '5000',
};
// config.apiUrl é string, não 'https://api.example.com'
// Com satisfies: validamos E mantemos a inferência
const config = {
apiUrl: 'https://api.example.com',
timeout: '5000',
} satisfies Record<string, string>;
// config.apiUrl é 'https://api.example.com'
// E ainda temos autocomplete para as keys
Útil especialmente para objetos de configuração e constantes.
as const para objetos imutáveis
Quando você quer que um objeto seja completamente readonly e mantenha tipos literais:
// Sem as const
const ROUTES = {
home: '/',
about: '/about',
blog: '/blog',
};
// typeof ROUTES.home = string
// Com as const
const ROUTES = {
home: '/',
about: '/about',
blog: '/blog',
} as const;
// typeof ROUTES.home = '/'
// Útil para criar tipos a partir de valores
type Route = typeof ROUTES[keyof typeof ROUTES];
// Route = '/' | '/about' | '/blog'
Template Literal Types para strings tipadas
Combine strings de forma type-safe:
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type ApiVersion = 'v1' | 'v2';
type Endpoint = `/${ApiVersion}/${string}`;
// Crie event names tipados
type DomainEvent = 'user' | 'order' | 'payment';
type EventAction = 'created' | 'updated' | 'deleted';
type EventName = `${DomainEvent}.${EventAction}`;
// EventName = 'user.created' | 'user.updated' | ... (9 combinações)
function emit(event: EventName, payload: unknown) {
// ...
}
emit('user.created', { id: '123' }); // OK
emit('user.something', {}); // Erro!
Mapped Types para transformações
Transforme tipos existentes de forma sistemática:
// Torna todas as propriedades opcionais e nullable
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
// Cria versão "patch" de um tipo (para updates parciais)
type Patch<T> = {
[K in keyof T]?: T[K];
};
// Remove campos específicos
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
// Exemplo prático: form state
interface User {
id: string;
name: string;
email: string;
}
type UserFormState = Omit<User, 'id'>; // { name: string; email: string }
type UserPatch = Patch<User>; // { id?: string; name?: string; email?: string }
infer para extrair tipos
Extraia tipos de dentro de outros tipos:
// Extrair tipo de retorno de função
type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never;
// Extrair tipo de Promise
type Awaited<T> = T extends Promise<infer U> ? U : T;
// Extrair props de componente React
type PropsOf<T> = T extends React.ComponentType<infer P> ? P : never;
// Extrair tipo de array
type ElementOf<T> = T extends (infer E)[] ? E : never;
// Uso
async function fetchUser(): Promise<User> { /* ... */ }
type FetchedUser = Awaited<ReturnType<typeof fetchUser>>; // User
O pattern que mais uso: Result type
Para funções que podem falhar, em vez de throw/catch:
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
function ok<T>(value: T): Result<T, never> {
return { ok: true, value };
}
function err<E>(error: E): Result<never, E> {
return { ok: false, error };
}
// Uso
async function parseConfig(path: string): Promise<Result<Config, string>> {
try {
const content = await readFile(path);
const config = JSON.parse(content);
return ok(config);
} catch {
return err(`Failed to parse config at ${path}`);
}
}
// Chamador é forçado a lidar com o erro
const result = await parseConfig('./config.json');
if (!result.ok) {
console.error(result.error);
process.exit(1);
}
// Aqui result.value existe e é Config
Esses patterns não são sobre ser “esperto” com tipos. São sobre usar o sistema de tipos para prevenir bugs antes que aconteçam. O compilador vira seu primeiro code reviewer.