Control flow

Os padrões de controle de fluxo do JavaScript continuam iguais aqui: sair cedo, evitar aninhamento, deixar o caminho feliz no nível de menos recuo. O TypeScript acrescenta um efeito que o JavaScript não tem. Cada checagem que você escreve para controlar o fluxo também informa o compilador, que passa a saber mais sobre o tipo da variável depois dela. Isso é o narrowing (estreitamento de tipo), e ele aparece de três formas nesta página: no guard que elimina o nulo, no switch sobre uma discriminated union (união discriminada) e no exhaustiveness check (verificação de exaustividade), que faz o compilador acusar o caso esquecido.

Conceitos fundamentais

ConceitoO que é
narrowing (estreitamento)Refinamento de um tipo amplo para um específico após guard ou comparação
type guard (guarda de tipo)Expressão que estreita o tipo (typeof, instanceof, predicate)
discriminated union (união discriminada)Union cujo membro é identificado por um campo literal (kind, type)
exhaustiveness check (verificação de exaustividade)Garantia em compilação de que todos os casos da union foram tratados
never (tipo impossível)Tipo sem valores; usado em ramos inalcançáveis e checagem de exaustividade
assertion function (função de afirmação)Função que lança se a condição falha e estreita o tipo no caller (asserts x is T)
early return (retorno antecipado)Sair da função assim que o resultado é conhecido; reduz aninhamento

A cláusula de proteção elimina o nulo do resto da função

Em TypeScript, a guard clause (cláusula de proteção, o if que sai da função assim que o caso inválido aparece) faz dois trabalhos ao mesmo tempo. Ela interrompe o fluxo, como em qualquer linguagem, e ensina o tipo ao compilador. Depois de if (!order) return, o tipo de order deixa de ser Order | null e passa a ser Order até o fim da função. O ?. some de todas as linhas seguintes, porque não existe mais nulo para tratar.

❌ Ruim: o valor pode ser nulo, e o ?. se espalha por todas as linhas
async function processOrder(orderId: string): Promise<void> {
  const order = await findOrder(orderId); // Order | null

  await sendReceipt(order?.customerId);   // customerId pode ser undefined aqui
  await updateStatus(order?.id, "done");  // id pode ser undefined
}
✅ Bom: guard estreita o tipo, resto do código é não-nulo
async function processOrder(orderId: string): Promise<void> {
  const order = await findOrder(orderId); // Order | null
  if (!order) return;
  // order: Order. Compilador garante não-nulo daqui para baixo

  await sendReceipt(order.customerId);
  await updateStatus(order.id, "done");
}

O switch sobre o campo discriminante estreita o tipo sozinho

Uma união discriminada é um union em que cada membro carrega um campo literal que o identifica, como o type de PaymentEvent. Ao ver switch (event.type), o compilador liga cada case ao membro correspondente: dentro de case "payment_success", event.amount existe e é um number; dentro de case "payment_failed", quem existe é event.reason. Escrever as para chegar ao campo é trabalho que o compilador já fez, e o as ainda desliga a checagem que protegia aquela linha.

❌ Ruim: if/else com o tipo forçado à mão em cada ramo
type PaymentEvent =
  | { type: "payment_success"; orderId: string; amount: number }
  | { type: "payment_failed"; orderId: string; reason: string }
  | { type: "payment_refunded"; orderId: string };

function handlePaymentEvent(event: PaymentEvent): void {
  if (event.type === "payment_success") {
    const e = event as { type: "payment_success"; orderId: string; amount: number };
    sendReceipt(e.orderId, e.amount);
  } else if (event.type === "payment_failed") {
    const e = event as { type: "payment_failed"; orderId: string; reason: string };
    notifyFailure(e.orderId, e.reason);
  }
}
✅ Bom: dentro de cada case, o compilador já sabe qual é o tipo
function handlePaymentEvent(event: PaymentEvent): void {
  switch (event.type) {
    case "payment_success":
      sendReceipt(event.orderId, event.amount); // amount disponível aqui
      break;

    case "payment_failed":
      notifyFailure(event.orderId, event.reason); // reason disponível aqui
      break;

    case "payment_refunded":
      issueRefund(event.orderId);
      break;
  }
}

O never no default faz o compilador acusar o caso que faltou

Um default: return "Unknown" aceita qualquer status que ninguém tratou, e o dia em que "cancelled" entra no tipo, a tela passa a mostrar "Unknown" sem que nada tenha quebrado no build. O erro só aparece quando alguém abre a página.

assertNever inverte isso. O parâmetro é do tipo never, que não aceita valor nenhum. Enquanto todos os case estiverem cobertos, o que sobra no default é never, e a chamada compila. Assim que um membro novo entra na união, sobra esse membro no default, ele não cabe em never, e o compilador aponta a linha. O caso esquecido vira erro de build.

❌ Ruim: o caso novo entra no tipo e o switch continua compilando
type OrderStatus = "pending" | "approved" | "shipped" | "cancelled";

function getStatusLabel(status: OrderStatus): string {
  switch (status) {
    case "pending":   return "Aguardando";
    case "approved":  return "Aprovado";
    case "shipped":   return "Enviado";
    // "cancelled" adicionado ao tipo mas esquecido aqui: retorna undefined
    default:          return "Unknown";
  }
}
✅ Bom: never no default, compilador avisa se faltar um caso
function assertNever(value: never): never {
  throw new Error(`Unhandled case: ${JSON.stringify(value)}`);
}

function getStatusLabel(status: OrderStatus): string {
  switch (status) {
    case "pending":    return "Aguardando";
    case "approved":   return "Aprovado";
    case "shipped":    return "Enviado";
    case "cancelled":  return "Cancelado";
    default:           return assertNever(status);
    // adicionar "returned" ao tipo → erro de compilação aqui
  }
}

A função predicado dá nome à checagem e a torna reaproveitável

A checagem escrita dentro do if estreita o tipo até certo ponto e para: o compilador conclui que data é um objeto com aquelas chaves, e ainda assim exige o as Order para chegar em customerId. Além disso, a mesma sequência de condições precisa ser repetida em cada lugar que recebe o valor.

Uma função com retorno value is Order resolve as duas coisas. Ela dá um nome à checagem (isOrder), e o compilador propaga a conclusão para quem chamou: depois de if (!isOrder(data)) throw, data é um Order, e o as desaparece.

❌ Ruim: a checagem se repete e ainda exige o as no final
function processApiResponse(data: unknown): void {
  if (
    typeof data === "object" &&
    data !== null &&
    "id" in data &&
    "customerId" in data
  ) {
    // data: object. Ainda não é Order para o compilador
    sendConfirmation((data as Order).customerId);
  }
}
✅ Bom: a função predicado nomeia a checagem, e o compilador confia nela
function isOrder(value: unknown): value is Order {
  return (
    typeof value === "object" &&
    value !== null &&
    "id" in value &&
    "customerId" in value &&
    "total" in value
  );
}

function processApiResponse(data: unknown): void {
  if (!isOrder(data)) throw new Error("Invalid order payload");

  sendConfirmation(data.customerId); // data: Order. Compilador sabe
}

DoDocs v3.7.0 · Desenvolvido por @thiagocajadev · Baseado no trabalho de pmndrs/docs · Poimandres.