Sobrecarga de funções no TypeScript significa escrever várias call signatures para uma função, seguidas de uma única implementação. Cada assinatura pode combinar tipos de parâmetro diferentes com um tipo de retorno diferente, e quem chama recebe o tipo preciso.
Sem os overloads, parse retornaria number | number[] em toda chamada, e one + 1 seria um erro até você mesmo fazer o narrowing do resultado.
Overload signatures e a implementação
Uma função sobrecarregada tem duas partes:
- Overload signatures: declarações sem corpo, uma por formato de chamada suportado. São as únicas assinaturas que quem chama pode usar.
- A implementation signature: a última declaração, com o corpo. Os parâmetros dela precisam aceitar tudo o que os overloads aceitam, e o tipo de retorno precisa cobrir o tipo de retorno de cada overload. Ela é invisível de fora.
Os tipos existem só em tempo de compilação, então em tempo de execução há uma única função JavaScript. A implementação precisa inspecionar os argumentos (typeof, Array.isArray, arguments.length...) para decidir o que fazer. O compilador verifica que os overloads e a implementação concordam:
function format(value: string): string;
function format(value: number): number {
return value;
}
// error TS2394: This overload signature is not compatible with its implementation signature.
A solução é ampliar a implementação: function format(value: string | number): string | number.
A implementation signature não pode ser chamada
Essa é a regra que mais surpreende. Uma chamada precisa corresponder sozinha a uma das overload signatures; o TypeScript não as combina.
O compilador mostra:
index.ts(12,19): error TS2769: No overload matches this call.
The last overload gave the following error.
Argument of type 'string | string[]' is not assignable to parameter of type 'string[]'.
Type 'string' is not assignable to type 'string[]'.
A implementação aceita string | string[], mas quem chama não a enxerga. Adicione um terceiro overload que recebe a union e retorna a union, e a chamada compila e imprime [ 1, 2 ]:
function parse(input: string): number;
function parse(input: string[]): number[];
function parse(input: string | string[]): number | number[];
function parse(input: string | string[]): number | number[] {
return Array.isArray(input) ? input.map(Number) : Number(input);
}
Quantidades diferentes de parâmetros
Overloads também descrevem chamadas com aridades diferentes. Aqui uma data pode ser criada a partir de um timestamp ou de ano, mês e dia, mas não a partir de dois números:
Uma única assinatura com dois parâmetros opcionais aceitaria makeDate(2024, 3) e criaria em silêncio a data errada. Os overloads transformam isso em um erro de compilação (TS2575).
A ordem importa
O TypeScript testa os overloads de cima para baixo e escolhe o primeiro que corresponde. Coloque as assinaturas mais específicas primeiro. Um overload amplo no começo da lista engole as chamadas destinadas aos que vêm depois:
function describe(value: unknown): string; // matches everything
function describe(value: string): "text"; // never chosen
function describe(value: unknown): string {
return typeof value === "string" ? "text" : "other";
}
const d = describe("hi"); // d: string, not "text"
Troque as duas primeiras assinaturas de lugar e describe("hi") passa a ter o tipo "text".
Overloads ou um parâmetro union?
Os overloads valem as linhas extras quando o tipo de retorno depende dos tipos dos argumentos. Quando não depende, uma única assinatura com um parâmetro union é mais curta, mais fácil de ler e aceita argumentos union que os overloads rejeitariam.
| Use | Quando |
|---|---|
| Um parâmetro union | O tipo de retorno é o mesmo para qualquer entrada |
| Parâmetros opcionais | Os formatos de chamada só diferem por argumentos finais que podem ser omitidos à vontade |
| Overloads | O tipo de retorno muda com os argumentos, ou algumas combinações de argumentos precisam ser rejeitadas |
| Um generic | O tipo de retorno é montado a partir do tipo do argumento, como identity<T>(x: T): T |
Um generic com um conditional type consegue expressar alguns conjuntos de overloads como uma única assinatura, mas para dois ou três casos os overloads costumam ser mais fáceis de ler.
Métodos e construtores sobrecarregados
Métodos usam o mesmo padrão dentro de uma classe: overload signatures e depois o método com corpo. Construtores podem ser sobrecarregados do mesmo jeito.
Interfaces e tipos de objeto também podem declarar overloads, como várias call signatures ou várias method signatures com o mesmo nome. Muitas funções nativas são declaradas assim: passe o mouse sobre reduce em um array no editor e ele mostra "+2 overloads".
Perguntas frequentes
O TypeScript suporta sobrecarga de funções?
Sim, no nível dos tipos. Você escreve várias overload signatures (declarações sem corpo) seguidas de uma implementação. Quem chama só vê as overload signatures. Em tempo de execução continua existindo uma única função JavaScript, então a implementação verifica ela mesma os argumentos e trata todos os casos.
O que significa "No overload matches this call"?
Erro TS2769: os argumentos não se encaixam em nenhuma das overload signatures. A implementation signature não conta, então uma chamada com um argumento union como string | string[] falha mesmo quando a implementação o aceita. Adicione um overload que receba a union, ou troque os overloads por uma única assinatura.
Quando usar overloads em vez de um union type?
Use overloads quando o tipo de retorno depende dos tipos de argumento passados, por exemplo string na entrada dá number na saída, mas string[] na entrada dá number[] na saída. Quando o tipo de retorno é o mesmo para qualquer entrada, uma única assinatura com um parâmetro union é mais simples e também aceita argumentos union.
Arrow functions podem ter overloads no TypeScript?
Não com a sintaxe de declaração de overloads, que só funciona em declarações function e em métodos. Você pode dar a uma variável um tipo com várias call signatures, type Parse = { (s: string): number; (s: string[]): number[] }, mas atribuir uma arrow function a ele normalmente exige uma type assertion, então uma declaração function é a escolha mais limpa.
Por que minha overload signature não é compatível com a implementation signature?
O erro TS2394 significa que um overload aceita ou retorna algo que a implementação não aceita ou não retorna. Os parâmetros da implementação precisam aceitar os parâmetros de todos os overloads, e o tipo de retorno dela precisa ser compatível com o de cada overload. Ampliar a implementação (muitas vezes para uma union) resolve.