Module augmentation: estendendo tipos de bibliotecas

1. O que é Module Augmentation e por que usar?

Module augmentation é um recurso do TypeScript que permite estender declarações de tipos existentes em módulos de terceiros sem modificar o código original da biblioteca. Em vez de criar definições de tipo paralelas ou recorrer a any, você pode adicionar propriedades, métodos ou sobrescrever comportamentos tipados de forma segura.

Casos de uso comuns incluem:
- Adicionar propriedades a objetos globais como Express.Request.user
- Estender protótipos nativos com métodos customizados
- Complementar tipos parciais de bibliotecas que não exportam tudo que você precisa

A principal diferença entre module augmentation e declaration merging é o escopo: enquanto declaration merging ocorre globalmente em arquivos .d.ts (scripts), module augmentation acontece dentro de módulos, respeitando o sistema de módulos do TypeScript.

2. Sintaxe básica de Module Augmentation

A estrutura fundamental utiliza declare module 'nome-do-modulo' para reabrir o módulo e adicionar novas declarações:

// types/express-augmentation.d.ts
import 'express';

declare module 'express' {
  interface Request {
    user?: {
      id: string;
      name: string;
      email: string;
    };
  }
}

Para que a augmentation seja reconhecida, o arquivo deve ser um módulo (conter pelo menos um import ou export) e estar incluído no tsconfig.json:

{
  "compilerOptions": {
    "typeRoots": ["./node_modules/@types", "./types"]
  },
  "include": ["src/**/*", "types/**/*"]
}

3. Estendendo tipos de bibliotecas populares

Express: adicionando propriedades a Request e Response

// types/express-augmentation.d.ts
import 'express';

declare module 'express' {
  interface Request {
    user?: { id: string; role: 'admin' | 'user' };
    startTime: number;
  }

  interface Response {
    sendSuccess(data: unknown): void;
    sendError(message: string, code?: number): void;
  }
}

React: estendendo atributos HTML

// types/react-augmentation.d.ts
import 'react';

declare module 'react' {
  interface HTMLAttributes<T> {
    'data-testid'?: string;
    'aria-label'?: string;
  }

  // Estendendo IntrinsicElements para elementos customizados
  interface IntrinsicElements {
    'my-component': React.DetailedHTMLProps<
      React.HTMLAttributes<HTMLElement> & { prop: string },
      HTMLElement
    >;
  }
}

Prisma: adicionando métodos a modelos

// types/prisma-augmentation.d.ts
import { PrismaClient } from '@prisma/client';

declare module '@prisma/client' {
  interface User {
    fullName(): string;
    isActive(): boolean;
  }
}

Lodash: estendendo funções utilitárias

// types/lodash-augmentation.d.ts
import 'lodash';

declare module 'lodash' {
  interface LoDashStatic {
    // Adiciona um método customizado
    deepClone<T>(obj: T): T;
  }
}

4. Trabalhando com módulos que exportam namespaces

Para estender namespaces, a sintaxe é similar, mas você precisa referenciar o namespace dentro do módulo:

// types/jquery-augmentation.d.ts
import 'jquery';

declare module 'jquery' {
  interface JQueryStatic {
    // Adiciona método estático
    myCustomPlugin(options: Record<string, unknown>): void;
  }

  interface JQuery {
    // Adiciona método de instância
    customMethod(): this;
  }
}

Quando o namespace está aninhado, use a mesma estrutura:

declare module 'biblioteca' {
  namespace NamespaceExterno {
    interface NamespaceInterno {
      novaPropriedade: string;
    }
  }
}

5. Augmentação de tipos genéricos e utilitários

Estender tipos genéricos requer cuidado com a poluição do escopo global:

// types/array-augmentation.d.ts
interface Array<T> {
  first(): T | undefined;
  last(): T | undefined;
  pluck<K extends keyof T>(key: K): T[K][];
}

// Implementação (arquivo .ts)
if (!Array.prototype.first) {
  Array.prototype.first = function<T>(this: T[]): T | undefined {
    return this[0];
  };
}

Para tipos que não são de módulo, use declare global:

// types/global-augmentation.d.ts
declare global {
  interface String {
    capitalize(): string;
    toTitleCase(): string;
  }

  interface Window {
    myAppConfig: { version: string; debug: boolean };
  }
}

export {}; // Torna o arquivo um módulo

6. Boas práticas e armadilhas comuns

Manter augmentations em arquivos separados

Organize as augmentações em types/augmentations.d.ts ou em arquivos por biblioteca:

// types/augmentations/express.d.ts
// types/augmentations/react.d.ts
// types/augmentations/prisma.d.ts

Evitar sobrescrever tipos existentes

Use interseção & quando precisar combinar tipos:

declare module 'express' {
  interface Request {
    // Não sobrescreva, adicione
    user?: { id: string } & { role: string };
  }
}

Problemas com versões da biblioteca

Sempre verifique a versão da biblioteca e suas definições de tipo. Atualizações podem quebrar augmentations existentes.

Testando se a augmentation está sendo reconhecida

// Em qualquer arquivo .ts
import express from 'express';
const app = express();

app.use((req, res, next) => {
  // Se o TypeScript reconhecer 'user', a augmentation funcionou
  console.log(req.user?.name);
  next();
});

7. Casos avançados: merged types com mapped types e utilitários

Combinando module augmentation com mapped types para estender dinamicamente:

// types/orm-augmentation.d.ts
import 'some-orm';

declare module 'some-orm' {
  interface Model {
    // Adiciona método de auditoria a todos os modelos
    auditTrail(): Promise<AuditEntry[]>;
  }
}

// Usando mapped types para adicionar métodos a propriedades específicas
type ModelWithTimestamps<T> = T & {
  createdAt: Date;
  updatedAt: Date;
};

declare module 'some-orm' {
  interface User extends ModelWithTimestamps<User> {}
}

Uso de utilitários dentro da augmentation:

declare module 'biblioteca' {
  interface Config {
    // Garante que propriedades opcionais sejam requeridas
    options: Required<Pick<OriginalOptions, 'host' | 'port'>>;
  }
}

Integração com satisfies para segurança adicional:

const config = {
  host: 'localhost',
  port: 3000,
} satisfies Required<Pick<Config['options'], 'host' | 'port'>>;

8. Resumo e checklist de implementação

Checklist para implementar module augmentation:

  1. ✅ Verifique se o módulo alvo é declarado com declare module 'nome'
  2. ✅ Crie um arquivo .d.ts separado com pelo menos um import
  3. ✅ Adicione o arquivo ao tsconfig.json (include ou typeRoots)
  4. ✅ Estenda interfaces ou namespaces, não sobrescreva
  5. ✅ Teste se o compilador reconhece as novas propriedades
  6. ✅ Para tipos globais, use declare global e export {}

Quando preferir declaration merging vs module augmentation:

  • Declaration merging: para tipos globais (sem módulo) ou quando o arquivo é um script
  • Module augmentation: para módulos com import/export, respeitando o escopo do módulo

Próximos passos:

  • Explore satisfies para validação de tipos sem widening
  • Aprofunde-se em declaration merging para tipos globais
  • Estude mapped types e conditional types para augmentations mais complexas

Referências