Intrinsic Types: Manipulação de Strings em Nível de Tipo

1. Introdução aos Intrinsic String Types

O TypeScript 4.1 introduziu quatro tipos intrínsecos (intrinsic types) para manipulação de strings em nível de tipo: Uppercase<T>, Lowercase<T>, Capitalize<T> e Uncapitalize<T>. Esses tipos operam exclusivamente em tipos literais de string durante a compilação, permitindo transformações tipográficas sem custo em runtime.

Diferentemente da manipulação tradicional de strings em JavaScript ("hello".toUpperCase()), que ocorre em tempo de execução, os intrinsic types são avaliados pelo compilador TypeScript. Isso significa que você pode validar, restringir e transformar padrões de strings antes mesmo do código ser executado.

Casos de uso principais:
- Normalização de nomes de propriedades em APIs externas
- Formatação consistente de constantes e enums
- Mapeamento automático de chaves entre diferentes convenções (snake_case, camelCase, PascalCase)

2. Uppercase<T> e Lowercase<T>: Transformação de Caixa

Os tipos Uppercase<T> e Lowercase<T> convertem todos os caracteres de um literal string para maiúsculas ou minúsculas, respectivamente.

type Mensagem = "Olá Mundo";
type MensagemAlta = Uppercase<Mensagem>; // "OLÁ MUNDO"
type MensagemBaixa = Lowercase<Mensagem>; // "olá mundo"

// Funciona com union types
type MetodoHTTP = "get" | "post" | "put" | "delete";
type MetodoMaiusculo = Uppercase<MetodoHTTP>; // "GET" | "POST" | "PUT" | "DELETE"

Aplicação prática: gerar variantes de chaves para objetos de configuração:

type ConfigKey = "host" | "port" | "ssl";
type ConfigKeyUpper = Uppercase<ConfigKey>; // "HOST" | "PORT" | "SSL"

type Config = Record<ConfigKey, string>;
type ConfigUpper = Record<ConfigKeyUpper, string>;
// { HOST: string; PORT: string; SSL: string }

Limitação importante: esses tipos não funcionam com o tipo genérico string:

function processar(valor: string): Uppercase<typeof valor> {
  // ❌ Erro: Type 'string' não satisfaz a constraint de tipo literal
  return valor.toUpperCase() as Uppercase<typeof valor>;
}

3. Capitalize<T> e Uncapitalize<T>: Controle de Primeira Letra

Capitalize<T> converte apenas o primeiro caractere para maiúsculo, enquanto Uncapitalize<T> faz o oposto:

type Nome = "joão";
type NomeCapitalizado = Capitalize<Nome>; // "João"
type NomeDescaptalizado = Uncapitalize<NomeCapitalizado>; // "joão"

Aplicação prática: converter nomes de propriedades snake_case para camelCase:

type SnakeToCamel<S extends string> = 
  S extends `${infer Primeira}_${infer Resto}`
    ? `${Primeira}${Capitalize<SnakeToCamel<Resto>>}`
    : S;

type NomeCompleto = "nome_completo_usuario";
type CamelCase = SnakeToCamel<NomeCompleto>; // "nomeCompletoUsuario"

4. Combinação com Template Literal Types

A verdadeira potência dos intrinsic types aparece quando combinados com template literal types. Vamos criar um sistema que transforma prefixos em métodos getters:

type GetterMethod<T extends string> = `get${Capitalize<T>}`;

type Propriedade = "nome";
type Getter = GetterMethod<Propriedade>; // "getNome"

// Inferindo tipo de retorno automaticamente
function criarGetter<T extends string>(prop: T): GetterMethod<T> {
  return `get${prop.charAt(0).toUpperCase() + prop.slice(1)}` as GetterMethod<T>;
}

const getNome = criarGetter("nome"); // tipo: "getNome"

Transformação completa snake_case para camelCase:

type SnakeToCamel<T extends string> = 
  T extends `${infer Primeira}_${infer Resto}`
    ? `${Primeira}${Capitalize<SnakeToCamel<Resto>>}`
    : T;

type Entrada = "usuario_id_data_criacao";
type Saida = SnakeToCamel<Entrada>; // "usuarioIdDataCriacao"

5. Validação e Restrição com Intrinsic Types

Podemos usar intrinsic types para garantir que strings sigam padrões específicos em nível de tipo:

type IDMaiusculo = Uppercase<string>; // Isso não funciona (string não é literal)

// Solução: constraint com genérico
function criarID<T extends string>(id: T): Uppercase<T> {
  return id.toUpperCase() as Uppercase<T>;
}

const id = criarID("abc123"); // tipo: "ABC123"
// id = "abc123"; ❌ Erro de tipo

Sistema de rotas com prefixos obrigatórios:

type RotaAPI<T extends string> = T extends `/api/${infer Recurso}`
  ? T
  : never;

type RotaValida = RotaAPI<"/api/usuarios">; // "/api/usuarios"
type RotaInvalida = RotaAPI<"/usuarios">; // never

// Garantindo que métodos sejam maiúsculos
type MetodoHTTPValido = Uppercase<"get" | "post" | "put" | "delete">;
// Equivalente a: "GET" | "POST" | "PUT" | "DELETE"

function request<T extends MetodoHTTPValido>(metodo: T, url: string) {
  // Implementação
}

request("GET", "/api"); // ✅ OK
request("get", "/api"); // ❌ Erro: "get" não é "GET"

6. Mapeamento de Objetos com Chaves Transformadas

Podemos criar tipos que transformam automaticamente todas as chaves de um objeto:

type CapitalizeKeys<T> = {
  [K in keyof T as Capitalize<K & string>]: T[K]
};

type ConfigOriginal = {
  host: string;
  port: number;
  ssl: boolean;
};

type ConfigCapitalizada = CapitalizeKeys<ConfigOriginal>;
// { Host: string; Port: number; Ssl: boolean }

// Exemplo prático: converter enum para objeto com chaves capitalizadas
enum Status {
  pending = "pending",
  active = "active",
  inactive = "inactive"
}

type StatusCapitalizado = {
  [K in keyof typeof Status as Capitalize<K & string>]: typeof Status[K]
};
// { Pending: Status.pending; Active: Status.active; Inactive: Status.inactive }

Mapeamento bidirecional com template literals:

type ToUpperCaseKeys<T> = {
  [K in keyof T as Uppercase<K & string>]: T[K]
};

type ToLowerCaseKeys<T> = {
  [K in keyof T as Lowercase<K & string>]: T[K]
};

type Dados = { Nome: string; Idade: number };
type Normalizado = ToLowerCaseKeys<Dados>;
// { nome: string; idade: number }

7. Casos Avançados e Limitações

Caracteres especiais e acentos:

type Acentuado = "coração";
type Maiusculo = Uppercase<Acentuado>; // "CORAÇÃO" ✅ (funciona com acentos)

type Especial = "olá mundo!";
type Capitalizado = Capitalize<Especial>; // "Olá mundo!" ✅

Limitação fundamental: intrinsic types só funcionam com tipos literais de string, não com string genérico:

function transformar<T extends string>(input: T): Capitalize<T> {
  return input.charAt(0).toUpperCase() + input.slice(1) as Capitalize<T>;
}

// Funciona
const resultado = transformar("hello"); // tipo: "Hello"

// Não funciona com string ampla
function processar(input: string) {
  // return Capitalize<typeof input>; // ❌ input é string, não literal
}

Alternativa para casos não suportados: use condicionais com inferência manual:

type CapitalizeString<T> = T extends string
  ? T extends `${infer Primeira}${infer Resto}`
    ? `${Uppercase<Primeira>}${Resto}`
    : T
  : never;

type Teste = CapitalizeString<"teste">; // "Teste"
type Teste2 = CapitalizeString<string>; // string (não quebra)

Conclusão

Os intrinsic types de string no TypeScript oferecem uma maneira poderosa e segura de manipular strings em nível de tipo. Eles são essenciais para criar APIs tipadas, normalizar dados e garantir consistência em todo o código. Apesar das limitações com tipos string genéricos, quando combinados com template literal types e genéricos, permitem transformações complexas que seriam impossíveis sem eles.

Referências