unknown, any e never no TypeScript: qual usar

1. Visão geral

TypeScript tem três tipos especiais que causam confusão: any, unknown e never. Todos os três aparecem nas “bordas” do sistema de tipos. any desativa a verificação de tipos, unknown força você a verificar antes de usar, enquanto never representa um caso que não pode ocorrer.

Após ler este artigo, você saberá qual tipo escolher para dados cuja origem é incerta. Você também saberá como usar never para fazer o compilador alertá-lo quando esquecer de lidar com uma ramificação.

Todos os exemplos executam com TypeScript 5.x e "strict": true em tsconfig.json.

2. any: desativa o type checker

any permite que um valor faça qualquer coisa. O compilador não verifica nada:

let payload: any = "olá";

payload.toUpperCase();   // OK
payload.foo.bar();       // OK em compile, erro em runtime
payload = 42;
const total: number = payload * 2; // OK

A terceira linha compila normalmente. Em tempo de execução, ela lança um erro:

TypeError: Cannot read properties of undefined (reading 'bar')

Com any, TypeScript assume que você assumiu a responsabilidade. Você perde o autocomplete, perde a verificação de nomes de propriedade, e perde os benefícios do refatoramento seguro.

2.1. any se propaga

O maior problema de any é que se propaga para tudo que toca:

function loadConfig(): any {
  return JSON.parse('{"port": "8080"}');
}

const config = loadConfig();
const port = config.port;      // port: any
const next = port + 1;         // next: any
console.log(next);
80801

port é a string "8080", então port + 1 retorna "80801" em vez de 8081. O compilador não avisa, porque any se propagou através de config, port e next. Um único any no início pode desativar a verificação de tipos em toda uma cadeia de chamadas de função.

Observação: JSON.parse, response.json() na fetch API e muitas bibliotecas legadas retornam any. Esta é a fonte mais comum de vazamento de any em código real.

3. unknown: seguro quando o tipo é desconhecido

unknown também aceita qualquer valor como any. A diferença é que você não pode usar esse valor até que tenha reduzido seu tipo (narrowing):

let payload: unknown = "olá";

payload.toUpperCase();
error TS18046: 'payload' is of type 'unknown'.

O compilador bloqueia imediatamente. Você precisa provar que payload é uma string antes:

if (typeof payload === "string") {
  console.log(payload.toUpperCase()); // payload: string
}
OLÁ

Dentro do bloco if, TypeScript sabe que payload é string, então toUpperCase() é válido.

3.1. Formas de reduzir unknown

Existem quatro ferramentas principais:

class PaymentError extends Error {
  constructor(message: string, public readonly code: string) {
    super(message);
  }
}

function describe(value: unknown): string {
  if (typeof value === "number") {
    return `Número: ${value.toFixed(2)}`;
  }
  if (value instanceof PaymentError) {
    return `Erro de pagamento ${value.code}`;
  }
  if (typeof value === "object" && value !== null && "id" in value) {
    return `Objeto com id: ${String(value.id)}`;
  }
  return "Desconhecido";
}

console.log(describe(12.5));
console.log(describe(new PaymentError("Sem saldo", "INSUFFICIENT")));
console.log(describe({ id: 7 }));
console.log(describe(null));
Número: 12.50
Erro de pagamento INSUFFICIENT
Objeto com id: 7
Desconhecido
  • typeof é usado para tipos primitivos: string, number, boolean…
  • instanceof é usado para classes.
  • in verifica se uma propriedade existe. A partir do TypeScript 4.9, após "id" in value então value tem tipo object & Record<"id", unknown>.
  • Você precisa verificar value !== null porque typeof null === "object".

3.2. Type guard para dados de fora

Para objetos complexos, você deve escrever um type guard, ou seja, uma função que retorna value is T:

interface Order {
  id: string;
  customerName: string;
  total: number;
}

export function isOrder(value: unknown): value is Order {
  if (typeof value !== "object" || value === null) return false;
  const v = value as Record<string, unknown>;
  return (
    typeof v.id === "string" &&
    typeof v.customerName === "string" &&
    typeof v.total === "number"
  );
}

export function parseOrder(json: string): Order {
  const data: unknown = JSON.parse(json);
  if (!isOrder(data)) {
    throw new Error("Dados de pedido inválidos");
  }
  return data; // data: Order
}

console.log(parseOrder('{"id":"A1","customerName":"Lan","total":250000}'));
{ id: 'A1', customerName: 'Lan', total: 250000 }

O ponto crucial é a linha const data: unknown = JSON.parse(json). Atribuir explicitamente o tipo unknown bloqueia any de JSON.parse vazar para fora. A partir daqui, o compilador força você a verificar antes de usar data.

Em projetos grandes, você pode substituir o type guard escrito à mão por uma biblioteca de validação de schema como Zod. O princípio permanece o mesmo: os dados que entram são unknown, e depois da validação se tornam um tipo específico.

3.3. unknown em catch

Com strict: true (especificamente a flag useUnknownInCatchVariables), a variável em catch tem tipo unknown:

try {
  parseOrder("{}");
} catch (err) {
  // err: unknown
  const message = err instanceof Error ? err.message : String(err);
  console.log(message);
}
Dados de pedido inválidos

Isso está correto com a realidade. JavaScript permite throw qualquer coisa, até strings ou números, então você não pode assumir que err é sempre um Error.

4. never: o tipo sem valores

never é um tipo vazio. Nenhum valor pertence ao tipo never. Ele aparece em três situações principais.

4.1. Função que nunca retorna

Uma função que sempre lança um erro, ou executa um loop infinito, tem tipo de retorno never:

function fail(message: string): never {
  throw new Error(message);
}

function getDiscount(code: string): number {
  if (code === "TET2026") return 0.2;
  return fail(`Código de desconto não existe: ${code}`);
}

Como never é atribuível a qualquer tipo, return fail(...) é válido em uma função que retorna number. O compilador também entende que o código após a chamada fail() é inalcançável.

4.2. Verificação exhaustiva

Esta é a forma mais útil de usar never. Suponha que você tenha uma union de métodos de pagamento:

type PaymentMethod =
  | { kind: "card"; last4: string }
  | { kind: "cash" }
  | { kind: "momo"; phone: string };

export function assertNever(value: never): never {
  throw new Error(`Caso não tratado: ${JSON.stringify(value)}`);
}

export function paymentLabel(method: PaymentMethod): string {
  switch (method.kind) {
    case "card":
      return `Cartão **** ${method.last4}`;
    case "cash":
      return "Dinheiro";
    case "momo":
      return `MoMo ${method.phone}`;
    default:
      return assertNever(method);
  }
}

Após lidar com todos os três case, na ramificação default method é reduzido para never. A chamada assertNever(method) é portanto válida.

Agora, suponha que um colega adicione { kind: "zalopay"; phone: string } à union mas esqueça de atualizar o switch. O compilador avisa imediatamente:

error TS2345: Argument of type '{ kind: "zalopay"; phone: string; }' is not assignable to parameter of type 'never'.

O erro é detectado em tempo de compilação, em vez de deixar o usuário ver um rótulo undefined na tela de pagamento. Se dados estranhos ainda chegarem em tempo de execução, por exemplo de uma API antiga, assertNever lançará um erro claro.

4.3. never em conditional type

Em conditional type, never é usado para remover um membro de uma union, porque A | never é apenas A:

type OnlyStrings<T> = T extends string ? T : never;

type Mixed = "vnd" | "usd" | 404 | null;
type Currencies = OnlyStrings<Mixed>; // "vnd" | "usd"

TypeScript distribui o conditional type a cada membro da union. Os membros 404 e null viram never e desaparecem. Utility types integrados como Exclude, Extract e NonNullable funcionam com esse mecanismo.

5. Comparação rápida

A diferença essencial está na relação de atribuição (assignability):

Atribuir outro valor a este tipo Atribuir este tipo a outro tipo Usar diretamente (chamar método, acessar propriedade)
any Sim Sim (exceto never) Sim, sem verificação
unknown Sim Apenas a unknown, any Não, requer narrowing
never Não (exceto never) Sim, a qualquer tipo Sem valores para usar

Pode-se resumir assim:

  • unknown é o top type seguro. Tudo é unknown.
  • never é o bottom type. never é um subtipo de todos os tipos.
  • any é uma brecha. É tanto um top type quanto um bottom type, e por isso quebra a solidez do sistema de tipos.

6. Testando o comportamento

Você pode testar tanto o comportamento em runtime quanto os tipos em tempo de compilação com Vitest. Instalação:

npm install -D vitest typescript

Supondo que as funções da seção 3.2 e 4.2 estejam em order.ts e payment.ts:

// payment.test.ts
import { describe, it, expect, expectTypeOf } from "vitest";
import { isOrder, parseOrder } from "./order";
import { paymentLabel, assertNever } from "./payment";

describe("unknown e type guard", () => {
  it("aceita pedido válido", () => {
    const order = parseOrder('{"id":"A1","customerName":"Lan","total":1000}');
    expect(order.total).toBe(1000);
    expectTypeOf(order.total).toEqualTypeOf<number>();
  });

  it("rejeita dados de tipo incorreto", () => {
    expect(isOrder({ id: "A1", customerName: "Lan", total: "1000" })).toBe(false);
    expect(() => parseOrder("null")).toThrow("Dados de pedido inválidos");
  });
});

describe("never e verificação exhaustiva", () => {
  it("retorna rótulo correto para cada método", () => {
    expect(paymentLabel({ kind: "card", last4: "4242" })).toBe("Cartão **** 4242");
    expect(paymentLabel({ kind: "cash" })).toBe("Dinheiro");
  });

  it("lança erro ao encontrar dados fora da union em runtime", () => {
    const legacy = { kind: "paypal" } as unknown as never;
    expect(() => assertNever(legacy)).toThrow("Caso não tratado");
  });
});
npx vitest run
 ✓ payment.test.ts (4 tests)
 Test Files  1 passed (1)
      Tests  4 passed (4)

expectTypeOf verifica os tipos em tempo de compilação. Se alguma mudança fizer order.total virar any, o teste falhará ao executar type check (vitest --typecheck). O último teste simula dados “estranhos” vindo de um sistema antigo, para garantir que a rede de segurança em runtime ainda funciona.

7. Erros comuns e quando não usar

Usar any para silenciar erros de compilação. Este é o hábito mais ruim. Se você realmente não sabe o tipo, use unknown. Ative a rule @typescript-eslint/no-explicit-any para pegar esses casos.

Fazer type assertion de unknown com as em vez de narrowing. data as Order apenas tranquiliza o compilador sem verificar nada em runtime. É essencialmente tão perigoso quanto any.

const data: unknown = JSON.parse('{"id": 1}');
const order = data as Order; // OK em compile
order.customerName.trim();   // TypeError em runtime

Usar unknown para tudo. unknown é para as bordas do sistema: entrada do usuário, resposta da API, catch, dados desserializados. Dentro do domínio, use tipos específicos. Uma função calculateTotal(items: unknown) é um design ruim.

Confundir never com void. void significa que a função retorna mas não tem um valor útil. never significa que a função nunca retorna. Uma função log() normal deve ser void, não never.

Esquecer o default em switch. Sem assertNever, TypeScript só avisa se a função tem tipo de retorno explícito e tem noImplicitReturns ativado. Mesmo assim, você perde a rede de segurança em runtime.

Observação: any ainda tem usos legítimos. É quando você está migrando gradualmente uma grande codebase JavaScript, ou quando escreve declaração para uma biblioteca onde o tipo é realmente muito dinâmico. Coloque-o em um escopo limitado e deixe um comentário explicando por quê.

8. Conclusão

Esses três tipos têm três papéis claros:

  • Use unknown para dados com tipo desconhecido nas bordas do sistema, depois reduza usando typeof, instanceof, in ou type guard.
  • Use never para funções que sempre lançam erro, para verificação exhaustiva com assertNever, e para filtrar union em conditional type.
  • Evite any, exceto quando tiver uma razão específica e tenha limitado seu escopo.

A regra prática é muito simples. Quando for digitar any, tente unknown primeiro. Se o compilador avisar, esses são exatamente os lugares onde você precisa verificar seus dados.

typescripttype-safetyunknownneverany