Como Usar Keyof, Typeof e Indexed Access Types em TypeScript em Produção Já leu

Entendendo Keyof em TypeScript O operador é um dos recursos mais poderosos do TypeScript para trabalhar com tipos de forma dinâmica e segura. Ele extrai as chaves de um objeto ou interface e as transforma em um tipo união literal. Quando você aplica a um tipo, recebe um novo tipo que representa todas as propriedades possíveis daquele objeto. A utilidade prática do aparece quando você precisa garantir que uma string ou variável corresponda exatamente a uma das propriedades existentes. Em vez de aceitar qualquer string, você força o sistema de tipos a validar se a chave é válida. Isso elimina erros em tempo de desenvolvimento e torna seu código muito mais robusto. Casos de Uso Práticos com Keyof Um padrão muito comum é criar funções genéricas que trabalham com propriedades de um objeto sem perder a segurança de tipos. A combinação de com genéricos permite que você acesse propriedades de forma type-safe, mantendo a inteligência do editor de código funcionando

Entendendo Keyof em TypeScript

O operador keyof é um dos recursos mais poderosos do TypeScript para trabalhar com tipos de forma dinâmica e segura. Ele extrai as chaves de um objeto ou interface e as transforma em um tipo união literal. Quando você aplica keyof a um tipo, recebe um novo tipo que representa todas as propriedades possíveis daquele objeto.

A utilidade prática do keyof aparece quando você precisa garantir que uma string ou variável corresponda exatamente a uma das propriedades existentes. Em vez de aceitar qualquer string, você força o sistema de tipos a validar se a chave é válida. Isso elimina erros em tempo de desenvolvimento e torna seu código muito mais robusto.

interface Usuario {
  id: number;
  nome: string;
  email: string;
  ativo: boolean;
}

type ChavesUsuario = keyof Usuario;
// Equivalente a: type ChavesUsuario = "id" | "nome" | "email" | "ativo"

const chave: ChavesUsuario = "nome"; // ✅ Válido
const chaveInvalida: ChavesUsuario = "telefone"; // ❌ Erro de compilação

function obterPropriedade(usuario: Usuario, chave: keyof Usuario) {
  return usuario[chave];
}

const user: Usuario = { id: 1, nome: "João", email: "joao@email.com", ativo: true };
const valor = obterPropriedade(user, "nome"); // ✅ TypeScript infere o tipo correto

Casos de Uso Práticos com Keyof

Um padrão muito comum é criar funções genéricas que trabalham com propriedades de um objeto sem perder a segurança de tipos. A combinação de keyof com genéricos permite que você acesse propriedades de forma type-safe, mantendo a inteligência do editor de código funcionando perfeitamente.

interface Produto {
  id: number;
  titulo: string;
  preco: number;
  estoque: number;
}

function atualizarProduto<K extends keyof Produto>(
  produto: Produto,
  propriedade: K,
  valor: Produto[K]
): void {
  produto[propriedade] = valor;
}

const meuProduto: Produto = {
  id: 1,
  titulo: "Notebook",
  preco: 3000,
  estoque: 5
};

atualizarProduto(meuProduto, "preco", 2800); // ✅ Correto
atualizarProduto(meuProduto, "titulo", "Notebook Gamer"); // ✅ Correto
atualizarProduto(meuProduto, "preco", "barato"); // ❌ Erro: "barato" não é number

Typeof: Inferindo Tipos de Valores

O typeof em TypeScript funciona de forma diferente do JavaScript puro. Enquanto em JavaScript ele retorna uma string em tempo de execução, no TypeScript ele é um operador de tipo que funciona em tempo de compilação. Ele permite extrair o tipo de qualquer expressão, variável ou valor e usá-lo como um tipo.

Essa funcionalidade é especialmente valiosa quando você tem dados complexos cujo tipo não foi explicitamente definido, mas você quer garantir que outras partes do código respeitem esse tipo automaticamente. Em vez de reescrever a interface manualmente, o typeof infere o tipo para você.

const configuracao = {
  apiUrl: "https://api.exemplo.com",
  timeout: 5000,
  retentativas: 3,
  debug: false
};

type ConfigurationType = typeof configuracao;
// Equivalente a:
// type ConfigurationType = {
//   apiUrl: string;
//   timeout: number;
//   retentativas: number;
//   debug: boolean;
// }

function aplicarConfiguracao(config: ConfigurationType): void {
  console.log(`Conectando em ${config.apiUrl} com timeout ${config.timeout}ms`);
}

aplicarConfiguracao(configuracao); // ✅ Correto

Typeof com Funções e Classes

O typeof também extrai o tipo de funções e construtores, sendo extremamente útil para trabalhar com callbacks, factories e padrões mais avançados. Você pode garantir que uma função receba o tipo correto de callback sem repetir sua assinatura.

const processarDados = (dados: string[], opcoes?: { verbose?: boolean }) => {
  if (opcoes?.verbose) {
    console.log(`Processando ${dados.length} itens`);
  }
  return dados.map(d => d.toUpperCase());
};

type ProcessarDadosType = typeof processarDados;

const meuCallback: ProcessarDadosType = (dados, opcoes) => {
  console.log("Executando callback");
  return dados.map(d => `[${d}]`);
};

// Com classes
class RepositorioUsuario {
  async buscarPorId(id: number) {
    return { id, nome: "Ana" };
  }
}

type RepositorioType = typeof RepositorioUsuario;
const instancia: InstanceType<RepositorioType> = new RepositorioUsuario();

Indexed Access Types: Acessando Tipos Dinamicamente

Os tipos de acesso indexado (Indexed Access Types) permitem que você acesse o tipo de uma propriedade específica de outro tipo usando a notação de colchetes. É como se você estivesse "subscrevendo" um tipo para obter o tipo de uma de suas propriedades. Esse recurso é fundamental para criar código genérico que se adapta aos dados que você está manipulando.

Quando combinado com keyof, o acesso indexado cria possibilidades extraordinárias para transformações de tipo. Você pode mapear tipos, validar correspondências entre chaves e valores, e construir abstrações poderosas que antes exigiriam código repetido.

interface NotaFiscal {
  numero: string;
  emissao: Date;
  valor: number;
  cliente: string;
}

type NumeroNotaFiscal = NotaFiscal["numero"]; // string
type DataNotaFiscal = NotaFiscal["emissao"]; // Date
type ValorNotaFiscal = NotaFiscal["valor"]; // number

const numero: NumeroNotaFiscal = "NF-001"; // ✅ Correto
const data: DataNotaFiscal = new Date(); // ✅ Correto
const valor: ValorNotaFiscal = 1500.50; // ✅ Correto

// Usando com literais de união
type PropriedadesNumericas = NotaFiscal[keyof NotaFiscal];
// Equivalente a: string | Date | number

Padrões Avançados com Acesso Indexado

A verdadeira potência emerge quando você combina acesso indexado com genéricos. Você pode criar funções que extraem tipos de valores dinamicamente, mapeiam estruturas e criam abstrações que escalam com segurança de tipos.

interface APIResponse {
  status: number;
  dados: { usuarios: Array<{ id: number; nome: string }> };
  mensagem: string;
}

type DadosAPI = APIResponse["dados"]; // { usuarios: Array<{ id: number; nome: string }> }
type UsuariosAPI = APIResponse["dados"]["usuarios"]; // Array<{ id: number; nome: string }>
type UsuarioAPI = APIResponse["dados"]["usuarios"][number]; // { id: number; nome: string }

// Criando uma função genérica que extrai tipos
function extrairPropriedade<T, K extends keyof T>(
  objeto: T,
  chave: K
): T[K] {
  return objeto[chave];
}

const resposta: APIResponse = {
  status: 200,
  dados: {
    usuarios: [
      { id: 1, nome: "Maria" },
      { id: 2, nome: "Pedro" }
    ]
  },
  mensagem: "Sucesso"
};

const usuarios = extrairPropriedade(resposta, "dados"); // tipo inferido como { usuarios: ... }
const status = extrairPropriedade(resposta, "status"); // tipo inferido como number

Mapeando Tipos com Acesso Indexado

Um padrão extremamente prático é criar tipos mapeados que transformam todas as propriedades de um tipo. Você pode converter um objeto em suas versões getters, setters, observáveis ou qualquer outra transformação, mantendo a segurança de tipos para cada propriedade.

interface Formulario {
  nome: string;
  email: string;
  idade: number;
  ativo: boolean;
}

// Tipo que converte todas as propriedades em getters
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

type FormularioGetters = Getters<Formulario>;
// Resultado:
// {
//   getNome: () => string;
//   getEmail: () => string;
//   getIdade: () => number;
//   getAtivo: () => boolean;
// }

class FormularioImpl implements FormularioGetters {
  private _nome = "";
  private _email = "";
  private _idade = 0;
  private _ativo = false;

  getNome() { return this._nome; }
  getEmail() { return this._email; }
  getIdade() { return this._idade; }
  getAtivo() { return this._ativo; }
}

Integrando os Três Conceitos

Para dominar completamente esses recursos, é essencial entender como eles funcionam em harmonia. Um padrão real que você encontrará frequentemente envolve usar keyof para identificar chaves válidas, typeof para inferir tipos de objetos dinâmicos, e acesso indexado para extrair o tipo específico de uma propriedade.

Esse trio forma a base de bibliotecas populares como Zod, Prisma e React Query. Entender como combinar esses conceitos permite que você crie abstrações poderosas que escalam com seu projeto sem sacrificar a segurança de tipos.

// Cenário real: Validador genérico de formulários

interface Usuario {
  id: number;
  nome: string;
  email: string;
  dataNascimento: Date;
}

const validadores = {
  id: (valor: any): valor is number => typeof valor === "number" && valor > 0,
  nome: (valor: any): valor is string => typeof valor === "string" && valor.length > 0,
  email: (valor: any): valor is string => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(valor),
  dataNascimento: (valor: any): valor is Date => valor instanceof Date
};

type ValidadoresType = typeof validadores;

function validarPropriedade<T, K extends keyof T>(
  tipo: T,
  propriedade: K,
  valor: unknown,
  validador: ValidadoresType[K & keyof ValidadoresType]
): valor is T[K] {
  return validador(valor) as boolean;
}

const usuarioInput = {
  id: 1,
  nome: "Carlos",
  email: "carlos@email.com",
  dataNascimento: new Date("1990-05-15")
};

// Verificação type-safe
if (validarPropriedade(usuarioInput, "id", 1, validadores.id)) {
  console.log("ID válido");
}

// Função de alto nível que combina os três conceitos
function criarValidadorGenerico<T>(objeto: T, schema: { [K in keyof T]: (v: unknown) => v is T[K] }) {
  return function validar(dados: Partial<T>): dados is T {
    return (Object.keys(schema) as (keyof T)[]).every(chave => {
      if (!(chave in dados)) return false;
      return schema[chave](dados[chave]);
    });
  };
}

const validadorUsuario = criarValidadorGenerico(usuarioInput, validadores);
const dadosTeste: Partial<Usuario> = usuarioInput;

if (validadorUsuario(dadosTeste)) {
  console.log("Usuário válido!");
}

Conclusão

Nesta aula, você aprendeu que keyof extrai as chaves de um tipo como união literal, permitindo que funções genéricas validem se uma propriedade realmente existe em um objeto sem sacrificar segurança de tipos. Esse operador é essencial para criar interfaces fluidas entre dados dinâmicos e código tipado.

typeof infere o tipo de qualquer expressão ou valor em tempo de compilação, transformando dados existentes em tipos reutilizáveis sem necessidade de reescrever interfaces manualmente. Ele é seu aliado quando trabalhando com configurações, constantes ou estruturas que já existem no código.

Indexed Access Types (acesso indexado) permite extrair o tipo de uma propriedade específica usando notação de colchetes, e quando combinado com os anteriores, cria abstrações poderosas para transformação e mapeamento de tipos. Esses três conceitos juntos formam a fundação de padrões avançados em TypeScript que você verá em código profissional de alta qualidade.


Referências


Artigos relacionados