Desafios Avançados em TypeScript para Testar seus Conhecimentos
Quatro exercícios técnicos que exploram padrões de tipos, generics e validação estática. Ideal para quem quer ir além do básico e internalizar as ferramentas poderosas do TypeScript.
1. Inferência de Tipos, Tipos Condicionais e Utilitários Avançados
A primeira área de desafio foca em como o TypeScript consegue inferir tipos de forma inteligente, combinando com tipos condicionais e utilitários nativos. Entender essas peças permite construir tipos que extraem informações de estruturas complexas sem recorrer a qualquer valor “hard-coded”.
// ElementType extrai o tipo do elemento de um array
type ElementType = T extends (infer E)[] ? E : T;
// Promise: extrai o tipo resolvido
type PromiseResolution = T extends Promise<infer R> ? R : T;
// DeepReadonly: imutabilidade profunda via mapped types
type DeepReadonly = T extends object ? { readonly [K in keyof T]: DeepReadonly } : T;
// Exemplo de uso
type A = ElementType<string[]>; // string
type B = ElementType<number[]>; // number
type C = PromiseResolution<Promise<Date>>; // Date
interface User { id: string; profile: { name: string; age: number } }
type D = DeepReadonly<User>; // readonly profile, etc.
2. Generics Avançados: Constraints, Inferência e Padrões de API
Generics são a espinha dorsal de APIs fortes em TypeScript. Neste desafio, abordamos constraints para impor contratos, inferência de tipos de retorno e padrões que ajudam a compor APIs reutilizáveis sem perder a segurança de tipos.
// Constraint simples: obj deve ter uma propriedade id do tipo string
function ident(obj: T): T {
return obj;
}
// Inferência de retorno de função com types utilitários nativos
type Fn = (a: number, b: string) => boolean;
type R = ReturnType<Fn>; // boolean
// Distributive conditional types para transformar uniões em listas
type ToArray = T extends any ? T[] : never;
type Example = ToArray<string | number>; // string[] | number[]
Dicas: combine constraints com parâmetros de tipo para criar funções mais seguras e sem perder flexibilidade. Em APIs, use generics para manter a tipagem estável quando a forma de seus dados muda.
3. Mapeamento de Tipos, Utilitários e Template Literal Types
Mapeamento de tipos (mapped types) e template literal types permitem transformar estruturas de forma declarativa, mantendo verificação de tipos em todo o fluxo. Vamos a um conjunto de exemplos práticos que você pode adaptar para bibliotecas e aplicações reais.
type APIUser = { id: string; name: string; email?: string; role: 'admin'|'user' };
// Partial/Omit combinados para planejar updates de API
type UserUpdate = Partial<Omit<APIUser, 'id'>>; // { name?: string; email?: string; role?: 'admin'|'user' }
// Template Literal Types para formatos de string
type Email = `${S}@example.com`; // ex.: "john@example.com"
// Branding de tipos para evitar colisões de identidade entre categorias
type Brand = K & { __brand: T };
type UserId = Brand<string, 'UserId'>;
// Uso típico
type EmailForJohn = Email<'john'>; // "john@example.com"
4. Verificação Estática de Tipos e Padrões de Design
Encerramos com padrões de verificação estática que ajudam a capturar inconsistências ainda em tempo de compilação, reduzindo bugs em produção. Incluo um exemplo prático de asserts para estreitar tipos no fluxo de código e um pattern de validação por template literal types.
// asserts: estreita tipos no fluxo de execução
function assertIsString(x: unknown): asserts x is string {
if (typeof x !== 'string') throw new TypeError('Expected string');
}
function greet(name: string | unknown) {
assertIsString(name);
console.log(`Olá, ${name.toUpperCase()}`);
}
// Template literal type para checagem de formato de email simplificado
type EmailPattern = `${string}@${string}.${string}`;
type IsEmail = S extends EmailPattern ? true : false;
type E1 = IsEmail<'alice@example.com'>; // true
type E2 = IsEmail<'alice'>; // false