Objetos tipados e propriedades opcionais

1. Fundamentos da Tipagem de Objetos

Em TypeScript, a tipagem de objetos começa com a definição explícita da estrutura que um objeto deve seguir. A forma mais básica é o tipo literal de objeto:

const usuario: { nome: string; idade: number } = {
  nome: "Ana",
  idade: 28
};

Para evitar repetição, criamos type aliases que encapsulam a estrutura do objeto:

type Usuario = {
  nome: string;
  idade: number;
  email: string;
};

const ana: Usuario = {
  nome: "Ana",
  idade: 28,
  email: "ana@exemplo.com"
};

Interfaces oferecem uma alternativa igualmente poderosa, com a vantagem de serem extensíveis:

interface IUsuario {
  nome: string;
  idade: number;
  email: string;
}

interface IUsuarioAdmin extends IUsuario {
  nivelAcesso: number;
}

A escolha entre type e interface depende do contexto. Interfaces são preferíveis para definições de contratos públicos e bibliotecas, enquanto types oferecem mais flexibilidade com uniões e interseções.

2. Propriedades Opcionais com ?

Propriedades opcionais permitem que um objeto funcione corretamente mesmo sem determinados campos. A sintaxe utiliza ? após o nome da propriedade:

type Configuracao = {
  url: string;
  timeout?: number; // opcional
  retry?: boolean;  // opcional
};

const config1: Configuracao = { url: "https://api.exemplo.com" };
const config2: Configuracao = { url: "https://api.exemplo.com", timeout: 5000 };

É crucial entender a diferença entre prop?: Tipo e prop: Tipo | undefined:

type Exemplo1 = {
  campo?: string; // pode não existir OU ser undefined
};

type Exemplo2 = {
  campo: string | undefined; // DEVE existir, mas pode ser undefined
};

const obj1: Exemplo1 = {}; // válido
const obj2: Exemplo2 = {}; // erro: propriedade 'campo' está faltando

3. Propriedades Readonly

O modificador readonly impede a reatribuição de uma propriedade após a criação do objeto:

type Ponto = {
  readonly x: number;
  readonly y: number;
};

const p: Ponto = { x: 10, y: 20 };
p.x = 15; // Erro: não é possível atribuir a 'x' porque é uma propriedade somente leitura

Podemos combinar readonly com opcionais:

type Documento = {
  readonly id: string;
  readonly dataCriacao?: Date; // opcional e imutável
  conteudo: string;
};

Importante: readonly impede a reatribuição da referência, mas não torna o valor profundo imutável. Objetos aninhados ainda podem ser modificados.

4. Tipos de União e Interseção em Objetos

União de tipos de objeto permite que uma variável assuma diferentes estruturas:

type Resposta = 
  | { status: "sucesso"; dados: unknown }
  | { status: "erro"; mensagem: string };

function processarResposta(res: Resposta) {
  if (res.status === "sucesso") {
    console.log(res.dados); // TypeScript sabe que é seguro
  } else {
    console.log(res.mensagem);
  }
}

Interseção combina múltiplos tipos em um único objeto:

type Identificavel = { id: number };
type Nomeavel = { nome: string };

type Entidade = Identificavel & Nomeavel;

const entidade: Entidade = {
  id: 1,
  nome: "Exemplo"
};

Cuidado com propriedades conflitantes em interseções:

type A = { valor: number };
type B = { valor: string };
type C = A & B; // 'valor' se torna `never` (number & string é impossível)

5. Index Signatures e Propriedades Dinâmicas

Para objetos com chaves dinâmicas, usamos index signatures:

type Dicionario = {
  [chave: string]: number;
};

const precos: Dicionario = {
  "maçã": 2.50,
  "banana": 1.80
};

Podemos combinar propriedades fixas com dinâmicas:

type Inventario = {
  readonly id: string;
  [item: string]: number | string; // propriedades dinâmicas
};

const estoque: Inventario = {
  id: "INV-001",
  "camisetas": 50,
  "calças": 30
};

O tipo utilitário Record<K, V> simplifica a criação de dicionários tipados:

type Notas = Record<string, number>;
const notasAlunos: Notas = {
  "Ana": 8.5,
  "Bruno": 7.0
};

6. Narrowing e Verificação de Propriedades

O operador in verifica a existência de propriedades em tempo de execução:

type Carro = { motor: string; portas: number };
type Bicicleta = { quadro: string };

function exibirDetalhes(veiculo: Carro | Bicicleta) {
  if ("motor" in veiculo) {
    console.log(`Motor: ${veiculo.motor}`); // TypeScript faz narrowing
  } else {
    console.log(`Quadro: ${veiculo.quadro}`);
  }
}

Para propriedades opcionais, use verificação explícita:

type Perfil = {
  nome: string;
  idade?: number;
};

function saudacao(perfil: Perfil) {
  const idade = perfil.idade ?? "não informada";
  console.log(`${perfil.nome}, idade: ${idade}`);
}

Desestruturação com valores padrão oferece segurança:

function configurar({ url, timeout = 3000, retry = false }: Configuracao) {
  console.log(`URL: ${url}, Timeout: ${timeout}, Retry: ${retry}`);
}

7. Utility Types para Manipulação de Objetos

TypeScript fornece utility types poderosos para transformar tipos de objetos:

type Usuario = {
  nome: string;
  email: string;
  idade: number;
};

// Partial: todas as propriedades se tornam opcionais
type UsuarioParcial = Partial<Usuario>;

// Required: todas as propriedades se tornam obrigatórias
type UsuarioCompleto = Required<Partial<Usuario>>;

// Pick: seleciona propriedades específicas
type NomeEmail = Pick<Usuario, "nome" | "email">;

// Omit: remove propriedades específicas
type SemIdade = Omit<Usuario, "idade">;

// Readonly: todas as propriedades se tornam imutáveis
type UsuarioImutavel = Readonly<Usuario>;

Exemplo prático combinando utility types:

function atualizarUsuario(
  id: number,
  mudancas: Partial<Usuario>
): Usuario {
  // lógica de atualização
  return { ...usuarioOriginal, ...mudancas };
}

8. Boas Práticas e Padrões Comuns

Evite excesso de propriedades opcionais. Muitas opcionais indicam design frágil. Prefira tipos separados ou discriminated unions:

// Ruim: muitas opcionais
type NotificacaoRuim = {
  tipo?: string;
  mensagem?: string;
  destinatario?: string;
};

// Bom: discriminated union
type Notificacao =
  | { tipo: "email"; mensagem: string; destinatario: string }
  | { tipo: "sms"; mensagem: string; telefone: string };

Documente propriedades opcionais complexas com JSDoc:

type ConfiguracaoAvancada = {
  /** URL base da API */
  baseUrl: string;
  /**
   * Timeout em milissegundos.
   * @default 5000
   */
  timeout?: number;
  /**
   * Estratégia de retry em caso de falha.
   * - 'none': sem retry
   * - 'linear': retry com intervalo fixo
   * - 'exponential': retry com backoff exponencial
   */
  retryStrategy?: 'none' | 'linear' | 'exponential';
};

Prefira interfaces para contratos públicos e types para composições complexas. Use readonly sempre que uma propriedade não deve ser alterada após a inicialização. Para objetos com muitas variantes, discriminated unions oferecem type safety superior a propriedades opcionais.

Dominar objetos tipados e propriedades opcionais em TypeScript é fundamental para escrever código expressivo, seguro e de fácil manutenção. A combinação correta de tipos, utility types e padrões de design eleva significativamente a qualidade do seu código.

Referências