TypeScript

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

11 min de leitura

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

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.

<h2>O que são Template Literal Types?</h2>

<p>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 <code>${...}</code>) 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.</p>

<p>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 &quot;user_&quot;, 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.</p>

<h2>Sintaxe Básica e Conceitos Fundamentais</h2>

<h3>Declarando um Template Literal Type</h3>

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

<pre><code class="language-typescript">// Template literal simples com tipos

type Greeting = Hello, ${string};

const msg1: Greeting = &quot;Hello, World&quot;; // ✓ válido

const msg2: Greeting = &quot;Hello, TypeScript&quot;; // ✓ válido

const msg3: Greeting = &quot;Goodbye, World&quot;; // ✗ erro: não começa com &quot;Hello, &quot;</code></pre>

<p>Neste exemplo, <code>Greeting</code> define um padrão onde qualquer string que comece com &quot;Hello, &quot; é válida. O <code>${string}</code> funciona como um wildcard que aceita qualquer string como continuação.</p>

<h3>Combinando Tipos Literais</h3>

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

<pre><code class="language-typescript">type Color = &quot;red&quot; | &quot;blue&quot; | &quot;green&quot;; type Size = &quot;small&quot; | &quot;medium&quot; | &quot;large&quot;;

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

const className1: CSSClass = &quot;red-small&quot;; // ✓ válido

const className2: CSSClass = &quot;blue-large&quot;; // ✓ válido

const className3: CSSClass = &quot;red-invalid&quot;; // ✗ erro: &quot;invalid&quot; não é um Size válido

const className4: CSSClass = &quot;yellow-small&quot;; // ✗ erro: &quot;yellow&quot; não é um Color válido</code></pre>

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

<h2>Casos de Uso Práticos e Padrões Avançados</h2>

<h3>Event Handlers e Namespace</h3>

<p>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:</p>

<pre><code class="language-typescript">type EventType = &quot;user&quot; | &quot;product&quot; | &quot;order&quot;; type EventAction = &quot;created&quot; | &quot;updated&quot; | &quot;deleted&quot;;

type Event = on${Capitalize&lt;EventType&gt;}${Capitalize&lt;EventAction&gt;};

function addEventListener(event: Event, callback: (data: any) =&gt; void) {

// implementação

}

addEventListener(&quot;onUserCreated&quot;, (data) =&gt; console.log(data)); // ✓ válido

addEventListener(&quot;onProductUpdated&quot;, (data) =&gt; console.log(data)); // ✓ válido

addEventListener(&quot;onOrderDeleted&quot;, (data) =&gt; console.log(data)); // ✓ válido

addEventListener(&quot;onInvalidEvent&quot;, (data) =&gt; {}); // ✗ erro</code></pre>

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

<h3>Manipulação de Propriedades Dinâmicas</h3>

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

<pre><code class="language-typescript">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&lt;string &amp; K&gt;}]: () =&gt; DataModel[K];

};

// Resultado esperado: GetUser_id, GetUser_name, GetUser_email, etc.

const getters: Getters = {

getUser_id: () =&gt; 123,

getUser_name: () =&gt; &quot;João&quot;,

getUser_email: () =&gt; &quot;joao@example.com&quot;,

getProduct_id: () =&gt; 456,

getProduct_name: () =&gt; &quot;Notebook&quot;,

};

getters.getUser_id(); // ✓ válido

getters.getInvalidMethod(); // ✗ erro: método não existe</code></pre>

<h3>Validação de Caminhos e URLs</h3>

<p>Um outro caso prático é validar caminhos de API ou rotas:</p>

<pre><code class="language-typescript">type HttpMethod = &quot;GET&quot; | &quot;POST&quot; | &quot;PUT&quot; | &quot;DELETE&quot;; type ApiVersion = &quot;v1&quot; | &quot;v2&quot; | &quot;v3&quot;; type Resource = &quot;users&quot; | &quot;products&quot; | &quot;orders&quot;;

type ApiRoute = /${ApiVersion}/${Resource};

type ApiEndpoint = ${HttpMethod} ${ApiRoute};

const endpoint1: ApiEndpoint = &quot;GET /v1/users&quot;; // ✓ válido

const endpoint2: ApiEndpoint = &quot;POST /v2/products&quot;; // ✓ válido

const endpoint3: ApiEndpoint = &quot;DELETE /v3/orders&quot;; // ✓ válido

const endpoint4: ApiEndpoint = &quot;GET /v4/users&quot;; // ✗ erro: v4 não existe

const endpoint5: ApiEndpoint = &quot;PATCH /v1/users&quot;; // ✗ erro: PATCH não é HttpMethod</code></pre>

<h2>Inferência de Tipos com Template Literals</h2>

<h3>Extraindo Informações de Strings</h3>

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

<pre><code class="language-typescript">type ExtractVersion&lt;T extends /${string}/${string}&gt; =

T extends /${infer V}/${string} ? V : never;

type Route = &quot;/v1/users&quot;;

type Version = ExtractVersion&lt;Route&gt;; // ✓ tipo é &quot;v1&quot;

type AnotherRoute = &quot;/v2/products&quot;;

type AnotherVersion = ExtractVersion&lt;AnotherRoute&gt;; // ✓ tipo é &quot;v2&quot;</code></pre>

<p>Aqui, usamos <code>infer</code> 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.</p>

<h3>Transformações com Mapped Types</h3>

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

<pre><code class="language-typescript">type SnakeCase&lt;T extends string&gt; = T extends ${infer F}${infer R}

? F extends Uppercase&lt;F&gt;

? _${Lowercase&lt;F&gt;}${SnakeCase&lt;R&gt;}

: ${F}${SnakeCase&lt;R&gt;}

: T;

type CamelCaseToSnake = SnakeCase&lt;&quot;firstName&quot;&gt;; // &quot;first_name&quot;

type AnotherExample = SnakeCase&lt;&quot;getUserById&quot;&gt;; // &quot;get_user_by_id&quot;

// Aplicando a transformação em todas as chaves de um objeto

type SnakeCaseProperties&lt;T&gt; = {

[K in keyof T as SnakeCase&lt;string &amp; K&gt;]: T[K];

};

type User = {

firstName: string;

lastName: string;

emailAddress: string;

};

type UserSnakeCase = SnakeCaseProperties&lt;User&gt;;

// Resultado: { first_name: string; last_name: string; email_address: string; }</code></pre>

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

<h2>Limitações e Boas Práticas</h2>

<h3>O que Template Literal Types NÃO pode fazer</h3>

<p>É importante entender os limites. Template Literal Types não aceitam regex ou expressões arbitrárias. Você não pode escrever <code>type ValidEmail = /^[a-z]+@[a-z]+\.[a-z]+$/;</code>. 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.</p>

<pre><code class="language-typescript">// ✗ 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 = &quot;pending&quot; | &quot;approved&quot; | &quot;rejected&quot;;

type StatusMessage = Status: ${ValidStatus};</code></pre>

<h3>Recomendações Práticas</h3>

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

<pre><code class="language-typescript">// ✓ BOM - Claro e bem documentado

/**

  • Padrão: on${Entity}${Action}
  • Exemplos válidos: onUserCreated, onProductUpdated

*/

type DomainEvent = on${Capitalize&lt;&quot;user&quot; | &quot;product&quot;&gt;}${Capitalize&lt;&quot;created&quot; | &quot;updated&quot;&gt;};

// ✗ EVITAR - Muito complexo e sem documentação

type WeirdPattern = ${string}${&quot;_&quot; | &quot;-&quot;}${number}${&quot;.&quot; | &quot;,&quot;}${boolean};</code></pre>

<h2>Conclusão</h2>

<p>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.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-4.html#template-literal-types" target="_blank" rel="noopener noreferrer">TypeScript Handbook - Template Literal Types</a></li>

<li><a href="https://www.typescriptlang.org/docs/handbook/2/conditional-types.html" target="_blank" rel="noopener noreferrer">TypeScript Handbook - Conditional Types</a></li>

<li><a href="https://basarat.gitbook.io/typescript/type-system/literals" target="_blank" rel="noopener noreferrer">TypeScript Deep Dive - Strings</a></li>

<li><a href="https://blog.logrocket.com/" target="_blank" rel="noopener noreferrer">Template Literal Types: A Comprehensive Guide - LogRocket Blog</a></li>

<li><a href="https://github.com/microsoft/TypeScript" target="_blank" rel="noopener noreferrer">TypeScript Official Repository - Advanced Types Discussion</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em TypeScript

O que Todo Dev Deve Saber sobre Node.js com TypeScript: Configuração, tsx e ts-node na Prática
O que Todo Dev Deve Saber sobre Node.js com TypeScript: Configuração, tsx e ts-node na Prática

Node.js com TypeScript: Configuração, tsx e ts-node na Prática TypeScript é u...

Componentes Genéricos em React com TypeScript: Do Básico ao Avançado
Componentes Genéricos em React com TypeScript: Do Básico ao Avançado

O que são Componentes Genéricos em React com TypeScript? Componentes genérico...

Como Usar Keyof, Typeof e Indexed Access Types em TypeScript em Produção
Como Usar Keyof, Typeof e Indexed Access Types em TypeScript em Produção

Entendendo Keyof em TypeScript O operador é um dos recursos mais poderosos do...