5 min

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.