Const type parameters e inferência mais precisa

1. O problema da inferência ampla em literais

1.1. Como o TypeScript infere tipos literais por padrão

Quando você escreve um literal em TypeScript, o compilador tende a inferir o tipo mais amplo possível. Por exemplo:

const mensagem = "hello"; // Tipo: "hello" (literal)
let saudacao = "hello";   // Tipo: string (amplo)

Essa diferença entre const e let é conhecida, mas em funções genéricas o problema se agrava:

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

const resultado = identidade("hello"); // Tipo: string, não "hello"

1.2. A limitação de as const em variáveis e objetos

O as const ajuda, mas tem limitações:

const config = {
  url: "https://api.exemplo.com",
  timeout: 5000
} as const;
// Tipo: { readonly url: "https://api.exemplo.com"; readonly timeout: 5000 }

Funciona para objetos fixos, mas não resolve o problema em parâmetros de função.

1.3. Cenários reais onde a inferência ampla causa perda de precisão

function criarRota<TPath extends string>(path: TPath) {
  return { path, metodo: "GET" as const };
}

const rota = criarRota("/usuarios/:id");
// TPath é inferido como string, não como "/usuarios/:id"

Isso impede pattern matching preciso e validações em tempo de compilação.

2. Introdução ao const type parameters

2.1. Sintaxe básica

function criarRota<const TPath extends string>(path: TPath) {
  return { path, metodo: "GET" as const };
}

const rota = criarRota("/usuarios/:id");
// TPath agora é inferido como "/usuarios/:id"

2.2. Diferença entre type parameters comuns e const

// Sem const
function semConst<T extends string>(arg: T) { return arg; }
const a = semConst("teste"); // Tipo: string

// Com const
function comConst<const T extends string>(arg: T) { return arg; }
const b = comConst("teste"); // Tipo: "teste"

2.3. Comportamento para diferentes tipos literais

function capturar<const T>(valor: T): T {
  return valor;
}

const num = capturar(42);      // Tipo: 42
const bool = capturar(true);   // Tipo: true
const str = capturar("abc");   // Tipo: "abc"

3. Inferência precisa em tuplas e arrays

3.1. Preservando comprimento exato de tuplas

function criarPar<const T extends readonly any[]>(...args: T): T {
  return args;
}

const par = criarPar("a", 42, true);
// Tipo: readonly ["a", 42, true] - comprimento exato preservado

3.2. Inferência em arrays heterogêneos

function processarElementos<const T extends readonly any[]>(elementos: T) {
  return elementos.map(el => String(el));
}

const resultado = processarElementos([1, "texto", false]);
// T é inferido como readonly [1, "texto", false]

3.3. Comparação com readonly arrays

// Sem const
function semConstArray<T extends readonly any[]>(arr: T) { return arr; }
const arr1 = semConstArray([1, 2]); // Tipo: readonly number[]

// Com const
function comConstArray<const T extends readonly any[]>(arr: T) { return arr; }
const arr2 = comConstArray([1, 2]); // Tipo: readonly [1, 2]

4. Objetos aninhados e deep inference

4.1. Comportamento em objetos com múltiplos níveis

function configurar<const T extends Record<string, any>>(config: T): T {
  return config;
}

const appConfig = configurar({
  api: { url: "https://api.com", port: 443 },
  debug: true
});
// Tipo preserva estrutura aninhada: { api: { url: "https://api.com", port: 443 }, debug: true }

4.2. Limitações em inferência profunda

function processarDeep<const T>(data: T): T {
  return data;
}

// Funciona para objetos simples
const obj1 = processarDeep({ a: { b: 1 } });
// Tipo: { a: { b: 1 } }

// Mas não para estruturas recursivas complexas
const obj2 = processarDeep({ items: [{ id: 1 }, { id: 2 }] });
// items é inferido como any[] em alguns casos

4.3. Combinação com satisfies

type Config = { url: string; timeout: number };

function criarConfig<const T extends Config>(config: T satisfies Config): T {
  return config;
}

const config = criarConfig({ url: "https://api.com", timeout: 5000 });
// Tipo exato preservado, mas valida contra Config

5. Uso prático em APIs e bibliotecas

5.1. Tipando funções de configuração

interface OpcaoConector {
  url: string;
  headers?: Record<string, string>;
}

function criarConector<const T extends OpcaoConector>(opcoes: T) {
  return {
    conectar: () => console.log(`Conectando a ${opcoes.url}`),
    opcoes
  };
}

const conector = criarConector({
  url: "https://api.exemplo.com",
  headers: { Authorization: "Bearer token123" }
});
// headers preserva tipo literal

5.2. Criando builders type-safe

class QueryBuilder<T extends Record<string, any>> {
  constructor(private query: T) {}

  where<K extends keyof T>(key: K, value: T[K]) {
    return new QueryBuilder({ ...this.query, [key]: value });
  }

  build(): T {
    return this.query;
  }
}

function criarQuery<const T extends Record<string, any>>(initial: T) {
  return new QueryBuilder(initial);
}

const query = criarQuery({ status: "ativo" })
  .where("status", "inativo")
  .build();
// Tipo: { status: "ativo" } & { status: "inativo" }

5.3. Roteamento tipado com paths literais

type Rota = {
  path: string;
  params: Record<string, string>;
};

function definirRota<const TPath extends string>(path: TPath): {
  path: TPath;
  params: TPath extends `${string}:${infer P}/${infer Rest}` 
    ? { [K in P | keyof definirRota<Rest>['params']]: string }
    : TPath extends `${string}:${infer P}` 
      ? { [K in P]: string }
      : {};
} {
  return { path, params: {} as any };
}

const rota = definirRota("/usuarios/:id/pedidos/:pedidoId");
// Tipo preserva parâmetros da rota

6. Interação com outras features do TypeScript

6.1. const vs as const

// Use const type parameter quando o valor vem como argumento
function comConst<const T>(arg: T): T { return arg; }

// Use as const para valores fixos
const valores = [1, 2, 3] as const;

6.2. Compatibilidade com tipos genéricos complexos

type Resposta<T> = { dados: T; erro?: string };

function processarResposta<const T>(dados: T): Resposta<T> {
  return { dados };
}

const resposta = processarResposta({ usuario: "João", idade: 30 });
// Resposta<{ usuario: string; idade: number }>

6.3. Limitações conhecidas (TypeScript 5.0+)

// Não funciona com tipos union complexos
function limitado<const T extends string | number>(arg: T) {
  return arg;
}

const x = limitado(Math.random() > 0.5 ? "texto" : 42);
// T é inferido como string | number, não como "texto" | 42

7. Padrões avançados e boas práticas

7.1. Uso em funções de alta ordem

function criarMiddleware<const T extends Record<string, any>>(handler: (ctx: T) => void) {
  return (ctx: T) => {
    console.log("Middleware executado");
    handler(ctx);
  };
}

const middleware = criarMiddleware((ctx: { usuario: string }) => {
  console.log(ctx.usuario);
});
// T preserva tipo do contexto

7.2. Inferência condicional combinada

type ExtrairTipo<T> = T extends Array<infer U> ? U : T;

function processar<const T>(valor: T): ExtrairTipo<T> {
  return (Array.isArray(valor) ? valor[0] : valor) as any;
}

const resultado = processar([1, 2, 3]); // Tipo: 1

7.3. Anti-patterns: quando evitar

// Evite usar const com tipos muito genéricos sem restrição
function ruim<const T>(arg: T): T { return arg; } // Muito permissivo

// Prefira restrições específicas
function bom<const T extends string | number>(arg: T): T { return arg; }

8. Comparação com alternativas e futuro

8.1. const vs type guards manuais vs satisfies

// Type guard manual
function isStringLiteral(valor: string): valor is "hello" | "world" {
  return valor === "hello" || valor === "world";
}

// satisfies para validação
const config = { url: "https://api.com" } satisfies { url: string };

// const type parameter para inferência em funções
function melhor<const T extends string>(arg: T): T { return arg; }

8.2. Impacto no roadmap do TypeScript

O const type parameters é um passo importante para:
- Pattern matching mais preciso
- Validação de tipos em tempo de compilação
- Melhor integração com bibliotecas de validação

8.3. Possíveis evoluções

// Preview conceitual de futuras features
type Pattern<T> = T extends `${infer A}:${infer B}` ? [A, B] : never;

function match<const T extends string>(pattern: T): Pattern<T> {
  return pattern.split(":") as any;
}

const [a, b] = match("nome:valor"); // Futuro: ["nome", "valor"]

Referências