Funções tipadas: parâmetros, retorno e overloads

1. Fundamentos da tipagem em funções

A tipagem em funções no TypeScript começa com a anotação explícita de tipos nos parâmetros e no valor de retorno. A sintaxe básica é direta:

function somar(a: number, b: number): number {
  return a + b;
}

O TypeScript também é capaz de inferir o tipo de retorno automaticamente quando o corpo da função é simples o suficiente:

function multiplicar(a: number, b: number) {
  return a * b; // TypeScript infere que o retorno é number
}

Para funções que não retornam valor, utilizamos o tipo void. É importante notar a diferença entre void e undefined:

function logMensagem(mensagem: string): void {
  console.log(mensagem);
  // Não há return, ou return sem valor
}

function retornaUndefined(): undefined {
  return undefined; // Obrigatório retornar explicitamente
}

Enquanto void indica que a função não deve ter retorno útil, undefined exige que o valor undefined seja explicitamente retornado.

2. Parâmetros opcionais e com valores padrão

Parâmetros opcionais são declarados com ? após o nome. Uma regra importante: parâmetros opcionais devem vir após os obrigatórios:

function criarUsuario(nome: string, idade?: number): string {
  if (idade !== undefined) {
    return `Usuário: ${nome}, Idade: ${idade}`;
  }
  return `Usuário: ${nome}`;
}

console.log(criarUsuario("Ana")); // Usuário: Ana
console.log(criarUsuario("João", 30)); // Usuário: João, Idade: 30

Parâmetros com valores padrão são mais flexíveis e o TypeScript infere o tipo automaticamente:

function configurarTimeout(ms: number = 1000): void {
  setTimeout(() => console.log(`Executando após ${ms}ms`), ms);
}

configurarTimeout(); // Usa 1000ms
configurarTimeout(3000); // Usa 3000ms

Uma diferença sutil: parâmetro?: tipo permite que o valor seja undefined ou omitido, enquanto parâmetro: tipo = valor define um fallback automático.

3. Parâmetros rest (...args) tipados

Para funções que aceitam quantidade variável de argumentos, usamos o operador rest com tipagem de array:

function somarTodos(...numeros: number[]): number {
  return numeros.reduce((acc, curr) => acc + curr, 0);
}

console.log(somarTodos(1, 2, 3, 4)); // 10

Podemos combinar parâmetros fixos com rest:

function criarFrase(prefixo: string, ...palavras: string[]): string {
  return `${prefixo}: ${palavras.join(" ")}`;
}

console.log(criarFrase("Frase", "TypeScript", "é", "poderoso"));

Para tipos heterogêneos, utilizamos tuplas:

function misturar(...args: [string, number, boolean]): void {
  const [texto, numero, bool] = args;
  console.log(texto, numero, bool);
}

misturar("teste", 42, true);

4. Tipos de função e callbacks

Podemos declarar tipos de função como variáveis ou parâmetros. A sintaxe usa seta (=>) para separar parâmetros do retorno:

type OperacaoMatematica = (a: number, b: number) => number;

const adicao: OperacaoMatematica = (x, y) => x + y;
const subtracao: OperacaoMatematica = (x, y) => x - y;

Type aliases são excelentes para reutilizar assinaturas complexas. Para callbacks, a tipagem garante segurança:

function processarArray(
  itens: number[],
  callback: (item: number, indice: number) => void
): void {
  itens.forEach((item, indice) => callback(item, indice));
}

processarArray([1, 2, 3], (item, indice) => {
  console.log(`Índice ${indice}: ${item}`);
});

5. Sobrecarga de funções (function overloads)

Sobrecarga permite que uma função tenha múltiplas assinaturas. Declaramos várias assinaturas e uma implementação única:

function processar(valor: string): string[];
function processar(valor: number): number;
function processar(valor: string | number): string[] | number {
  if (typeof valor === "string") {
    return valor.split("");
  }
  return valor * 2;
}

console.log(processar("abc")); // ["a", "b", "c"]
console.log(processar(5));     // 10

Casos comuns incluem funções que aceitam diferentes tipos de parâmetros e retornam tipos correspondentes:

function obterDados(id: number): { id: number; nome: string };
function obterDados(ids: number[]): { id: number; nome: string }[];
function obterDados(entrada: number | number[]): any {
  if (Array.isArray(entrada)) {
    return entrada.map(id => ({ id, nome: `Usuário ${id}` }));
  }
  return { id: entrada, nome: `Usuário ${entrada}` };
}

6. Funções genéricas básicas (introdução)

Genéricos permitem que funções trabalhem com tipos variados mantendo a relação entre parâmetro e retorno:

function primeiroElemento<T>(array: T[]): T | undefined {
  return array[0];
}

const numero = primeiroElemento([1, 2, 3]); // tipo inferido como number
const texto = primeiroElemento(["a", "b"]); // tipo inferido como string

Podemos restringir genéricos com extends para maior segurança:

function obterPropriedade<T, K extends keyof T>(obj: T, chave: K): T[K] {
  return obj[chave];
}

const pessoa = { nome: "Maria", idade: 25 };
console.log(obterPropriedade(pessoa, "nome")); // Maria
// console.log(obterPropriedade(pessoa, "altura")); // Erro: 'altura' não existe

Genéricos são ideais quando o tipo de retorno depende do tipo de entrada.

7. Boas práticas e armadilhas comuns

Evite any sempre que possível. Prefira tipos específicos ou genéricos:

// Ruim
function identidade(value: any): any {
  return value;
}

// Bom
function identidade<T>(value: T): T {
  return value;
}

Cuidado com overloads excessivas. Se os tipos de parâmetros são muito variados, considere usar união de tipos:

// Em vez de várias overloads:
function formatar(data: Date): string;
function formatar(data: string): string;
function formatar(data: number): string;

// Considere:
function formatar(data: Date | string | number): string {
  // implementação única
}

Parâmetro this tipado é útil em funções que são usadas como métodos:

type ObjetoComNome = { nome: string };

function saudacao(this: ObjetoComNome): string {
  return `Olá, ${this.nome}!`;
}

const obj = { nome: "Carlos", saudacao };
console.log(obj.saudacao()); // Olá, Carlos!

O TypeScript verifica se o this está sendo usado no contexto correto, prevenindo erros comuns.

Referências

  • TypeScript Handbook: Functions — Documentação oficial sobre funções tipadas, incluindo parâmetros, retorno e overloads.
  • TypeScript: Function Overloads — Seção específica sobre sobrecarga de funções na documentação oficial.
  • TypeScript: Generics — Guia completo sobre funções genéricas e restrições com extends.
  • TypeScript Deep Dive: Functions — Tutorial avançado sobre funções em TypeScript, com exemplos práticos de callbacks e tipos de função.
  • [TypeScript: Callback Types and Best Practices](https://www.typescriptlang.org/play/#code/PTAEHkFcEsEsAcBcCmBPAZqFA9gOwBc4BLAOwHMAXKAUwCcBPAZToAc4BzOAYzQGNIAvKAA+oANoBdAJRIA3KAC+iRq3Zc+A4aIlSZ8hUpVqNmrDjyESZclVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR4qTPlLla8lVr0mAXT3DR

Evite overloads quando a lógica de implementação for a mesma. Overloads são úteis quando você precisa de diferentes assinaturas que se comportam de maneiras distintas, mas se a implementação é idêntica, uma união de tipos ou genéricos é mais adequada.

Prefira tipos específicos a any em callbacks:

// Ruim
function executar(fn: (dados: any) => void) {}

// Bom
function executar<T>(fn: (dados: T) => void) {}

Documente overloads complexas. Quando usar múltiplas overloads, explique o propósito de cada uma:

/**
 * Busca um usuário por ID (retorna um único objeto)
 * ou por array de IDs (retorna array de objetos)
 */
function buscarUsuario(id: number): Usuario;
function buscarUsuario(ids: number[]): Usuario[];
function buscarUsuario(entrada: number | number[]): Usuario | Usuario[] {
  // implementação
}

Conclusão

Funções tipadas em TypeScript oferecem um sistema poderoso para garantir segurança de tipos em todo o fluxo de dados da aplicação. Desde a simples anotação de parâmetros e retorno até overloads e genéricos, cada recurso tem seu lugar específico:

  • Use parâmetros opcionais e valores padrão para flexibilidade controlada
  • Recorra a parâmetros rest para funções com número variável de argumentos
  • Defina tipos de função para reutilizar assinaturas e tipar callbacks
  • Aplique overloads quando diferentes combinações de parâmetros exigem retornos específicos
  • Utilize genéricos para criar funções reutilizáveis que preservam a relação entre tipos

O equilíbrio entre expressividade e segurança é a chave: evite any, prefira tipos específicos ou genéricos, e não crie overloads desnecessárias quando uniões de tipos resolvem o problema.