Overloads honestos
Overloads deixam o TypeScript mais preciso sobre como o tipo de retorno muda conforme o argumento. Mas é fácil abusar deles onde um union type simples já resolveria.
Estudar esta lição interativa →Às vezes uma função aceita mais de um tipo de entrada, e o tipo do retorno muda dependendo de qual entrada foi usada. Um union type simples nos parâmetros já cobre boa parte desses casos, mas ele tem um limite: ele não consegue expressar "se entrar X, sai Y; se entrar W, sai Z" com precisão, só "entra X ou W, sai Y ou Z" (misturando as duas possibilidades para qualquer chamada). Para descrever esse contrato mais fino, existe o overload: várias assinaturas declaradas para a mesma função, seguidas de uma única implementação que decide o que fazer.
// Overloads: o TIPO DE RETORNO muda conforme o tipo do argumento.
function dobrar(valor: number): number;
function dobrar(valor: string): string;
function dobrar(valor: number | string): number | string {
return typeof valor === "number" ? valor * 2 : valor + valor;
}
const n = dobrar(4); // o TypeScript sabe: number
const s = dobrar("ab"); // o TypeScript sabe: string
console.log(n.toFixed(1), s.toUpperCase());
// Armadilha: só as assinaturas de overload são visíveis para quem chama.
// A implementação (number | string) NÃO conta. Por isso isto dá erro TS2769:
// function usar(v: number | string) { return dobrar(v); }Repare na estrutura: duas linhas de assinatura (sem corpo, só a promessa "se entrar number, sai number" e "se entrar string, sai string"), seguidas da implementação de verdade, que recebe a união dos dois tipos possíveis e decide o comportamento. Quem chama a função só enxerga as assinaturas de overload; a implementação combinada fica "escondida" por trás delas. Por isso dobrar(4) é um number e dobrar("ab") é um string, cada um com o seu tipo exato.
Essa regra tem um efeito colateral que surpreende: como a implementação não conta, passar um valor do tipo number | string para dobrar dá erro TS2769 ("No overload matches this call"), porque nenhuma assinatura, sozinha, aceita a união. Se você precisa aceitar a união, declare também uma assinatura para ela.
Pense nos overloads como as entradas diferentes de um mesmo prédio, cada uma com uma placa específica ("Entrada para clientes", "Entrada para entregas"), mas que levam ao mesmo saguão interno por trás. Quem chega de fora só vê a placa que usou; por dentro, existe um único funcionário cuidando de tudo.
Overloads valem a pena quando o retorno muda de tipo conforme o argumento, como no exemplo acima. Mas é comum ver gente usando overload onde um simples tipo: A | B no parâmetro já resolveria perfeitamente, sem exigir duas assinaturas a mais para manter. Antes de escrever um overload, vale perguntar: "o tipo de retorno realmente muda, ou eu só estou complicando uma função que aceita dois tipos de entrada com o mesmo tipo de saída?". Na dúvida, comece pelo union type simples; só migre para overload se a precisão do retorno realmente for necessária.
Há um segundo cuidado, sobre a ordem das assinaturas. O TypeScript escolhe a primeira assinatura que serve para a chamada, de cima para baixo. Por isso as mais específicas vão antes e a mais genérica fica por último; se a genérica viesse primeiro, ela "engoliria" todas as chamadas e as específicas nunca seriam usadas. No exemplo, uma chamada com o texto exato "idade" devolve number, e qualquer outro texto cai na assinatura genérica e devolve string.
// O TypeScript escolhe a PRIMEIRA assinatura que serve. Por isso as mais
// específicas vêm antes das mais genéricas.
function ler(chave: "idade"): number;
function ler(chave: string): string;
function ler(chave: string): number | string {
return chave === "idade" ? 30 : "texto de " + chave;
}
const idade = ler("idade"); // number: casou com a primeira assinatura
const nome = ler("nome"); // string: caiu na genérica
console.log(idade.toFixed(0), nome.toUpperCase());A primeira assinatura que serve vence: ler("idade") é number, ler("nome") é string.
Funções do próprio navegador usam esse padrão. O document.createElement("input") devolve um HTMLInputElement (o TypeScript conhece o nome da tag), mas com um nome qualquer ele devolve um HTMLElement genérico: é uma assinatura específica antes de uma genérica, exatamente como no exemplo. Escrever os seus próprios overloads é bem mais raro do que usar os que já existem.
buscar já tem os overloads declarados: recebendo um number devolve uma string; recebendo um number[] devolve uma string[]. Complete a implementação: se idOuIds for um array (use Array.isArray), devolva o array mapeado prefixando cada id com "item-"; senão, devolva a string única no mesmo formato.function buscar(id: number): string;
function buscar(ids: number[]): string[];
function buscar(idOuIds: number | number[]): string | string[] {
// se for array, mapeie cada id para `"item-" + id`
// se for número, devolva `"item-" + idOuIds`
}
console.log(buscar(1));
console.log(buscar([1, 2, 3]));
🧩 Este é um exercício com correção automática — abra no app para completá-lo e verificar.