Variadic tuple types

1. Introdução aos Variadic Tuple Types

Variadic tuple types foram introduzidos no TypeScript 4.0 como uma evolução poderosa para trabalhar com tuplas de tamanho dinâmico. Eles permitem declarar tipos de tupla que podem aceitar um número variável de elementos, utilizando a sintaxe ...T (spread) dentro de definições de tipo.

Antes dessa feature, manipular tuplas de tamanho desconhecido exigia sobrecargas de função ou tipos genéricos limitados. A principal diferença entre spreads em valores e spreads em tipos é que, enquanto ...arr em valores expande um array, ...T em tipos representa um conjunto de elementos de tipo desconhecido que será inferido.

// Spread em valores (runtime)
const arr = [1, 2, 3];
const novo = [...arr, 4]; // [1, 2, 3, 4]

// Spread em tipos (compile-time)
type ComPrefixo<T extends unknown[]> = [string, ...T];
type Exemplo = ComPrefixo<[number, boolean]>; // [string, number, boolean]

2. Sintaxe e Operações Fundamentais

A sintaxe básica utiliza ... dentro de colchetes de tupla para representar uma sequência variádica:

type AdicionaNoFinal<T extends unknown[], V> = [...T, V];
type AdicionaNoInicio<T extends unknown[], V> = [V, ...T];

type Teste1 = AdicionaNoFinal<[string, number], boolean>; // [string, number, boolean]
type Teste2 = AdicionaNoInicio<[string, number], boolean>; // [boolean, string, number]

Para inferir partes de uma tupla, usamos infer em tipos condicionais:

type ExtraiPrimeiro<T extends unknown[]> = T extends [infer P, ...unknown[]] ? P : never;
type Primeiro = ExtraiPrimeiro<[string, number, boolean]>; // string

type ExtraiUltimo<T extends unknown[]> = T extends [...unknown[], infer U] ? U : never;
type Ultimo = ExtraiUltimo<[string, number, boolean]>; // boolean

Com readonly e as const, podemos criar tuplas imutáveis e inferir tipos literais:

const tupla = [1, 'texto', true] as const;
// type: readonly [1, "texto", true]

type ProcessaTupla<T extends readonly unknown[]> = [...T, number];
type Resultado = ProcessaTupla<typeof tupla>; // readonly [1, "texto", true, number]

3. Tipos Utilitários com Variadic Tuples

Vamos implementar tipos utilitários clássicos:

// Concat: concatena duas tuplas
type Concat<T extends unknown[], U extends unknown[]> = [...T, ...U];
type Concatenado = Concat<[1, 2], [3, 4]>; // [1, 2, 3, 4]

// Push: adiciona um elemento ao final
type Push<T extends unknown[], V> = [...T, V];
type ComPush = Push<[string, number], boolean>; // [string, number, boolean]

// Unshift: adiciona um elemento ao início
type Unshift<T extends unknown[], V> = [V, ...T];
type ComUnshift = Unshift<[string, number], boolean>; // [boolean, string, number]

// Zip: intercala elementos de duas tuplas
type Zip<T extends unknown[], U extends unknown[]> = T extends [infer TFirst, ...infer TRest]
  ? U extends [infer UFirst, ...infer URest]
    ? [[TFirst, UFirst], ...Zip<TRest, URest>]
    : []
  : [];

type Zippado = Zip<[1, 2, 3], ['a', 'b', 'c']>; // [[1, 'a'], [2, 'b'], [3, 'c']]

4. Pattern Matching e Inferência em Funções

Funções podem se beneficiar de variadic tuples para tipar parâmetros variádicos:

function concatenar<T extends unknown[]>(...args: [...T]): T {
  return args;
}

const resultado = concatenar(1, 'texto', true);
// type: [number, string, boolean]

// Usando Parameters<T> com variadic tuples
type MeusParametros = Parameters<typeof concatenar<[number, string]>>;
// type: [args_0: number, args_1: string]

// Função pipe com tipos inferidos dinamicamente
function pipe<T extends unknown[], R>(
  fn: (...args: T) => R
): (...args: T) => R {
  return fn;
}

const soma = pipe((a: number, b: number) => a + b);
// type: (a: number, b: number) => number

5. Transformações Avançadas de Tuplas

Transformações mais complexas são possíveis:

// MapTuple: mapeia tipos em cada posição
type MapTuple<T extends unknown[], F extends (item: any) => any> = {
  [K in keyof T]: T[K] extends infer Item ? F extends (x: Item) => infer R ? R : never : never;
};

type Mapeado = MapTuple<[string, number], (x: string | number) => string>;
// type: [string, string]

// FilterTuple: filtra condicionalmente (simplificado)
type FilterTuple<T extends unknown[], Predicate extends (item: any) => boolean> = 
  T extends [infer First, ...infer Rest]
    ? Predicate extends (x: First) => true
      ? [First, ...FilterTuple<Rest, Predicate>]
      : FilterTuple<Rest, Predicate>
    : [];

// Reverse: inverte ordem dos elementos
type Reverse<T extends unknown[]> = T extends [infer First, ...infer Rest]
  ? [...Reverse<Rest>, First]
  : [];

type Reverso = Reverse<[1, 2, 3, 4]>; // [4, 3, 2, 1]

6. Interação com Template Literal Types

A combinação com template literal types abre possibilidades interessantes:

// Join: une strings de uma tupla com um separador
type Join<T extends string[], Separator extends string> = 
  T extends [infer First extends string, ...infer Rest extends string[]]
    ? Rest extends []
      ? First
      : `${First}${Separator}${Join<Rest, Separator>}`
    : '';

type Unido = Join<['a', 'b', 'c'], '-'>; // "a-b-c"

// Validação de argumentos com strings literais
function criarRota<T extends string[]>(...parts: T): `/${Join<T, '/'>}` {
  return `/${parts.join('/')}` as any;
}

const rota = criarRota('api', 'users', ':id');
// type: "/api/users/:id"

7. Casos de Uso Reais e Padrões

Funções curry com número variável de argumentos

type Curry<T extends unknown[], R> = 
  T extends [infer First, ...infer Rest]
    ? (arg: First) => Curry<Rest, R>
    : R;

function curry<T extends unknown[], R>(fn: (...args: T) => R): Curry<T, R> {
  return function curried(...args: any[]): any {
    if (args.length >= fn.length) {
      return fn(...args as T);
    }
    return (...next: any[]) => curried(...args, ...next);
  } as any;
}

const somaCurry = curry((a: number, b: number, c: number) => a + b + c);
const resultadoCurry = somaCurry(1)(2)(3); // 6

Eventos com payloads tipados

type Evento<T extends unknown[]> = {
  tipo: string;
  payload: [...T];
};

function criarEvento<T extends unknown[]>(tipo: string, ...payload: T): Evento<T> {
  return { tipo, payload };
}

const evento = criarEvento('click', 10, 20);
// type: Evento<[number, number]>

Função merge para múltiplos objetos

function merge<T extends object[]>(...objetos: T): T[number] {
  return Object.assign({}, ...objetos);
}

const obj1 = { a: 1 };
const obj2 = { b: 2 };
const obj3 = { c: 3 };

const merged = merge(obj1, obj2, obj3);
// type: { a: number; } & { b: number; } & { c: number; }

8. Limitações e Boas Práticas

Apesar do poder, variadic tuples têm limitações:

  1. Complexidade de tipos: Tipos muito grandes ou recursivos podem causar lentidão no compilador
  2. Profundidade máxima: TypeScript impõe limites de recursão (tipicamente 50 níveis)
  3. Inferência limitada: Em alguns casos, a inferência pode falhar com tipos muito complexos

Boas práticas:
- Prefira variadic tuples quando o número de elementos é realmente variável
- Use sobrecargas de função para casos com até 3-4 parâmetros
- Evite recursão profunda em tipos utilitários
- Documente tipos complexos com comentários

// Alternativa com sobrecarga (mais simples)
function soma(a: number, b: number): number;
function soma(a: number, b: number, c: number): number;
function soma(a: number, b: number, c?: number): number {
  return c !== undefined ? a + b + c : a + b;
}

// Quando usar variadic tuples
function somaVariadica<T extends number[]>(...args: T): number {
  return args.reduce((acc, val) => acc + val, 0);
}

Variadic tuple types são uma ferramenta poderosa para criar APIs type-safe e flexíveis, mas devem ser usadas com discernimento para não comprometer a legibilidade e performance do código.

Referências