Functions

Os princípios de função do JavaScript valem aqui inteiros: uma responsabilidade por função, o orquestrador no topo, nenhuma lógica dentro do return. O TypeScript acrescenta uma camada de contrato. A signature (assinatura, a lista de parâmetros tipados mais o tipo de retorno) passa a ser a documentação da função, e ela precisa se sustentar sozinha: quem chama entende o que entra e o que sai sem abrir a implementação. Isso pede return type (tipo de retorno) explícito nas funções exportadas, e generics (tipos paramétricos) quando o retorno depende do tipo que o chamador passou.

Conceitos fundamentais

ConceitoO que é
return type (tipo de retorno)Anotação do que a função devolve; obrigatória em função exportada
signature (assinatura)Lista de parâmetros tipados + return type; o contrato público da função
generic (tipo paramétrico)Parâmetro de tipo (<T>) que carrega tipo do caller para o retorno
overload (sobrecarga)Múltiplas assinaturas para a mesma implementação; usar com parcimônia
parameter type (tipo de parâmetro)Tipo do argumento na entrada; aceita union, intersection ou genérico
default parameter (parâmetro padrão)Valor usado quando o argumento é undefined (function f(x = 0))
arrow function (função flecha)() => {}: sintaxe curta sem this próprio; ideal para callbacks
void return (retorno vazio)Função sem valor de retorno significativo; declara : void explicitamente

Toda função exportada declara o que devolve

O compilador infere o retorno sozinho, então a anotação existe por dois outros motivos. O primeiro é o leitor: Promise<User | null> na assinatura avisa que o usuário pode não existir, e quem chama trata o caso sem precisar abrir a função. O segundo é a estabilidade do contrato. Sem a anotação, o dia em que alguém mudar o corpo da função e passar a devolver outra coisa, o tipo público muda junto, sem um erro sequer no arquivo que foi editado. O erro aparece longe dali, nos arquivos que consomem a função.

❌ Ruim: return type implícito em função exportada
export async function findUserById(id: string) {
  const user = await db.users.findById(id);
  return user; // o que retorna? depende de olhar a implementação
}

export function calculateInvoiceTotal(items: LineItem[]) {
  const total = items.reduce((sum, item) => sum + item.price * item.qty, 0);
  return total;
}
✅ Bom: return type explícito nas funções exportadas
export async function findUserById(id: string): Promise<User | null> {
  const user = await userRepository.findById(id);
  return user;
}

export function calculateInvoiceTotal(items: LineItem[]): number {
  const total = items.reduce((sum, item) => sum + item.price * item.qty, 0);
  return total;
}

O objeto de entrada ganha uma interface própria

Quando a função recebe um objeto de configuração ou de domínio, declare o tipo em uma interface separada e use o nome dela no parâmetro. Escrever o objeto inteiro dentro dos parênteses empurra cinco linhas de campo para dentro da assinatura, e o leitor que queria saber o que a função faz precisa atravessar a lista antes de chegar ao retorno. Vale a mesma regra do estilo vertical: a partir de quatro campos, os argumentos viram um objeto, e o objeto vira uma interface.

❌ Ruim: o tipo escrito dentro dos parênteses esconde a assinatura
function createInvoice(data: {
  orderId: string;
  customerId: string;
  amount: number;
  dueDate: string;
  currency: string;
}): Promise<Invoice> { /* ... */ }
✅ Bom: interface separada, assinatura limpa
interface CreateInvoiceInput {
  orderId: string;
  customerId: string;
  amount: number;
  dueDate: string;
  currency: string;
}

function createInvoice(input: CreateInvoiceInput): Promise<Invoice> { /* ... */ }

Os sufixos Input e Result nomeiam o contrato da operação

User e Invoice são tipos do domínio: existem no negócio, e sobrevivem a qualquer função. O que uma operação recebe e devolve é outra coisa, que só existe por causa dela. Os sufixos Input e Result (ou Output) marcam essa diferença. CreateUserInput diz o que a criação de usuário precisa receber, e CreateUserResult diz o que ela entrega, sem que nenhum dos dois se confunda com o User que o sistema inteiro usa.

❌ Ruim: parâmetros soltos e um objeto de retorno que ninguém nomeou
async function createUser(
  name: string,
  email: string,
  password: string
): Promise<{ user: User; token: string }> {
  const user = await persistUser({ name, email, password });
  const token = generateToken(user.id);

  const result = { user, token };

  return result;
}
✅ Bom: o contrato da operação tem nome próprio, separado do tipo de domínio
interface CreateUserInput {
  name: string;
  email: string;
  password: string;
}

interface CreateUserResult {
  user: User;
  token: string;
}

async function createUser(input: CreateUserInput): Promise<CreateUserResult> {
  const user = await persistUser(input);
  const token = generateToken(user.id);

  const result = { user, token };
  return result;
}

O overload amarra o tipo do retorno ao tipo da entrada

Uma função que devolve string | number obriga quem chama a checar qual dos dois veio, mesmo quando isso já está decidido: quem passou uma string sabe que vai receber um number. O overload (sobrecarga, várias assinaturas declaradas para a mesma implementação) escreve essa relação em código, e o compilador passa a saber dela. parse("42") tem tipo number, e nenhuma checagem sobra para quem chamou. Declare overload quando a variação é real; para uma assinatura que não varia, ele é uma linha a mais para manter.

❌ Ruim: o retorno é `string | number`, e quem chama não sabe qual dos dois veio
function parse(value: string | number): string | number {
  if (typeof value === "string") return parseInt(value, 10);
  return value.toString();
}

const result = parse("42"); // string | number: compilador não sabe que é number
✅ Bom: overloads tornam o contrato preciso
function parse(value: string): number;
function parse(value: number): string;
function parse(value: string | number): string | number {
  if (typeof value === "string") return parseInt(value, 10);
  return value.toString();
}

const asNumber = parse("42"); // number, compilador sabe
const asString = parse(42); // string, compilador sabe

O genérico serve para o retorno acompanhar o argumento

Um genérico se paga quando o tipo do retorno depende do tipo que entrou. firstOrThrow é o caso: passe um Order[] e receba um Order, passe um User[] e receba um User, sem as e sem checagem no lado de quem chamou. Quando o tipo declarado não aparece no retorno, o genérico adiciona um parâmetro de tipo à assinatura e não entrega nada em troca. logAndReturn<T> funciona igual com unknown, e validateSchema<T> devolve boolean de qualquer jeito.

❌ Ruim: o genérico não chega ao retorno, então não serve para nada
function logAndReturn<T>(value: T): T {
  console.log(value); // o genérico não adiciona nada aqui: unknown seria suficiente
  return value;
}

function validateSchema<T>(schema: ZodSchema<T>, data: unknown): boolean {
  return schema.safeParse(data).success; // T não aparece no retorno: desnecessário
}
✅ Bom: genérico quando o tipo do retorno depende do argumento
function firstOrThrow<TItem>(items: TItem[]): TItem {
  if (items.length === 0) throw new NotFoundError({ message: "List is empty." });

  const first = items[0];
  return first;
}

function parseSchema<TOutput>(schema: ZodSchema<TOutput>, data: unknown): TOutput {
  const result = schema.parse(data); // retorna TOutput: o genérico é necessário
  return result;
}

DoDocs v3.7.0 · Desenvolvido por @thiagocajadev · Baseado no trabalho de pmndrs/docs · Poimandres.