Frontend

TypeScript

Referência completa de TypeScript — tipos, generics, utility types, mapped types, conditional types, decorators, tsconfig e padrões avançados

Tipos básicos e inferência

TypeScript infere o tipo na maioria dos casos a partir da atribuição. Anotar explicitamente é mais útil em parâmetros de funções públicas, retornos não-óbvios e variáveis de domínio onde a intenção deve ser clara sem ler a implementação.

// Inferência automática — TypeScript deduz os tipos
const contagem = 0;           // number
const nome = "Rafael";        // string
const ativo = true;           // boolean
const lista = [1, 2, 3];      // number[]
const tupla = [1, "ok"] as const; // readonly [1, "ok"]

// Tipos primitivos explícitos
let status: string;
let total: number;
let habilitado: boolean;

// Tipos especiais
let nulo: null;
let indefinido: undefined;

// any — desabilita a checagem de tipos (evite)
let qualquerCoisa: any = 42;
qualquerCoisa = "string";   // sem erro — mas sem segurança

// unknown — recebe qualquer valor, mas exige narrowing antes de usar
let entrada: unknown = receberDaAPI();
if (typeof entrada === "string") {
  console.log(entrada.toUpperCase()); // TypeScript sabe que é string aqui
}

// never — tipo de valores que nunca ocorrem
function lancarErro(msg: string): never {
  throw new Error(msg);
}

// void — funções que não retornam valor significativo
function registrarLog(msg: string): void {
  console.log(msg);
}

// Anotação explícita em funções — sempre anotar parâmetros e retorno
function somar(a: number, b: number): number {
  return a + b;
}

// Arrow function tipada
const dobrar = (n: number): number => n * 2;

// Parâmetros opcionais e com valor padrão
function cumprimentar(nome: string, saudacao?: string): string {
  return `${saudacao ?? "Olá"}, ${nome}!`;
}

// Arrays e tuplas
const tags: string[] = ["typescript", "frontend"];
const tags2: Array<string> = ["typescript", "frontend"]; // forma alternativa
const ponto: [number, number] = [10, 20];
const entrada2: [string, number, boolean] = ["id", 1, true];

// Tuplas com nomes (TypeScript 4.0+)
type EventoHttp = [metodo: string, url: string, status: number];

Interfaces vs Type Aliases

interface descreve a forma de objetos e pode ser estendida e mesclada (declaration merging). type é mais versátil — suporta unions, intersections, tipos computados e literais.

// Interface — preferida para objetos, contratos de API, herança
interface Usuario {
  id: string;
  nome: string;
  email: string;
  avatarUrl?: string;          // opcional com ?
  readonly criadoEm: Date;     // somente leitura — não pode ser reatribuído
}

// Estendendo interface
interface UsuarioAdmin extends Usuario {
  permissoes: string[];
  nivel: 1 | 2 | 3;
}

// Estendendo múltiplas interfaces
interface EntidadeAuditada extends Usuario, { atualizadoEm: Date } {}

// Declaration merging — interfaces com o mesmo nome são mescladas
// Útil para augmentar tipos de bibliotecas
interface Window {
  APP_CONFIG: { apiUrl: string; version: string };
}

// Type alias — para unions, intersections e tipos computados
type Status = "pendente" | "pago" | "cancelado";  // union
type ID = string | number;

// Intersection — combina múltiplos tipos em um
type EntidadeBase = { id: string; criadoEm: Date };
type Produto = EntidadeBase & {
  nome: string;
  preco: number;
  estoque: number;
};

// Type pode representar qualquer tipo, interface só objetos
type Primitivo = string | number | boolean;
type Callback = (err: Error | null, dados: unknown) => void;
type Mapa<K extends string> = { [key in K]: number };

/*
  Quando usar cada um:
  - Interface: objetos de domínio, contratos de classe, APIs públicas de biblioteca
  - Type: unions, intersections, tipos utilitários, tipos computados, aliases de complexidade
  - Ambos são equivalentes para objetos simples — prefira consistência no projeto
*/

Union e Intersection Types

Unions expressam “um ou outro”; intersections expressam “ambos ao mesmo tempo”. Narrowing é a técnica de afunilar um union para um tipo específico.

type Tamanho = "pequeno" | "médio" | "grande";
type Resultado<T> = { ok: true; dados: T } | { ok: false; erro: string };

// Union com tipos complexos
type Notificacao =
  | { tipo: "email";  destinatario: string; assunto: string }
  | { tipo: "sms";    telefone: string }
  | { tipo: "push";   deviceToken: string; payload: object };

function enviarNotificacao(n: Notificacao): void {
  // Narrowing via discriminated union
  switch (n.tipo) {
    case "email":
      enviarEmail(n.destinatario, n.assunto);
      break;
    case "sms":
      enviarSms(n.telefone);
      break;
    case "push":
      enviarPush(n.deviceToken, n.payload);
      break;
  }
}

// Intersection — combina propriedades de múltiplos tipos
type ConfigBase = { timeout: number; retries: number };
type ConfigAuth = { token: string; refreshUrl: string };
type ClienteHttp = ConfigBase & ConfigAuth;

// Distribuição de union em generics
// Quando T é um union, operações são distribuídas sobre cada membro
type ToArray<T> = T extends unknown ? T[] : never;
type Strings = ToArray<string | number>;  // string[] | number[]

// Para evitar distribuição, use tupla
type ToArrayNaoDistribuido<T> = [T] extends [unknown] ? T[] : never;
type AmbosJuntos = ToArrayNaoDistribuido<string | number>; // (string | number)[]

Literal Types e Template Literal Types

Tipos literais restringem um valor a um conjunto exato de strings, números ou booleans. Template literal types constroem novos tipos de string via interpolação.

// Literal types
type Direcao = "norte" | "sul" | "leste" | "oeste";
type CodigoHTTP = 200 | 201 | 400 | 401 | 403 | 404 | 500;
type Booleano = true | false;  // equivalente a boolean

// const assertion — infere o tipo mais estreito possível
const config = {
  host: "localhost",
  porta: 3000,
} as const;
// config.host: "localhost" (literal), não string

// Template literal types
type EventoDOM = "click" | "focus" | "blur" | "change";
type HandlerEvento = `on${Capitalize<EventoDOM>}`;
// "onClick" | "onFocus" | "onBlur" | "onChange"

type CSSPropriedade = "margin" | "padding" | "border";
type CSSLado = "Top" | "Right" | "Bottom" | "Left";
type CSSPropriedadeExpandida = `${CSSPropriedade}${CSSLado}`;
// "marginTop" | "marginRight" | ... | "borderLeft"

// Casos práticos
type Rota = "/usuarios" | "/produtos" | "/pedidos";
type MetodoHTTP = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
type EndpointChave = `${MetodoHTTP} ${Rota}`;
// "GET /usuarios" | "POST /usuarios" | ...

// Interpolação com tipos genéricos
type Getter<T extends string> = `get${Capitalize<T>}`;
type Setter<T extends string> = `set${Capitalize<T>}`;
type Propriedades = "nome" | "email" | "idade";
type Getters = { [K in Propriedades as Getter<K>]: string };
// { getNome: string; getEmail: string; getIdade: string }

Enums

Enums agrupam valores relacionados com nomes legíveis. const enum é inlinado pelo compilador — sem overhead de runtime. Enums de string são os mais seguros para uso em APIs e debug.

// Enum numérico — valores auto-incrementados a partir de 0
enum Direcao {
  Norte,    // 0
  Sul,      // 1
  Leste,    // 2
  Oeste,    // 3
}

// Enum com valores iniciais customizados
enum CodigoHTTP {
  Ok = 200,
  Criado = 201,
  NaoEncontrado = 404,
  ErroInterno = 500,
}

// String enum — mais seguro e legível em debug/logs/APIs
enum Status {
  Pendente  = "PENDENTE",
  Pago      = "PAGO",
  Cancelado = "CANCELADO",
}

function processarPedido(status: Status): void {
  if (status === Status.Pago) { ... }
}

// const enum — completamente inlinado, sem código gerado em runtime
const enum Permissao {
  Ler    = 1,
  Gravar = 2,
  Admin  = 4,
}

const minhasPermissoes = Permissao.Ler | Permissao.Gravar; // compilado para: 1 | 2

/*
  Problemas conhecidos com enums:
  1. Enums numéricos permitem valor reverso: Direcao[0] === "Norte" — acesso inesperado
  2. Não funcionam bem com isolatedModules (esbuild, Vite) — use string enums ou const
  3. const enum com "isolatedModules" gera erro — prefira union de string literals

  Alternativa moderna (preferida por muitos projetos):
*/
const STATUS = {
  PENDENTE: "PENDENTE",
  PAGO: "PAGO",
  CANCELADO: "CANCELADO",
} as const;

type Status2 = typeof STATUS[keyof typeof STATUS];
// "PENDENTE" | "PAGO" | "CANCELADO"

Generics básicos

Generics permitem escrever código que funciona com qualquer tipo, mantendo a segurança em tempo de compilação. O tipo genérico é inferido automaticamente na maioria dos casos.

// Função genérica
function primeiro<T>(arr: T[]): T | undefined {
  return arr[0];
}

const n = primeiro([1, 2, 3]);     // TypeScript infere T = number
const s = primeiro(["a", "b"]);   // T = string

// Múltiplos tipos genéricos
function mapeamento<T, U>(arr: T[], fn: (item: T) => U): U[] {
  return arr.map(fn);
}

const comprimentos = mapeamento(["a", "bb", "ccc"], s => s.length); // number[]

// Interface genérica
interface RespostaAPI<T> {
  dados: T;
  meta: {
    total: number;
    pagina: number;
    porPagina: number;
  };
  erro?: string;
}

type RespostaUsuarios = RespostaAPI<Usuario[]>;
type RespostaProduto  = RespostaAPI<Produto>;

// Classe genérica
class Pilha<T> {
  private items: T[] = [];

  empilhar(item: T): void {
    this.items.push(item);
  }

  desempilhar(): T | undefined {
    return this.items.pop();
  }

  get tamanho(): number {
    return this.items.length;
  }
}

const pilhaNumeros = new Pilha<number>();
pilhaNumeros.empilhar(1);
pilhaNumeros.empilhar(2);
pilhaNumeros.desempilhar(); // number | undefined

Generics avançados — keyof, typeof, infer, conditional

Estes operadores permitem construir tipos derivados de outros tipos sem duplicação.

// keyof — extrai as chaves de um tipo como union
type ChavesUsuario = keyof Usuario; // "id" | "nome" | "email" | "avatarUrl" | "criadoEm"

// Função tipada com keyof
function obterPropriedade<T, K extends keyof T>(obj: T, chave: K): T[K] {
  return obj[chave];
}

const usuario: Usuario = { id: "1", nome: "Rafael", email: "r@r.dev", criadoEm: new Date() };
const nomeUsuario = obterPropriedade(usuario, "nome"); // string — TypeScript sabe o tipo
// obterPropriedade(usuario, "senha"); // Erro de compilação

// typeof — captura o tipo de um valor existente
const configuracao = {
  apiUrl: "https://api.rafael.dev",
  versao: "1.0.0",
  depuracao: false,
} as const;

type Configuracao = typeof configuracao;
// { readonly apiUrl: "https://api.rafael.dev"; readonly versao: "1.0.0"; readonly depuracao: false }

// infer — extrai um tipo de dentro de outro (usado em conditional types)
type TipoRetorno<T> = T extends (...args: unknown[]) => infer R ? R : never;
type TipoParametros<T> = T extends (...args: infer P) => unknown ? P : never;

type RetornoBuscarUsuario = TipoRetorno<typeof buscarUsuario>; // Usuario
type ParamsBuscarUsuario = TipoParametros<typeof buscarUsuario>; // [id: string]

// Desembrulhar Promise com infer
type DesembrulharPromise<T> = T extends Promise<infer U> ? U : T;
type ValorResolvidoPromise = DesembrulharPromise<Promise<Usuario>>; // Usuario

// Extrair tipo de elemento de array
type ElementoArray<T> = T extends (infer U)[] ? U : never;
type ElementoLista = ElementoArray<string[]>; // string

// Conditional types complexos
type EhFuncao<T> = T extends (...args: unknown[]) => unknown ? true : false;
type Resultado1 = EhFuncao<() => void>;  // true
type Resultado2 = EhFuncao<string>;      // false

// Constraints — T extends X restringe os tipos aceitos
function comprimento<T extends { length: number }>(valor: T): number {
  return valor.length;
}

comprimento("hello");      // OK — string tem .length
comprimento([1, 2, 3]);    // OK — array tem .length
comprimento({ length: 5 }); // OK — objeto com .length
// comprimento(42);         // Erro — number não tem .length

Utility Types completo

Utility types derivam novos tipos de existentes sem duplicar definições. Parte da biblioteca padrão do TypeScript.

interface Pedido {
  id: string;
  clienteId: string;
  total: number;
  status: "pendente" | "pago" | "cancelado";
  notas?: string;
  criadoEm: Date;
}

// Partial<T> — todos os campos tornam-se opcionais
type AtualizarPedidoDto = Partial<Pedido>;
// { id?: string; clienteId?: string; total?: number; ... }

// Required<T> — todos os campos tornam-se obrigatórios (remove ?)
type PedidoCompleto = Required<Pedido>;

// Readonly<T> — nenhum campo pode ser reatribuído
type PedidoImutavel = Readonly<Pedido>;
// pedidoImutavel.total = 100; // Erro de compilação

// Pick<T, K> — seleciona apenas os campos K
type ResumoPedido = Pick<Pedido, "id" | "total" | "status">;

// Omit<T, K> — remove os campos K
type CriarPedidoDto = Omit<Pedido, "id" | "criadoEm">;

// Record<K, V> — objeto com chaves K e valores V
type ContagemStatus = Record<Pedido["status"], number>;
// { pendente: number; pago: number; cancelado: number }

type CacheResposta = Record<string, { dados: unknown; expira: Date }>;

// Extract<T, U> — extrai da union T os tipos atribuíveis a U
type StatusAtivos = Extract<Pedido["status"], "pendente" | "pago">;
// "pendente" | "pago"

// Exclude<T, U> — remove da union T os tipos atribuíveis a U
type StatusFinais = Exclude<Pedido["status"], "pendente">;
// "pago" | "cancelado"

// NonNullable<T> — remove null e undefined da union
type IDSeguro = NonNullable<string | null | undefined>;
// string

// ReturnType<T> — extrai o tipo de retorno de uma função
async function buscarPedido(id: string): Promise<Pedido> { ... }
type PedidoBuscado = ReturnType<typeof buscarPedido>;       // Promise<Pedido>
type PedidoResolvido = Awaited<ReturnType<typeof buscarPedido>>; // Pedido

// Parameters<T> — extrai parâmetros como tupla
type ParamsBuscarPedido = Parameters<typeof buscarPedido>;  // [id: string]

// ConstructorParameters<T> — parâmetros do constructor
class Servico {
  constructor(public url: string, public timeout: number) {}
}
type ParamsConstrutor = ConstructorParameters<typeof Servico>; // [url: string, timeout: number]

// InstanceType<T> — tipo da instância criada pelo constructor
type InstanciaServico = InstanceType<typeof Servico>; // Servico

// Awaited<T> — desembrulha Promises recursivamente
type Valor = Awaited<Promise<Promise<string>>>; // string

// ThisType<T> — define o tipo de this dentro de um objeto
const metodos: ThisType<{ nome: string }> & {
  cumprimentar(): string;
} = {
  cumprimentar() { return `Olá, ${this.nome}`; }
};

// Template Literal Utility Types
type Maiusculas = Uppercase<"hello">;     // "HELLO"
type Minusculas = Lowercase<"HELLO">;     // "hello"
type Capitalizado = Capitalize<"hello">;  // "Hello"
type Descapitalizado = Uncapitalize<"Hello">; // "hello"

Mapped Types

Mapped types iteram sobre as chaves de um tipo e criam novo tipo transformando cada chave ou valor.

// Forma básica: [K in keyof T]
type Opcional<T> = {
  [K in keyof T]?: T[K];
};

type SomenteLeitura<T> = {
  readonly [K in keyof T]: T[K];
};

// Modificadores: + e - adicionam ou removem readonly e ?
type Mutavel<T> = {
  -readonly [K in keyof T]: T[K];  // remove readonly de todas as chaves
};

type Obrigatorio<T> = {
  [K in keyof T]-?: T[K];          // remove ? de todas as chaves
};

// Remapeamento de chaves com as (TypeScript 4.1+)
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

type GettersUsuario = Getters<{ nome: string; email: string }>;
// { getNome: () => string; getEmail: () => string }

// Filtrar chaves por tipo
type ChavesString<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};

type ChavesStringUsuario = ChavesString<Usuario>;
// { id: string; nome: string; email: string; avatarUrl?: string }

// Eventos tipados a partir de um mapa
interface EventMap {
  click: MouseEvent;
  focus: FocusEvent;
  keydown: KeyboardEvent;
}

type EventListeners = {
  [K in keyof EventMap]: (event: EventMap[K]) => void;
};

// Validador gerado automaticamente
type Validadores<T> = {
  [K in keyof T]: (valor: T[K]) => boolean | string;
};

type ValidadoresUsuario = Validadores<Pick<Usuario, "nome" | "email">>;
// { nome: (valor: string) => boolean | string; email: (valor: string) => boolean | string }

Conditional Types e infer avançado

Conditional types criam tipos que dependem de uma condição. Combinados com infer, permitem extrair partes de tipos complexos.

// Sintaxe básica: T extends U ? TipoSe : TipoSenão
type EhString<T> = T extends string ? "sim" : "não";
type R1 = EhString<string>;  // "sim"
type R2 = EhString<number>;  // "não"

// Distributividade — aplicado a cada membro do union individualmente
type TiposArray<T> = T extends unknown ? T[] : never;
type Arrays = TiposArray<string | number>; // string[] | number[]

// Evitar distribuição com tupla
type TipoArrayUnico<T> = [T] extends [unknown] ? T[] : never;
type ArrayUnion = TipoArrayUnico<string | number>; // (string | number)[]

// Extraindo tipos com infer
type Desembrulhar<T> = T extends Array<infer Item> ? Item : T;
type Str = Desembrulhar<string[]>;    // string
type Num = Desembrulhar<number>;      // number (não é array)

// Extrair tipo de Promise
type Resolver<T> = T extends Promise<infer U> ? Resolver<U> : T; // recursivo
type Profundo = Resolver<Promise<Promise<string>>>; // string

// Extrair parâmetros de função específica
type SegundoParametro<T> =
  T extends (arg1: unknown, arg2: infer P, ...args: unknown[]) => unknown
    ? P
    : never;

type Segundo = SegundoParametro<(a: string, b: number, c: boolean) => void>; // number

// infer em tipos de objeto
type TipoPropriedade<T, K extends keyof T> =
  T extends { [key in K]: infer V } ? V : never;

// Padrão de permissão — only allows keys where value matches type
type SomenteMetodos<T> = {
  [K in keyof T as T[K] extends Function ? K : never]: T[K];
};

Narrowing completo

Narrowing usa verificações de runtime para afunilar um tipo genérico para um específico. O TypeScript analisa o fluxo de controle para inferir o tipo mais específico possível em cada branch.

// typeof narrowing
function formatar(valor: string | number): string {
  if (typeof valor === "string") {
    return valor.toUpperCase();
  }
  return valor.toFixed(2); // TypeScript sabe que é number aqui
}

// instanceof narrowing
function tratarErro(err: unknown): string {
  if (err instanceof Error) {
    return err.message;          // err é Error aqui
  }
  if (err instanceof Response) {
    return `HTTP ${err.status}`; // err é Response aqui
  }
  return String(err);
}

// in operator — verifica se propriedade existe
type Circulo = { tipo: "circulo"; raio: number };
type Retangulo = { tipo: "retangulo"; largura: number; altura: number };
type Forma = Circulo | Retangulo;

function calcularArea(forma: Forma): number {
  if ("raio" in forma) {
    return Math.PI * forma.raio ** 2;
  }
  return forma.largura * forma.altura;
}

// Discriminated union — propriedade literal única
type EventoApp =
  | { tipo: "USUARIO_LOGOU";   payload: { usuarioId: string } }
  | { tipo: "PEDIDO_CRIADO";   payload: { pedidoId: string; total: number } }
  | { tipo: "PRODUTO_ATUALIZADO"; payload: { produtoId: string } };

function processarEvento(evento: EventoApp): void {
  switch (evento.tipo) {
    case "USUARIO_LOGOU":
      // evento.payload.usuarioId disponível
      break;
    case "PEDIDO_CRIADO":
      // evento.payload.pedidoId e total disponíveis
      break;
    case "PRODUTO_ATUALIZADO":
      // evento.payload.produtoId disponível
      break;
    default:
      // never — TypeScript garante que todos os casos foram cobertos
      const _exhaustivo: never = evento;
  }
}

// Type predicate — função que ensina o TypeScript a fazer narrowing
function ehPedido(valor: unknown): valor is Pedido {
  return (
    typeof valor === "object" &&
    valor !== null &&
    "id" in valor &&
    "total" in valor &&
    typeof (valor as Pedido).total === "number"
  );
}

const dados: unknown = receberDaAPI();
if (ehPedido(dados)) {
  console.log(dados.total); // TypeScript sabe que é Pedido
}

// Assertion functions — lançam exceção se a condição for falsa
function garantir<T>(valor: T | null | undefined, msg: string): asserts valor is T {
  if (valor === null || valor === undefined) {
    throw new Error(msg);
  }
}

const usuario2 = await buscarUsuario("123"); // Usuario | null
garantir(usuario2, "Usuário não encontrado");
console.log(usuario2.nome); // TypeScript sabe que não é null após garantir()

// Inferência automática de type predicates (TypeScript 5.5+)
// Antes do 5.5, era necessário escrever `: valor is T` manualmente
const numeros = [1, 2, null, 3, null].filter(Boolean); // number[] (inferido automaticamente)

Classes

TypeScript adiciona modificadores de acesso, propriedades de readonly, classes abstratas e decorators às classes JavaScript.

// Modificadores de acesso
class ContaBancaria {
  public   titular: string;       // acessível de qualquer lugar (padrão)
  protected saldo: number;        // acessível na classe e subclasses
  private  historico: string[];   // acessível apenas na própria classe
  readonly numeroConta: string;   // não pode ser reatribuído após inicialização

  // Shorthand de constructor — cria e atribui as propriedades automaticamente
  constructor(
    public readonly banco: string,
    protected agencia: string,
    titular: string,
    saldoInicial: number = 0
  ) {
    this.titular = titular;
    this.saldo = saldoInicial;
    this.historico = [];
    this.numeroConta = crypto.randomUUID();
  }

  depositar(valor: number): void {
    this.saldo += valor;
    this.historico.push(`+${valor}`);
  }

  get saldoAtual(): number {
    return this.saldo;
  }
}

// Classe abstrata — define contrato, não pode ser instanciada
abstract class Repositorio<T extends { id: string }> {
  abstract buscarPorId(id: string): Promise<T | null>;
  abstract listar(): Promise<T[]>;
  abstract salvar(entidade: T): Promise<T>;
  abstract deletar(id: string): Promise<void>;

  // Método concreto disponível para subclasses
  async buscarOuFalhar(id: string): Promise<T> {
    const entidade = await this.buscarPorId(id);
    if (!entidade) throw new Error(`Entidade ${id} não encontrada`);
    return entidade;
  }
}

// Implementação concreta
class RepositorioPedidos extends Repositorio<Pedido> {
  private pedidos: Map<string, Pedido> = new Map();

  async buscarPorId(id: string): Promise<Pedido | null> {
    return this.pedidos.get(id) ?? null;
  }

  // ... implementações dos outros métodos
}

// Implements — valida que a classe segue um contrato (interface)
interface ServicoEmail {
  enviar(para: string, assunto: string, corpo: string): Promise<void>;
}

class ServicoEmailSendGrid implements ServicoEmail {
  async enviar(para: string, assunto: string, corpo: string): Promise<void> {
    // implementação...
  }
}

// override — documenta e valida que está sobrescrevendo método da superclasse
class ContaPoupanca extends ContaBancaria {
  private rendimento: number;

  constructor(banco: string, agencia: string, titular: string, taxa: number) {
    super(banco, agencia, titular, 0);
    this.rendimento = taxa;
  }

  override depositar(valor: number): void {
    super.depositar(valor);
    console.log(`Rendimento de ${this.rendimento}% aplicado`);
  }
}

Decorators (TypeScript 5.0 — Stage 3)

Decorators são funções que envolvem classes, métodos, acessores, campos e parâmetros para adicionar comportamento. No TypeScript 5.0+, seguem a especificação Stage 3 — diferente e incompatível com experimentalDecorators.

// Decorator de classe — recebe o constructor e contexto
function Singleton<T extends new (...args: unknown[]) => unknown>(
  Base: T,
  ctx: ClassDecoratorContext
) {
  let instancia: InstanceType<T>;
  return class extends Base {
    constructor(...args: unknown[]) {
      super(...args);
      if (!instancia) instancia = this as InstanceType<T>;
      return instancia;
    }
  } as T;
}

// Decorator de método — recebe a função e contexto
function Memoizar<T extends (...args: unknown[]) => unknown>(
  metodo: T,
  ctx: ClassMethodDecoratorContext
) {
  const cache = new Map<string, ReturnType<T>>();
  return function (this: unknown, ...args: Parameters<T>): ReturnType<T> {
    const chave = JSON.stringify(args);
    if (cache.has(chave)) return cache.get(chave)!;
    const resultado = metodo.apply(this, args) as ReturnType<T>;
    cache.set(chave, resultado);
    return resultado;
  };
}

// Decorator de acessor
function Validar<T>(
  original: ClassAccessorDecoratorTarget<unknown, T>,
  ctx: ClassAccessorDecoratorContext
) {
  return {
    set(this: unknown, valor: T) {
      if (typeof valor === "string" && valor.trim() === "") {
        throw new Error(`${String(ctx.name)} não pode ser vazio`);
      }
      original.set.call(this, valor);
    },
  };
}

// Decorator de campo
function PadraoZero(
  _: undefined,
  ctx: ClassFieldDecoratorContext
) {
  return function (this: unknown) {
    return 0;
  };
}

// Uso
@Singleton
class ConfiguracaoApp {
  @Validar
  accessor nomeAplicacao = "Meu App";

  @PadraoZero
  contadorRequisicoes!: number;

  @Memoizar
  calcularHash(input: string): string {
    return btoa(input); // exemplo simplificado
  }
}

Module System e Declaration Files

TypeScript oferece controle fino sobre como módulos são importados e exportados. import type garante que importações de tipos sejam removidas em tempo de compilação.

// import type — importa apenas tipos (zero runtime overhead)
import type { Usuario, Pedido } from "./tipos";
import type { FC, ReactNode } from "react";

// verbatimModuleSyntax (tsconfig) — obriga uso de import type para tipos
// Com esta flag, o compilador errou se você importar tipo sem "type"

// Re-exportar tipos
export type { Usuario } from "./usuario";
export { processarPedido } from "./pedido";

// Importação com alias
import { buscarUsuario as fetchUser } from "./api/usuario";

// Namespace import
import * as ApiUsuario from "./api/usuario";

// Dynamic import — carrega módulo sob demanda
const { parse } = await import("./lib/parse-csv");
const modulo = await import(`./templates/${nome}-template`);

// esModuleInterop — permite importar módulos CommonJS como default
// Com flag habilitada no tsconfig:
import express from "express";  // em vez de: import * as express from "express"

Declaration files (.d.ts) — tipando bibliotecas sem tipos:

// biblioteca-sem-tipos.d.ts
// Declaração de módulo externo
declare module "biblioteca-legada" {
  export function processar(input: string): string;
  export interface Opcoes {
    modo: "rapido" | "lento";
    limite?: number;
  }
}

// Augmentation — estender tipos de módulos externos
declare module "express" {
  interface Request {
    usuarioLogado?: { id: string; email: string };
  }
}

// Declarar variáveis globais (injetadas por bundler/HTML)
declare const __DEV__: boolean;
declare const __APP_VERSION__: string;

// Triple-slash directive — referência explícita a outro arquivo de tipos
/// <reference types="vite/client" />
/// <reference path="./tipos-extras.d.ts" />

tsconfig.json completo

O tsconfig.json controla o compilador TypeScript. As opções de strict são as mais importantes — habilitam um conjunto de verificações que previnem classes inteiras de bugs.

{
  "compilerOptions": {
    // ── Target e módulos ──
    "target": "ES2022",
    // Versão JS de saída: ES5, ES6, ES2017, ES2022, ESNext
    // Afeta quais polyfills são gerados

    "module": "ESNext",
    // Formato de módulos: CommonJS, ESNext, Node16, NodeNext

    "moduleResolution": "bundler",
    // Como resolver imports: "bundler" para Vite/esbuild, "node16" para Node.js puro

    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    // APIs disponíveis para o compilador verificar

    // ── Strict mode — ative todos ──
    "strict": true,
    // Ativa: noImplicitAny, strictNullChecks, strictFunctionTypes,
    //        strictBindCallApply, strictPropertyInitialization, noImplicitThis,
    //        alwaysStrict, useUnknownInCatchVariables

    "noUncheckedIndexedAccess": true,
    // arr[0] é T | undefined, não T — previne erros de acesso fora do bounds

    "exactOptionalPropertyTypes": true,
    // Distingue { x?: string } (ausente) de { x: string | undefined } (presente mas undefined)

    "noImplicitOverride": true,
    // Obriga usar "override" ao sobrescrever métodos da superclasse

    "noPropertyAccessFromIndexSignature": true,
    // Obriga notação de colchetes para acesso em índice: obj["chave"] vs obj.chave

    // ── Paths e aliases ──
    "baseUrl": ".",
    "paths": {
      "@/*":          ["./src/*"],
      "@componentes/*": ["./src/componentes/*"],
      "@utils/*":     ["./src/utils/*"],
      "@tipos/*":     ["./src/tipos/*"]
    },

    // ── Saída ──
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,          // gera arquivos .d.ts
    "declarationMap": true,       // mapeia .d.ts para o .ts original
    "sourceMap": true,            // gera sourcemaps para debug
    "removeComments": false,

    // ── Compatibilidade e interop ──
    "esModuleInterop": true,      // default imports para módulos CJS
    "allowSyntheticDefaultImports": true,
    "verbatimModuleSyntax": true, // obriga import type para tipos
    "isolatedModules": true,      // compatível com esbuild/swc/Vite

    // ── References (projeto monorepo) ──
    "composite": true,            // habilita project references
    "incremental": true,          // cache de compilação

    // ── Experimental ──
    "experimentalDecorators": false, // NÃO usar com Stage 3 decorators
    "emitDecoratorMetadata": false   // somente para decorators legados
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist", "**/*.test.ts"],
  "references": [
    { "path": "./pacotes/core" },
    { "path": "./pacotes/ui" }
  ]
}

Padrões avançados

Builder Pattern tipado

O builder pattern com TypeScript permite construir objetos complexos com validação de tipo em tempo de compilação — garantindo que todos os campos obrigatórios sejam fornecidos antes de chamar build().

// Opaque types / Branding — tipos nominais em TypeScript estrutural
type Marcado<T, Marca extends string> = T & { readonly _marca: Marca };

type UsuarioID  = Marcado<string, "UsuarioID">;
type PedidoID   = Marcado<string, "PedidoID">;
type EmailValido = Marcado<string, "EmailValido">;

// Criadores — única forma de criar os tipos marcados
const criarUsuarioID = (id: string): UsuarioID => id as UsuarioID;
const criarEmailValido = (email: string): EmailValido => {
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
    throw new Error("E-mail inválido");
  }
  return email as EmailValido;
};

// Agora não é possível passar um PedidoID onde UsuarioID é esperado
function buscarPedidosDoUsuario(id: UsuarioID): Promise<Pedido[]> { ... }
// buscarPedidosDoUsuario("abc");            // Erro — string não é UsuarioID
// buscarPedidosDoUsuario(criarPedidoID("x")); // Erro — PedidoID não é UsuarioID
buscarPedidosDoUsuario(criarUsuarioID("1")); // OK

// Builder pattern tipado — campos obrigatórios como generics
class ConstrutorRequisicao<
  TModo extends "GET" | "POST" | "PUT" | "DELETE" | undefined = undefined,
  TURL extends string | undefined = undefined
> {
  private config: {
    modo?: TModo;
    url?: TURL;
    cabecalhos: Record<string, string>;
    corpo?: unknown;
  } = { cabecalhos: {} };

  setModo<M extends "GET" | "POST" | "PUT" | "DELETE">(
    modo: M
  ): ConstrutorRequisicao<M, TURL> {
    this.config.modo = modo as M;
    return this as unknown as ConstrutorRequisicao<M, TURL>;
  }

  setURL<U extends string>(url: U): ConstrutorRequisicao<TModo, U> {
    this.config.url = url as U;
    return this as unknown as ConstrutorRequisicao<TModo, U>;
  }

  adicionarCabecalho(chave: string, valor: string): this {
    this.config.cabecalhos[chave] = valor;
    return this;
  }

  // build() só disponível quando modo E url foram definidos
  build(
    this: ConstrutorRequisicao<
      "GET" | "POST" | "PUT" | "DELETE",
      string
    >
  ): Request {
    return new Request(this.config.url!, {
      method: this.config.modo,
      headers: this.config.cabecalhos,
    });
  }
}

// Uso:
const req = new ConstrutorRequisicao()
  .setModo("GET")
  .setURL("/api/usuarios")
  .adicionarCabecalho("Authorization", "Bearer abc")
  .build(); // OK — modo e URL estão definidos

// new ConstrutorRequisicao().build(); // Erro de compilação

Type-safe Event Emitter

// Emitter tipado — garante que eventos e handlers correspondam
type HandlerEvento<T> = (payload: T) => void;

class EmissorEventos<TMapaEventos extends Record<string, unknown>> {
  private handlers = new Map<
    keyof TMapaEventos,
    Set<HandlerEvento<unknown>>
  >();

  on<K extends keyof TMapaEventos>(
    evento: K,
    handler: HandlerEvento<TMapaEventos[K]>
  ): () => void {
    if (!this.handlers.has(evento)) {
      this.handlers.set(evento, new Set());
    }
    this.handlers.get(evento)!.add(handler as HandlerEvento<unknown>);

    // Retorna função para remover o listener
    return () => this.off(evento, handler);
  }

  off<K extends keyof TMapaEventos>(
    evento: K,
    handler: HandlerEvento<TMapaEventos[K]>
  ): void {
    this.handlers.get(evento)?.delete(handler as HandlerEvento<unknown>);
  }

  emit<K extends keyof TMapaEventos>(evento: K, payload: TMapaEventos[K]): void {
    this.handlers.get(evento)?.forEach(h => h(payload));
  }
}

// Definição do mapa de eventos
interface EventosSistema {
  "usuario:logado":    { usuario: Usuario; timestamp: Date };
  "pedido:criado":     { pedido: Pedido };
  "erro:capturado":   { erro: Error; contexto: string };
}

const emitter = new EmissorEventos<EventosSistema>();

// TypeScript valida evento e payload automaticamente
const removerListener = emitter.on("usuario:logado", ({ usuario, timestamp }) => {
  console.log(`${usuario.nome} logou às ${timestamp.toISOString()}`);
});

emitter.emit("usuario:logado", {
  usuario: { id: "1", nome: "Rafael", email: "r@r.dev", criadoEm: new Date() },
  timestamp: new Date(),
});

// emitter.emit("evento:inexistente", {}); // Erro de compilação
// emitter.on("pedido:criado", ({ pedidoId }) => {}); // Erro — pedidoId não existe

Versões — TypeScript 4.0 ao 5.5

VersãoAnoPrincipais novidades
4.02020Variadic tuple types ([...T, ...U]); labeled tuple elements; unknown em catch blocks por padrão (com flag); editor performance
4.12020Template literal types; remapeamento de chaves em mapped types (as); recursive conditional types
4.22021Elementos rest no meio de tuplas; abstract em construct signatures; melhores mensagens de erro
4.42021Análise de controle de fluxo para condições; useUnknownInCatchVariables como parte de strict
4.52021Awaited<T> utility type; type modifier em named imports (import { type Foo }); tail-recursion elimination para conditional types
4.72022Suporte nativo a ESM no Node.js ("module": "node16"); instantiation expressions; infer extends
4.92022Operador satisfies — valida shape sem alterar o tipo inferido; in narrowing melhorado para tipos não-primitivos
5.02023Decorators Stage 3 — reescritos e incompatíveis com experimentalDecorators; const type parameters (<const T>); --moduleResolution bundler; suporte a múltiplos arquivos de configuração em extends
5.12023Tipos de retorno relacionados em getters/setters (get x(): string | undefined / set x(v: string)); JSDoc @param inline; velocidade de checagem melhorada
5.22023using / await using (Explicit Resource Management — Stage 3); decorator metadata; array destructuring narrowing
5.32023import attributes (import x from "y" with { type: "json" }); narrowing em comparações de tipo por fluxo de controle
5.42024NoInfer<T> utility type (impede inferência em posição específica); melhorias em Object.groupBy e Map.groupBy; import.meta.dirname/filename para ESM Node
5.52024Type predicates inferidos automaticamente para arrow functions de filtragem; --isolatedDeclarations para builds paralelas de .d.ts; ${configDir} em paths do tsconfig