Técnicas de metaprogramação TypeScript explicadas

Metaprogramação é uma técnica poderosa que permite que programas manipulem a si mesmos ou a outros programas. Em TypeScript, metaprogramação se refere à capacidade de usar tipos, genéricos e decoradores para aprimorar a flexibilidade e a abstração do código. Este artigo explora as principais técnicas de metaprogramação em TypeScript e como implementá-las efetivamente.

1. Usando genéricos para código flexível

Genéricos permitem que funções e classes trabalhem com uma variedade de tipos, aumentando a flexibilidade e a reutilização do código. Ao introduzir parâmetros de tipo, podemos tornar nosso código genérico, mantendo a segurança do tipo.

function identity<T>(arg: T): T {
  return arg;
}

const num = identity<number>(42);
const str = identity<string>("Hello");

Neste exemplo, <T> permite que a função identity aceite qualquer tipo e retorne o mesmo tipo, garantindo flexibilidade e segurança de tipo.

2. Inferência de Tipos e Tipos Condicionais

O sistema de inferência de tipos do TypeScript infere automaticamente os tipos de expressões. Além disso, os tipos condicionais permitem criar tipos que dependem de condições, permitindo técnicas de metaprogramação mais avançadas.

type IsString<T> = T extends string ? true : false;

type Test1 = IsString<string>;  // true
type Test2 = IsString<number>;  // false

Neste exemplo, IsString é um tipo condicional que verifica se um dado tipo T estende string. Ele retorna true para strings e false para outros tipos.

3. Tipos mapeados

Tipos mapeados são uma maneira de transformar um tipo em outro iterando sobre as propriedades de um tipo. Isso é especialmente útil em metaprogramação para criar variações de tipos existentes.

type ReadOnly<T> = {
  readonly [K in keyof T]: T[K];
};

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

const user: ReadOnly<User> = {
  name: "John",
  age: 30,
};

// user.name = "Doe";  // Error: Cannot assign to 'name' because it is a read-only property.

Aqui, ReadOnly é um tipo mapeado que torna todas as propriedades de um dado tipo readonly. Isso garante que objetos desse tipo não possam ter suas propriedades modificadas.

4. Tipos de Literal de Modelo

TypeScript permite que você manipule tipos de string com literais de template. Esse recurso habilita metaprogramação para operações baseadas em string.

type WelcomeMessage<T extends string> = `Welcome, ${T}!`;

type Message = WelcomeMessage<"Alice">;  // "Welcome, Alice!"

Essa técnica pode ser útil para gerar tipos de string dinamicamente, o que é comum em grandes aplicativos que dependem de padrões de string consistentes.

5. Definições de Tipos Recursivos

TypeScript permite tipos recursivos, que são tipos que se referem a si mesmos. Isso é especialmente útil para metaprogramação ao lidar com estruturas de dados complexas, como objetos JSON ou dados profundamente aninhados.

type Json = string | number | boolean | null | { [key: string]: Json } | Json[];

const data: Json = {
  name: "John",
  age: 30,
  friends: ["Alice", "Bob"],
};

Neste exemplo, Json é um tipo recursivo que pode representar qualquer estrutura de dados JSON válida, permitindo representações de dados flexíveis.

6. Decoradores para Metaprogramação

Decoradores em TypeScript são uma forma de metaprogramação usada para modificar ou anotar classes e métodos. Eles nos permitem aplicar comportamento dinamicamente, tornando-os ideais para registro, validação ou injeção de dependência.

function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${propertyKey} with`, args);
    return originalMethod.apply(this, args);
  };
}

class Calculator {
  @Log
  add(a: number, b: number): number {
    return a + b;
  }
}

const calc = new Calculator();
calc.add(2, 3);  // Logs: "Calling add with [2, 3]"

Neste exemplo, o decorador Log registra o nome do método e os argumentos toda vez que o método add é chamado. Esta é uma maneira poderosa de estender ou modificar o comportamento sem alterar diretamente o código do método.

Conclusão

Os recursos de metaprogramação do TypeScript permitem que os desenvolvedores escrevam códigos flexíveis, reutilizáveis ​​e escaláveis. Técnicas como genéricos, tipos condicionais, decoradores e tipos literais de modelo abrem novas possibilidades para a construção de aplicativos robustos e sustentáveis. Ao dominar esses recursos avançados, você pode desbloquear todo o potencial do TypeScript em seus projetos.