Funções em TypeScript: Assinaturas, Overloads e this Tipado na Prática Já leu

Fundamentos de Funções em TypeScript Uma função em TypeScript é bem mais que um bloco de código reutilizável. É um contrato entre você e o compilador sobre o que entra, o que sai e como se comporta. Diferentemente de JavaScript puro, TypeScript exige que você seja explícito sobre tipos de parâmetros e retorno, o que reduz bugs em tempo de execução e melhora a inteligência das ferramentas de desenvolvimento. Toda função em TypeScript começa com uma declaração clara de tipos. Mesmo que você não declare explicitamente, o TypeScript infere os tipos com base no contexto. Isso não é opcional em um projeto profissional — é o alicerce que permite confiar no código. Olá, ${nome} ${sobrenome} Olá, ${nome} Observe que os tipos vêm após os parâmetros ( ) e o tipo de retorno vem após os parênteses ( ). Isso é sintaxe TypeScript pura. O compilador valida essas assinaturas antes mesmo de você executar o código. --- Assinaturas de Função: O

Fundamentos de Funções em TypeScript

Uma função em TypeScript é bem mais que um bloco de código reutilizável. É um contrato entre você e o compilador sobre o que entra, o que sai e como se comporta. Diferentemente de JavaScript puro, TypeScript exige que você seja explícito sobre tipos de parâmetros e retorno, o que reduz bugs em tempo de execução e melhora a inteligência das ferramentas de desenvolvimento.

Toda função em TypeScript começa com uma declaração clara de tipos. Mesmo que você não declare explicitamente, o TypeScript infere os tipos com base no contexto. Isso não é opcional em um projeto profissional — é o alicerce que permite confiar no código.

// Declaração básica com tipos explícitos
function somar(a: number, b: number): number {
  return a + b;
}

// Função com parâmetros opcionais
function saudacao(nome: string, sobrenome?: string): string {
  return sobrenome ? `Olá, ${nome} ${sobrenome}` : `Olá, ${nome}`;
}

// Função com valor padrão
function criar(tipo: string = "padrão"): object {
  return { tipo, criadoEm: new Date() };
}

// Função com rest parameters (aceita múltiplos argumentos)
function concatenar(...palavras: string[]): string {
  return palavras.join(" ");
}

// Arrow function tipada
const multiplicar = (x: number, y: number): number => x * y;

Observe que os tipos vêm após os parâmetros (:tipo) e o tipo de retorno vem após os parênteses (): tipo). Isso é sintaxe TypeScript pura. O compilador valida essas assinaturas antes mesmo de você executar o código.


Assinaturas de Função: O Contrato Explícito

Uma assinatura de função é a definição do seu contrato — quantos parâmetros, quais tipos, qual o retorno. Em TypeScript, você pode declarar assinaturas separadamente da implementação, o que é especialmente poderoso em cenários de complexidade.

Assinaturas Simples e Compostas

A forma mais simples é declarar a função e deixar o TypeScript inferir tudo. Mas isso não é profissional quando você trabalha em equipes. O ideal é ser explícito:

// Assinatura com tipos aninhados
function processar(dados: { nome: string; idade: number }): { sucesso: boolean; msg: string } {
  return {
    sucesso: dados.idade >= 18,
    msg: dados.idade >= 18 ? "Maior de idade" : "Menor de idade"
  };
}

// Usando tipos nomeados (mais limpo)
interface Usuario {
  nome: string;
  idade: number;
}

interface Resultado {
  sucesso: boolean;
  msg: string;
}

function processarUsuario(dados: Usuario): Resultado {
  return {
    sucesso: dados.idade >= 18,
    msg: dados.idade >= 18 ? "Maior de idade" : "Menor de idade"
  };
}

A segunda abordagem é claramente superior. Interfaces e tipos nomeados tornam o código legível e reutilizável. Quando alguém ler processarUsuario, imediatamente sabe exatamente o que entra e o que sai.

Assinaturas com Genéricos

Genéricos são a forma TypeScript de escrever funções flexíveis mantendo segurança de tipo. Ao invés de aceitar any (o inimigo da segurança), você deixa o tipo ser descoberto dinamicamente:

// Função genérica que retorna o mesmo tipo que recebe
function primeiro<T>(array: T[]): T | undefined {
  return array[0];
}

// TypeScript infere o tipo automaticamente
const num = primeiro([1, 2, 3]); // num é number
const str = primeiro(["a", "b"]); // str é string

// Genérico com restrição
function obterPropriedade<T, K extends keyof T>(obj: T, chave: K): T[K] {
  return obj[chave];
}

const pessoa = { nome: "Ana", idade: 28 };
const nome = obterPropriedade(pessoa, "nome"); // string
// obterPropriedade(pessoa, "email"); // ERRO: email não existe em pessoa

Essa é a verdadeira força do TypeScript: segurança de tipo sem sacrificar flexibilidade. O genérico <T> diz "qualquer tipo", mas mantém a coerência dentro da função.


Overloads: Múltiplas Assinaturas, Uma Implementação

Overload em TypeScript permite que uma função tenha múltiplas assinaturas válidas. Não é sobrecarga como em Java ou C++ — é um recurso de compilação que oferece melhor experiência ao desenvolvedor e segurança de tipo mais rigorosa.

Casos de Uso Reais

Imagine uma função que aceita tanto strings quanto números, mas se comporta diferentemente com cada tipo. Sem overload, você teria que usar any ou string | number, perdendo a segurança de tipo:

// SEM overload (ruim)
function processar(valor: string | number): string | number {
  if (typeof valor === "string") {
    return valor.toUpperCase();
  }
  return valor * 2;
}

// O problema: o retorno é impreciso
const resultado = processar("olá"); // TypeScript não sabe se é string

Agora com overload:

// COM overload (correto)
function processar(valor: string): string;
function processar(valor: number): number;
function processar(valor: string | number): string | number {
  if (typeof valor === "string") {
    return valor.toUpperCase();
  }
  return valor * 2;
}

// Agora TypeScript entende a relação exata
const resultado1 = processar("olá");  // string
const resultado2 = processar(5);      // number

As duas primeiras linhas são assinaturas (sem implementação). A terceira é a implementação real, que precisa ser compatível com todas as assinaturas. Quando você chama a função, TypeScript usa a assinatura mais apropriada.

Overload com Interfaces e Tipos Complexos

Overloads brilham quando combinados com tipos complexos:

interface ConfigAPI {
  url: string;
  metodo: "GET" | "POST";
  body?: unknown;
}

// Sobrecargas: GET retorna string, POST retorna object
function requisicao(config: { url: string; metodo: "GET" }): Promise<string>;
function requisicao(config: { url: string; metodo: "POST"; body: unknown }): Promise<object>;
function requisicao(config: ConfigAPI): Promise<string | object> {
  // Implementação simulada
  if (config.metodo === "GET") {
    return Promise.resolve("dados");
  }
  return Promise.resolve({ status: 200 });
}

// Uso tipado com segurança
requisicao({ url: "/users", metodo: "GET" }).then(data => {
  console.log(data); // string
});

requisicao({ url: "/users", metodo: "POST", body: { nome: "João" } }).then(data => {
  console.log(data); // object
});

O compilador valida que quando metodo é "POST", body é obrigatório. Sem overload, seria impossível expressar essa relação.

Limitações de Overload

Overload não resolve tudo. Ele é bom para funções utilitárias, mas não substitui boas decisões arquiteturais. Se você precisa de mais de 3-4 overloads, seu design pode estar ruim. Nesses casos, considere usar genéricos ou padrões como factory functions.

// Genérico é melhor aqui que múltiplos overloads
function transformar<T, R>(valor: T, transformador: (v: T) => R): R {
  return transformador(valor);
}

transformar(5, n => n * 2);           // 10
transformar("oi", s => s.length);     // 2

this Tipado: Controlando o Contexto de Execução

this é famoso por ser confuso em JavaScript. TypeScript oferece uma forma elegante de controlar seu tipo e evitar erros em tempo de execução.

O Problema com this não Tipado

Em JavaScript, this depende de como a função é chamada, não de onde é definida. Isso causa bugs:

// Sem tipagem explícita (problema)
const obj = {
  nome: "João",
  saudar: function() {
    console.log(this.nome); // 'this' pode ser undefined ou outra coisa
  }
};

obj.saudar(); // funciona
const fn = obj.saudar;
fn(); // ERRO: this é undefined ou window

Solução: Parâmetro this Explícito

TypeScript permite declarar this como primeiro parâmetro (não contado na chamada):

interface Usuario {
  nome: string;
  idade: number;
}

// 'this' é tipado como Usuario
function apresentar(this: Usuario): string {
  return `Sou ${this.nome} e tenho ${this.idade} anos`;
}

const usuario: Usuario = { nome: "Maria", idade: 30 };

// Precisa ser chamado com call, apply ou bind
console.log(apresentar.call(usuario));   // "Sou Maria e tenho 30 anos"
console.log(apresentar.apply(usuario));  // mesmo resultado

// Ou com bind
const apresentarMaria = apresentar.bind(usuario);
console.log(apresentarMaria()); // funciona sem argumentos

TypeScript agora valida que this é do tipo Usuario. Se você tentar chamar sem o contexto correto, há erro em tempo de compilação.

Classes e Métodos com this Tipado

Em classes, this é implicitamente tipado, mas você pode ser explícito para maior controle:

class Conta {
  saldo: number = 1000;

  depositar(this: Conta, valor: number): void {
    this.saldo += valor;
    console.log(`Novo saldo: ${this.saldo}`);
  }

  // Método arrow não precisa, pois já tem 'this' vinculado
  sacar = (valor: number): void => {
    this.saldo -= valor;
    console.log(`Novo saldo: ${this.saldo}`);
  };
}

const conta = new Conta();
conta.depositar(500);  // OK
conta.sacar(200);      // OK

// Com método regular, isso causaria erro
const dep = conta.depositar;
// dep(100); // ERRO: this não está vinculado

Caso Avançado: Validação de this em Callbacks

Uma aplicação real e poderosa é em callbacks de eventos ou observadores:

class Botao {
  elemento: HTMLButtonElement;
  cliques: number = 0;

  constructor(id: string) {
    this.elemento = document.getElementById(id) as HTMLButtonElement;
    // Usa arrow function para manter 'this' automático
    this.elemento.addEventListener("click", () => this.aoClicar());
  }

  // 'this' é explicitamente tipado como Botao
  aoClicar(this: Botao): void {
    this.cliques++;
    console.log(`Cliques: ${this.cliques}`);
  }
}

const botao = new Botao("meuBotao");

A palavra-chave this: Botao força o compilador a validar que o método é sempre chamado com a instância correta como contexto.


Conclusão

Dominando funções em TypeScript, você controla três dimensões essenciais do código: assinaturas precisas que documentam intenção, overloads que permitem múltiplas formas de uso sem sacrificar segurança, e this tipado que elimina erros sutis de contexto. Esses conceitos são a diferença entre código JavaScript "que funciona" e código TypeScript "que você entende e confia". Aplique-os consistentemente em seus projetos e verá imediatamente a redução de bugs em produção.


Referências


Artigos relacionados