Template literal types

Template literal types são uma feature poderosa do TypeScript introduzida na versão 4.1 que permite construir tipos baseados em strings dinâmicas usando a mesma sintaxe de template strings do JavaScript, mas operando no nível de tipos. Diferentemente das template strings do JavaScript, que produzem valores em tempo de execução, os template literal types geram novos tipos em tempo de compilação.

Sintaxe básica

A sintaxe utiliza crases e placeholders ${} com tipos:

type UserId = `user_${number}`;
// Tipo resultante: `user_${number}`

const validId: UserId = "user_123"; // ✅ OK
const invalidId: UserId = "admin_456"; // ❌ Erro: Tipo incompatível

Combinando Template Literals com Union Types

Quando interpolamos union types, o TypeScript expande automaticamente todas as combinações possíveis:

type EventName = `${'click' | 'hover' | 'focus'}_${'start' | 'end'}`;
// Resultado: "click_start" | "click_end" | "hover_start" | "hover_end" | "focus_start" | "focus_end"

type ApiEndpoint = `/api/${'users' | 'posts'}/${'list' | 'create' | 'delete'}`;
// Resultado: "/api/users/list" | "/api/users/create" | "/api/users/delete" | "/api/posts/list" | ...

function handleEvent(event: EventName) {
  console.log(`Processing event: ${event}`);
}

handleEvent("click_start"); // ✅ OK
handleEvent("hover_end");   // ✅ OK
handleEvent("scroll_start"); // ❌ Erro

Manipulação de Strings com Tipos Utilitários Intrínsecos

O TypeScript fornece quatro tipos utilitários para manipular strings em tempo de compilação:

type CssProperty = 'background-color' | 'font-size' | 'margin-top';

// Convertendo para camelCase
type CamelCase<T extends string> = T extends `${infer First}_${infer Rest}`
  ? `${Lowercase<First>}${Capitalize<Rest>}`
  : Lowercase<T>;

type CssCamelCase = CamelCase<CssProperty>;
// Resultado: "backgroundColor" | "fontSize" | "marginTop"

// Uppercase e Lowercase
type HttpMethod = 'GET' | 'POST' | 'PUT';
type HttpHeader = `X-${Capitalize<Lowercase<HttpMethod>>}-Token`;
// Resultado: "X-Get-Token" | "X-Post-Token" | "X-Put-Token"

Inferência e Extração de Partes com infer

O operador infer dentro de template literals permite extrair partes específicas de strings:

type ExtractId<T extends string> = 
  T extends `${infer Prefix}_${infer Id}_${infer Suffix}` 
    ? { prefix: Prefix; id: Id; suffix: Suffix }
    : never;

type Parsed = ExtractId<"user_123_active">;
// Resultado: { prefix: "user"; id: "123"; suffix: "active" }

type ExtractRouteParams<T extends string> = 
  T extends `${string}:${infer Param}/${infer Rest}`
    ? Param | ExtractRouteParams<Rest>
    : T extends `${string}:${infer Param}`
      ? Param
      : never;

type RouteParams = ExtractRouteParams<"/users/:userId/posts/:postId">;
// Resultado: "userId" | "postId"

Validação e Parsing de Strings em Tempo de Compilação

Podemos criar tipos que validam formatos específicos combinando template literals com conditional types:

// Validação de UUID
type UUID = `${string}-${string}-${string}-${string}-${string}`;

type ValidateUUID<T extends string> = 
  T extends UUID ? T : never;

function processUUID<T extends string>(uuid: ValidateUUID<T>) {
  return uuid;
}

processUUID("550e8400-e29b-41d4-a716-446655440000"); // ✅ OK
processUUID("invalid-uuid"); // ❌ Erro

// Validação de caminhos com parâmetros numéricos
type ValidPath<T extends string> = 
  T extends `/users/${number}/posts/${number}`
    ? T
    : `Invalid path: ${T}`;

type Path1 = ValidPath<"/users/123/posts/456">; // ✅ "/users/123/posts/456"
type Path2 = ValidPath<"/users/abc/posts/456">; // ❌ "Invalid path: /users/abc/posts/456"

Padrões Avançados com Recursão em Template Literals

Tipos recursivos permitem manipular strings de tamanho variável:

// Join: concatena elementos de um tuple 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 PathJoined = Join<["users", "posts", "comments"], "/">;
// Resultado: "users/posts/comments"

// Split: divide uma string em um tuple
type Split<T extends string, Separator extends string> = 
  T extends `${infer First}${Separator}${infer Rest}`
    ? [First, ...Split<Rest, Separator>]
    : [T];

type PathSplit = Split<"a.b.c.d", ".">;
// Resultado: ["a", "b", "c", "d"]

Limitações: O TypeScript limita a profundidade de recursão a aproximadamente 50 níveis. Para strings muito longas, considere alternativas.

Aplicações Práticas em Projetos Reais

Tipagem de rotas de frameworks

// React Router com tipos seguros
type RouteDefinition = {
  path: string;
  component: React.ComponentType;
};

type ExtractParams<T extends string> = 
  T extends `${string}:${infer Param}/${infer Rest}`
    ? { [K in Param | keyof ExtractParams<Rest>]: string }
    : T extends `${string}:${infer Param}`
      ? { [K in Param]: string }
      : {};

type UserRoute = ExtractParams<"/users/:userId/posts/:postId">;
// Resultado: { userId: string; postId: string }

function navigate<T extends string>(path: T, params: ExtractParams<T>) {
  // Implementação
}

navigate("/users/:userId/posts/:postId", { userId: "123", postId: "456" }); // ✅ OK

Geração de tipos para queries de banco de dados

type TableName = "users" | "posts" | "comments";
type ColumnName<T extends TableName> = 
  T extends "users" ? "id" | "name" | "email"
  : T extends "posts" ? "id" | "title" | "content" | "authorId"
  : "id" | "text" | "postId";

type Query<T extends TableName> = 
  `SELECT ${ColumnName<T>} FROM ${T} WHERE id = :id`;

type UserQuery = Query<"users">;
// Resultado: "SELECT id | name | email FROM users WHERE id = :id"

Armadilhas Comuns e Dicas de Performance

Limitação de profundidade

// ❌ Isso pode falhar para strings muito longas
type DeepRecursive<T extends string> = 
  T extends `${infer First}${infer Rest}`
    ? `${First}${DeepRecursive<Rest>}`
    : "";

// ✅ Prefira soluções não recursivas quando possível
type SimpleValidation<T extends string> = 
  T extends `/api/${string}/v${number}` ? T : never;

Explosão combinatória

// ❌ Evite unions grandes em template literals
type Dangerous = `${1 | 2 | 3 | 4 | 5}_${'a' | 'b' | 'c' | 'd' | 'e'}_${'x' | 'y' | 'z'}`;
// 5 * 5 * 3 = 75 combinações (aceitável)
// Mas 10 * 10 * 10 = 1000 combinações já pode afetar performance

// ✅ Prefira tipos mais específicos
type SmallUnion = `${'read' | 'write'}_${'file' | 'db'}`;
// Apenas 4 combinações

Quando não usar template literals

// ❌ Use enum ou union simples para valores fixos
type Status = "active" | "inactive" | "pending"; // ✅ Melhor que template literal

// ❌ Evite para validações complexas em runtime
// Template literals são para tipos, não para lógica de negócio complexa

Dicas para depuração

// Use // @ts-expect-error para testar tipos
type TestType = `${'a' | 'b'}_${1 | 2}`;
// @ts-expect-error: "a_3" não está no tipo
const test: TestType = "a_3";

// Use o playground do TypeScript para visualizar tipos expandidos
// https://www.typescriptlang.org/play

Template literal types são uma ferramenta extremamente poderosa para criar APIs tipadas com segurança, validar formatos de strings em tempo de compilação e reduzir erros em tempo de execução. Use-os com moderação e sempre considere o impacto na performance e legibilidade do código.

Referências