Skip to content

Tipos de template literal

Um template literal em JavaScript monta uma string interpolando valores entre crases: `Olá, ${nome}`. O TypeScript leva a mesma sintaxe para o nível de tipos. Um tipo de template literal descreve o formato de uma string, com partes fixas e partes variáveis, e só aceita strings que se encaixam nesse formato:

type Saudacao = `Olá, ${string}`;
const a: Saudacao = "Olá, mundo"; // ok, casa com o formato
const b: Saudacao = "Oi"; // erro: não começa com "Olá, "

O ${string} é um buraco que qualquer string preenche, então Saudacao é o tipo de todas as strings que começam com "Olá, ". Você pode interpolar outros tipos primitivos também, como ${number} para o trecho numérico de uma string, e é isso que transforma strings, antes tipos opacos, em algo que o compilador consegue verificar por forma.

Interpolar uniões expande em combinações

Section titled “Interpolar uniões expande em combinações”

O comportamento que dá poder de verdade aos template literals aparece quando você interpola uma união. Em vez de um buraco livre, cada união vira um conjunto de opções, e o tipo resultante é o produto de todas as combinações possíveis:

type Vertical = "top" | "bottom";
type Horizontal = "left" | "right";
type Posicao = `${Vertical}-${Horizontal}`;
// ^? "top-left" | "top-right" | "bottom-left" | "bottom-right"

Duas uniões de dois membros geraram uma união de quatro strings, todas verificadas. Isso é útil sempre que existe um vocabulário fechado de strings que segue um padrão: variantes de um componente, nomes de variáveis CSS, chaves de tradução. Em vez de listar as quatro à mão e arriscar esquecer uma, você descreve a regra e deixa o compilador enumerar.

O TypeScript embute quatro tipos que transformam o caixa de uma string no nível de tipo, e que combinam com template literals: Uppercase, Lowercase, Capitalize e Uncapitalize. Eles fazem o esperado:

type Grito = Uppercase<"socorro">;
// ^? "SOCORRO"
type Titulo = Capitalize<"nome do campo">;
// ^? "Nome do campo"

São chamados de intrínsecos porque não são escritos em TypeScript; o compilador os implementa por dentro. O uso mais comum deles é ajustar uma chave ao montar um nome derivado, como o get${Capitalize<...>} que apareceu nos tipos mapeados.

Assim como o infer extraía o elemento de um array ou a cauda de uma tupla, ele extrai pedaços de uma string quando usado dentro de um padrão de template literal. Você descreve o formato, marca com infer a parte que quer capturar, e o compilador a devolve:

// Captura tudo que vem depois de "user_".
type Sufixo<T extends string> = T extends `user_${infer Id}` ? Id : never;
type Id = Sufixo<"user_42">;
// ^? "42"

Combine isso com o que os capítulos anteriores mostraram e os usos práticos aparecem. Tipar nomes de eventos derivados de um estado, por exemplo, junta template literal, keyof e interpolação de união num único tipo:

type Estado = { nome: string; idade: number };
// Um evento "mudou" para cada campo do estado.
type EventosDeMudanca = `${string & keyof Estado}Changed`;
// ^? "nomeChanged" | "idadeChanged"

Esse padrão é a espinha do event emitter tipado que o último capítulo monta: os nomes de evento válidos deixam de ser strings quaisquer e passam a ser derivados, um a um, das chaves do estado, de modo que ouvir um evento que não existe vira erro de compilação.

O exemplo que junta tudo: parâmetros de rota

Section titled “O exemplo que junta tudo: parâmetros de rota”

O ponto alto dos template literals é extrair informação estruturada de uma string, e o caso didático perfeito é pegar os parâmetros de um padrão de rota, aquelas partes marcadas com dois-pontos. Ele combina três coisas que o curso já cobriu: casamento de template com infer, um condicional para ramificar, e recursão para varrer a string até o fim.

type ParamsDaRota<T extends string> =
// Caso 1: há um ":param" seguido de mais caminho. Captura o param e recorre no resto.
T extends `${string}:${infer Param}/${infer Resto}`
? Param | ParamsDaRota<Resto>
// Caso 2: há um ":param" no fim, sem mais caminho. Captura e para.
: T extends `${string}:${infer Param}`
? Param
// Caso 3: não há mais nenhum ":param". Fim da recursão.
: never;
type Params = ParamsDaRota<"/users/:userId/posts/:postId">;
// ^? "userId" | "postId"

Vale seguir a recursão uma volta. Na primeira passada, o caso 1 casa: ${string} engole "/users/", o : marca o início do parâmetro, Param captura "userId" até a barra seguinte, e Resto fica com "posts/:postId". O resultado é "userId" unido ao que a chamada recursiva sobre "posts/:postId" devolver. Nessa segunda passada não há barra depois do parâmetro, então o caso 1 falha e o caso 2 assume, capturando "postId" e encerrando. As duas capturas se somam em "userId" | "postId".

Um tipo desses deixa de ser curiosidade quando você o pluga num roteador: a função que registra "/users/:userId" pode exigir um handler cujo objeto de parâmetros tenha exatamente a chave userId, tipada, e um typo em userIdd vira erro no editor. É o mesmo princípio dos capítulos anteriores levado às strings: transformar uma convenção que se verificava com testes (ou não se verificava) numa garantia que o compilador dá de graça.

Com condicionais, tipos mapeados e agora template literals, você tem as três primitivas de transformação do sistema de tipos. O próximo capítulo cobra o débito que os últimos três acumularam: usar essas primitivas para reconstruir, do zero, os utility types que a biblioteca padrão oferece pronta.

Conceitos: tipo de template literal, expansão combinatória de união, tipo intrínseco de string, extração por infer Intrínsecos: Uppercase, Lowercase, Capitalize, Uncapitalize Exemplos: Saudacao, Posicao, Sufixo, EventosDeMudanca, ParamsDaRota