O que Todo Dev Deve Saber sobre Template Literal Types em TypeScript: Tipos a partir de Strings Já leu

O que são Template Literal Types? Template Literal Types é um recurso avançado do TypeScript que permite criar tipos a partir de padrões de strings. Diferente de strings simples, eles usam a sintaxe de template literals (crase e ) para gerar tipos dinamicamente, combinando outros tipos e valores literais. Esse recurso foi introduzido no TypeScript 4.4 e revolucionou a forma como podemos trabalhar com tipos baseados em strings, permitindo validações e inferências muito mais sofisticadas. A ideia central é que você pode expressar restrições de tipo usando patterns de strings. Por exemplo, você pode definir que uma variável deve ser uma string que sempre comece com "user", ou que combine dois tipos em um padrão específico. Isso torna o código mais seguro, pois erros de digitação ou formato são detectados em tempo de compilação, não em runtime. Sintaxe Básica e Conceitos Fundamentais Declarando um Template Literal Type A sintaxe é similar à interpolação de strings JavaScript, mas aplicada a tipos.

O que são Template Literal Types?

Template Literal Types é um recurso avançado do TypeScript que permite criar tipos a partir de padrões de strings. Diferente de strings simples, eles usam a sintaxe de template literals (crase e ${...}) para gerar tipos dinamicamente, combinando outros tipos e valores literais. Esse recurso foi introduzido no TypeScript 4.4 e revolucionou a forma como podemos trabalhar com tipos baseados em strings, permitindo validações e inferências muito mais sofisticadas.

A ideia central é que você pode expressar restrições de tipo usando patterns de strings. Por exemplo, você pode definir que uma variável deve ser uma string que sempre comece com "user_", ou que combine dois tipos em um padrão específico. Isso torna o código mais seguro, pois erros de digitação ou formato são detectados em tempo de compilação, não em runtime.

Sintaxe Básica e Conceitos Fundamentais

Declarando um Template Literal Type

A sintaxe é similar à interpolação de strings JavaScript, mas aplicada a tipos. Você usa backticks e ${TipoOuValor} para compor o padrão:

// Template literal simples com tipos
type Greeting = `Hello, ${string}`;

const msg1: Greeting = "Hello, World";      // ✓ válido
const msg2: Greeting = "Hello, TypeScript"; // ✓ válido
const msg3: Greeting = "Goodbye, World";    // ✗ erro: não começa com "Hello, "

Neste exemplo, Greeting define um padrão onde qualquer string que comece com "Hello, " é válida. O ${string} funciona como um wildcard que aceita qualquer string como continuação.

Combinando Tipos Literais

Você pode combinar union types e tipos literais para criar padrões mais específicos:

type Color = "red" | "blue" | "green";
type Size = "small" | "medium" | "large";

type CSSClass = `${Color}-${Size}`;

const className1: CSSClass = "red-small";      // ✓ válido
const className2: CSSClass = "blue-large";     // ✓ válido
const className3: CSSClass = "red-invalid";    // ✗ erro: "invalid" não é um Size válido
const className4: CSSClass = "yellow-small";   // ✗ erro: "yellow" não é um Color válido

Aqui vemos o poder real: o TypeScript expande automaticamente todas as combinações possíveis. O tipo CSSClass aceita apenas 9 combinações (3 cores × 3 tamanhos), e qualquer outra string é rejeitada em tempo de compilação.

Casos de Uso Práticos e Padrões Avançados

Event Handlers e Namespace

Um padrão muito comum é usar Template Literal Types para eventos ou funcionalidades com namespace. Considere um sistema de eventos onde cada listener segue um padrão:

type EventType = "user" | "product" | "order";
type EventAction = "created" | "updated" | "deleted";

type Event = `on${Capitalize<EventType>}${Capitalize<EventAction>}`;

function addEventListener(event: Event, callback: (data: any) => void) {
  // implementação
}

addEventListener("onUserCreated", (data) => console.log(data));     // ✓ válido
addEventListener("onProductUpdated", (data) => console.log(data)); // ✓ válido
addEventListener("onOrderDeleted", (data) => console.log(data));   // ✓ válido
addEventListener("onInvalidEvent", (data) => {});                  // ✗ erro

Aqui utilizamos Capitalize (uma utility type built-in do TypeScript) para transformar os tipos. Isso garante que apenas eventos válidos sejam passados à função.

Manipulação de Propriedades Dinâmicas

Template Literal Types são especialmente úteis para criar getters e setters tipados dinamicamente:

type DataModel = {
  user_id: number;
  user_name: string;
  user_email: string;
  product_id: number;
  product_name: string;
};

type Getters = {
  [K in keyof DataModel as `get${Capitalize<string & K>}`]: () => DataModel[K];
};

// Resultado esperado: GetUser_id, GetUser_name, GetUser_email, etc.
const getters: Getters = {
  getUser_id: () => 123,
  getUser_name: () => "João",
  getUser_email: () => "joao@example.com",
  getProduct_id: () => 456,
  getProduct_name: () => "Notebook",
};

getters.getUser_id();       // ✓ válido
getters.getInvalidMethod(); // ✗ erro: método não existe

Validação de Caminhos e URLs

Um outro caso prático é validar caminhos de API ou rotas:

type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type ApiVersion = "v1" | "v2" | "v3";
type Resource = "users" | "products" | "orders";

type ApiRoute = `/${ApiVersion}/${Resource}`;
type ApiEndpoint = `${HttpMethod} ${ApiRoute}`;

const endpoint1: ApiEndpoint = "GET /v1/users";      // ✓ válido
const endpoint2: ApiEndpoint = "POST /v2/products";  // ✓ válido
const endpoint3: ApiEndpoint = "DELETE /v3/orders";  // ✓ válido
const endpoint4: ApiEndpoint = "GET /v4/users";      // ✗ erro: v4 não existe
const endpoint5: ApiEndpoint = "PATCH /v1/users";    // ✗ erro: PATCH não é HttpMethod

Inferência de Tipos com Template Literals

Extraindo Informações de Strings

O TypeScript pode inferir informações estruturadas a partir de padrões de template literals usando tipos condicionais e a palavra-chave infer:

type ExtractVersion<T extends `/${string}/${string}`> = 
  T extends `/${infer V}/${string}` ? V : never;

type Route = "/v1/users";
type Version = ExtractVersion<Route>; // ✓ tipo é "v1"

type AnotherRoute = "/v2/products";
type AnotherVersion = ExtractVersion<AnotherRoute>; // ✓ tipo é "v2"

Aqui, usamos infer para extrair a versão de uma rota. O padrão funciona como uma desestruturação de tipo, capturando parte da string em uma variável de tipo.

Transformações com Mapped Types

Você pode combinar Template Literal Types com mapped types para criar transformações complexas:

type SnakeCase<T extends string> = T extends `${infer F}${infer R}`
  ? F extends Uppercase<F>
    ? `_${Lowercase<F>}${SnakeCase<R>}`
    : `${F}${SnakeCase<R>}`
  : T;

type CamelCaseToSnake = SnakeCase<"firstName">; // "first_name"
type AnotherExample = SnakeCase<"getUserById">;  // "get_user_by_id"

// Aplicando a transformação em todas as chaves de um objeto
type SnakeCaseProperties<T> = {
  [K in keyof T as SnakeCase<string & K>]: T[K];
};

type User = {
  firstName: string;
  lastName: string;
  emailAddress: string;
};

type UserSnakeCase = SnakeCaseProperties<User>;
// Resultado: { first_name: string; last_name: string; email_address: string; }

Esta é uma transformação poderosa que converte camelCase em snake_case recursivamente, muito útil ao trabalhar com APIs que usam convenções diferentes.

Limitações e Boas Práticas

O que Template Literal Types NÃO pode fazer

É importante entender os limites. Template Literal Types não aceitam regex ou expressões arbitrárias. Você não pode escrever type ValidEmail = /^[a-z]+@[a-z]+\.[a-z]+$/;. O padrão é sempre literal ou composto por tipos e valores explícitos. Além disso, quando você cria muitas combinações (por exemplo, 10 × 10 × 10), o TypeScript pode enfrentar problemas de performance.

// ✗ NÃO FUNCIONA - regex não é suportado
type EmailPattern = /^[\w.-]+@[\w.-]+\.\w+$/;

// ✗ NÃO FUNCIONA - Truthy é muito vago
type AnyString = `${boolean}`;

// ✓ FUNCIONA - Valores literais específicos
type ValidStatus = "pending" | "approved" | "rejected";
type StatusMessage = `Status: ${ValidStatus}`;

Recomendações Práticas

Use Template Literal Types quando você realmente precisar de validação de padrão em tempo de compilação. Para simples strings, string é suficiente. Documente seus padrões bem: quando alguém vê type Event = \on${Capitalize}${Capitalize}``, pode ser confuso sem contexto. Se o padrão ficar muito complexo, considere refatorar ou adicionar comentários explicativos.

// ✓ BOM - Claro e bem documentado
/**
 * Padrão: `on${Entity}${Action}`
 * Exemplos válidos: onUserCreated, onProductUpdated
 */
type DomainEvent = `on${Capitalize<"user" | "product">}${Capitalize<"created" | "updated">}`;

// ✗ EVITAR - Muito complexo e sem documentação
type WeirdPattern = `${string}${"_" | "-"}${number}${"." | ","}${boolean}`;

Conclusão

Template Literal Types representam um salto qualitativo na segurança de tipos em TypeScript. Primeiro, eles permitem expressar restrições de formato de string que antes só eram possíveis em runtime, movendo validações para tempo de compilação. Segundo, combinados com mapped types e tipos condicionais, eles oferecem capacidades de transformação de tipos muito poderosas, desde conversão de casos até extração de partes de padrões. Por fim, quando usados apropriadamente, reduzem bugs relacionados a strings, melhoram a autocompletar do IDE e documentam implicitamente o contrato esperado do código.

Referências


Artigos relacionados