Type aliases e interfaces: diferenças e quando usar cada um

1. Conceitos Fundamentais

O que são Type Aliases?

Type aliases são definidos com a palavra-chave type e permitem criar nomes para qualquer tipo em TypeScript. Diferentemente das interfaces, eles podem representar tipos primitivos, uniões, interseções e tipos complexos.

// Tipos primitivos
type ID = string | number;
type Status = 'active' | 'inactive' | 'pending';

// Uniões e interseções
type SuccessResponse = { data: unknown; status: 200 };
type ErrorResponse = { error: string; status: 400 | 500 };
type ApiResponse = SuccessResponse | ErrorResponse;

// Tuplas
type Coordinate = [number, number];
type Pair = [string, number];

// Tipos utilitários
type Nullable<T> = T | null;
type ReadonlyUser<T> = { readonly [K in keyof T]: T[K] };

O que são Interfaces?

Interfaces são definidas com a palavra-chave interface e são usadas principalmente para descrever contratos de objetos. Elas são extensíveis naturalmente e suportam declaração de métodos.

interface User {
  id: number;
  name: string;
  email: string;
  getFullName(): string;
}

interface Config {
  apiUrl: string;
  timeout: number;
  retries?: number;
}

Semelhanças superficiais

Ambos podem descrever a forma de um objeto e são usados em anotações de tipo:

// Type alias
type PersonType = {
  name: string;
  age: number;
};

// Interface
interface PersonInterface {
  name: string;
  age: number;
}

// Ambos funcionam da mesma forma para objetos simples
const person1: PersonType = { name: "Alice", age: 30 };
const person2: PersonInterface = { name: "Bob", age: 25 };

2. Diferenças Estruturais e Semânticas

Extensibilidade e Declaration Merging

Interfaces suportam declaration merging — múltiplas declarações com o mesmo nome são automaticamente mescladas:

interface User {
  name: string;
}

interface User {
  age: number;
}

// Resultado: User tem name e age
const user: User = { name: "Alice", age: 30 };

Type aliases não permitem redeclaração:

type User = { name: string };
type User = { age: number }; // Erro: identificador duplicado

Suporte a Tipos Não-Objeto

Type aliases são mais flexíveis para tipos que não são objetos:

// Type aliases podem representar qualquer tipo
type Primitive = string | number | boolean;
type Tuple = [string, number];
type MappedType<T> = { [K in keyof T]: string };

// Interfaces são restritas a objetos e funções
interface Callable {
  (x: number): string;
}

Herança e Composição

Interfaces usam extends para herança múltipla:

interface BaseUser {
  id: number;
  name: string;
}

interface Admin extends BaseUser {
  role: 'admin';
  permissions: string[];
}

// Implementação em classes
class AdminUser implements Admin {
  id: number;
  name: string;
  role: 'admin';
  permissions: string[];
}

Type aliases usam interseção (&) para composição:

type BaseUser = { id: number; name: string };
type Admin = BaseUser & { role: 'admin'; permissions: string[] };

3. Casos de Uso Onde Interfaces Brilham

APIs Públicas e Bibliotecas

Interfaces são ideais para APIs públicas devido ao declaration merging:

// Biblioteca
export interface HttpClient {
  get(url: string): Promise<Response>;
  post(url: string, data: unknown): Promise<Response>;
}

// Consumidor pode estender
interface HttpClient {
  baseUrl: string;
}

Contratos de Objetos Complexos

Hierarquias claras com extends:

interface Animal {
  name: string;
  age: number;
}

interface Mammal extends Animal {
  furColor: string;
}

interface Dog extends Mammal {
  breed: string;
  bark(): void;
}

4. Casos de Uso Onde Type Aliases São Preferíveis

Tipos Compostos e Uniões

type Status = 'active' | 'inactive' | 'pending';
type Result<T> = { success: true; data: T } | { success: false; error: string };

function processResult(result: Result<number>) {
  if (result.success) {
    console.log(result.data); // number
  } else {
    console.error(result.error); // string
  }
}

Tipos Utilitários e Mapeados

type PartialConfig<T> = { [K in keyof T]?: T[K] };
type Readonly<T> = { readonly [K in keyof T]: T[K] };
type Nullable<T> = T | null;

type FunctionPropertyNames<T> = {
  [K in keyof T]: T[K] extends Function ? K : never
}[keyof T];

5. Limitações e Armadilhas Comuns

Type Aliases: Sem Declaration Merging

Para bibliotecas que precisam de extensão por terceiros, isso pode ser limitante:

// Em uma biblioteca
type Config = { apiUrl: string };

// Consumidor não pode estender automaticamente
type ExtendedConfig = Config & { timeout: number }; // Solução manual

Interfaces: Incapacidade de Representar Uniões

// Isto não funciona com interfaces
interface Result = Success | Error; // Erro de sintaxe

// Solução com type
type Result = Success | Error;

6. Guia Prático de Decisão

Use Interfaces quando:

  • Definir contratos para objetos em APIs públicas
  • Precisar de declaration merging
  • O tipo será implementado por classes
  • Criar hierarquias com herança múltipla

Use Type Aliases quando:

  • Precisar de uniões, interseções ou tuplas
  • Criar tipos utilitários genéricos
  • Trabalhar com tipos primitivos
  • O tipo for complexo e não se encaixar em objeto simples

Regra de Ouro da Comunidade

Comece com interface para objetos, troque para type se precisar de uniões. Seja consistente dentro do mesmo projeto.

7. Exemplos Comparativos e Boas Práticas

Refatoração de Interface para Type

// Antes: Interface limitante
interface ApiResponse {
  data: unknown;
  error?: string;
  status: number;
}

// Depois: Type mais flexível
type ApiResponse<T> = 
  | { success: true; data: T; status: 200 }
  | { success: false; error: string; status: 400 | 500 };

Combinação de Ambos

interface User {
  name: string;
  email: string;
}

type AdminUser = User & { role: 'admin' };
type ReadonlyUser = Readonly<User>;

Recomendações Finais

  • Prefira interfaces em código público e types em código interno complexo
  • Evite misturar estilos sem critério claro
  • Documente a escolha no guia de estilo da equipe
  • Lembre-se: a escolha entre type e interface é mais sobre semântica e manutenibilidade do que funcionalidade

Referências