TypeScript: Patterns Avançados para Aplicações Robustas
O TypeScript vai muito além de adicionar tipos ao JavaScript. Quando dominamos seus recursos avançados, conseguimos criar código mais seguro, expressivo e fácil de manter. Neste guia, vamos explorar patterns avançados que vão elevar seu código TypeScript ao próximo nível.
Por que Patterns Avançados?
Patterns avançados de TypeScript nos ajudam a:
| Benefício | Descrição |
|---|---|
| Type Safety | Detectar erros em tempo de compilação |
| Autocompletion | IDE sugere opções corretas |
| Refactoring | Mudanças propagam automaticamente |
| Documentação | Tipos servem como documentação viva |
| Manutenibilidade | Código mais fácil de entender |
"TypeScript é JavaScript que escala. Os patterns avançados são o que fazem essa escalabilidade possível." - Anders Hejlsberg
Utility Types Avançados
Criando Utility Types Customizados
// DeepPartial - torna todas as propriedades opcionais recursivamente
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
// Exemplo de uso
interface Config {
api: {
baseUrl: string;
timeout: number;
headers: {
authorization: string;
contentType: string;
};
};
features: {
darkMode: boolean;
analytics: boolean;
};
}
// Agora podemos fazer updates parciais profundos
function updateConfig(config: Config, updates: DeepPartial<Config>): Config {
return deepMerge(config, updates);
}
updateConfig(defaultConfig, {
api: {
timeout: 5000, // Só atualiza timeout
},
});
DeepReadonly
// DeepReadonly - imutabilidade profunda
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? T[P] extends Function
? T[P]
: DeepReadonly<T[P]>
: T[P];
};
// Uso com estado imutável
const state: DeepReadonly<AppState> = {
user: {
name: 'John',
preferences: {
theme: 'dark',
},
},
};
// ❌ Erro de compilação!
state.user.preferences.theme = 'light';
NonNullableDeep
// Remove null e undefined recursivamente
type NonNullableDeep<T> = {
[P in keyof T]-?: NonNullableDeep<NonNullable<T[P]>>;
};
interface ApiResponse {
data?: {
user?: {
name?: string;
email?: string;
} | null;
} | null;
}
// Após validação, garantimos que tudo existe
type ValidatedResponse = NonNullableDeep<ApiResponse>;
// { data: { user: { name: string; email: string } } }
Template Literal Types
O TypeScript 4.1+ introduziu Template Literal Types, permitindo manipulação de strings em nível de tipo:
Event Handlers Tipados
// Gerar tipos de eventos automaticamente
type EventName = 'click' | 'focus' | 'blur' | 'change';
type EventHandler = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur" | "onChange"
// Usando para criar props de componentes
type EventHandlers = {
[K in EventHandler]?: (event: Event) => void;
};
interface ButtonProps extends EventHandlers {
label: string;
variant: 'primary' | 'secondary';
}
API Routes Tipadas
// Definir rotas de API com parâmetros tipados
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type ApiRoute<
Method extends HttpMethod,
Path extends string
> = `${Method} ${Path}`;
// Rotas específicas
type UserRoutes =
| ApiRoute<'GET', '/users'>
| ApiRoute<'GET', '/users/:id'>
| ApiRoute<'POST', '/users'>
| ApiRoute<'PUT', '/users/:id'>
| ApiRoute<'DELETE', '/users/:id'>;
// Extrair parâmetros da rota
type ExtractParams<T extends string> =
T extends `${infer _Start}:${infer Param}/${infer Rest}`
? Param | ExtractParams<Rest>
: T extends `${infer _Start}:${infer Param}`
? Param
: never;
type UserIdParam = ExtractParams<'/users/:id/posts/:postId'>;
// "id" | "postId"
Conditional Types Avançados
Type Inference com infer
// Extrair tipo de retorno de função async
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
async function fetchUser(): Promise<{ id: number; name: string }> {
return { id: 1, name: 'John' };
}
type User = UnwrapPromise<ReturnType<typeof fetchUser>>;
// { id: number; name: string }
// Extrair tipo de elementos de array
type ArrayElement<T> = T extends (infer U)[] ? U : never;
type Item = ArrayElement<string[]>; // string
Distributive Conditional Types
// Filtrar tipos de uma union
type FilterByType<T, U> = T extends U ? T : never;
type Mixed = string | number | boolean | null | undefined;
type OnlyStringsAndNumbers = FilterByType<Mixed, string | number>;
// string | number
// Excluir tipos de uma union (similar ao Exclude built-in)
type ExcludeType<T, U> = T extends U ? never : T;
type NoNullOrUndefined = ExcludeType<Mixed, null | undefined>;
// string | number | boolean
Mapped Types com Modificadores
Criando Builders Tipados
// Builder pattern com tipos
type Builder<T> = {
[K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => Builder<T>;
} & {
build: () => T;
};
interface UserConfig {
name: string;
email: string;
age: number;
}
// O tipo Builder<UserConfig> terá:
// setName(value: string): Builder<UserConfig>
// setEmail(value: string): Builder<UserConfig>
// setAge(value: number): Builder<UserConfig>
// build(): UserConfig
function createBuilder<T>(): Builder<T> {
const data: Partial<T> = {};
const builder = new Proxy({} as Builder<T>, {
get(_, prop: string) {
if (prop === 'build') {
return () => data as T;
}
if (prop.startsWith('set')) {
const key = prop.slice(3).toLowerCase() as keyof T;
return (value: T[keyof T]) => {
data[key] = value;
return builder;
};
}
},
});
return builder;
}
// Uso
const user = createBuilder<UserConfig>()
.setName('John')
.setEmail('john@example.com')
.setAge(30)
.build();
Getters e Setters Automáticos
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type Setters<T> = {
[K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};
type Accessors<T> = Getters<T> & Setters<T>;
interface State {
count: number;
name: string;
}
type StateAccessors = Accessors<State>;
// {
// getCount: () => number;
// setCount: (value: number) => void;
// getName: () => string;
// setName: (value: string) => void;
// }
Branded Types
Branded types adicionam segurança semântica ao código:
// Definir branded types
declare const brand: unique symbol;
type Brand<T, B> = T & { [brand]: B };
// Criar tipos específicos
type UserId = Brand<string, 'UserId'>;
type PostId = Brand<string, 'PostId'>;
type Email = Brand<string, 'Email'>;
// Funções para criar instâncias validadas
function createUserId(id: string): UserId {
if (!id.startsWith('user_')) {
throw new Error('Invalid user ID format');
}
return id as UserId;
}
function createEmail(email: string): Email {
if (!email.includes('@')) {
throw new Error('Invalid email format');
}
return email as Email;
}
// Agora o TypeScript previne erros
function getUser(id: UserId): Promise<User> {
return fetch(`/api/users/${id}`).then(r => r.json());
}
function getPost(id: PostId): Promise<Post> {
return fetch(`/api/posts/${id}`).then(r => r.json());
}
const userId = createUserId('user_123');
const postId = 'post_456' as PostId;
getUser(userId); // ✅ OK
getUser(postId); // ❌ Erro! PostId não é UserId
Discriminated Unions
Pattern essencial para state machines e handling de diferentes casos:
// Estado de requisição assíncrona
type AsyncState<T, E = Error> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: E };
// Uso
function useAsync<T>(): AsyncState<T> {
// implementação...
}
function renderUser(state: AsyncState<User>) {
switch (state.status) {
case 'idle':
return <p>Click to load</p>;
case 'loading':
return <Spinner />;
case 'success':
// TypeScript sabe que state.data existe aqui!
return <UserCard user={state.data} />;
case 'error':
// TypeScript sabe que state.error existe aqui!
return <ErrorMessage error={state.error} />;
}
}
Exhaustive Check
// Garantir que todos os casos são tratados
function assertNever(x: never): never {
throw new Error(`Unexpected value: ${x}`);
}
type Action =
| { type: 'INCREMENT' }
| { type: 'DECREMENT' }
| { type: 'RESET'; payload: number };
function reducer(state: number, action: Action): number {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
case 'RESET':
return action.payload;
default:
// Se adicionar novo tipo de action e esquecer de tratar,
// TypeScript vai reclamar aqui!
return assertNever(action);
}
}
Function Overloading
// Overloads para diferentes tipos de input/output
function parse(input: string): object;
function parse(input: string, reviver: (key: string, value: unknown) => unknown): object;
function parse(input: Buffer): object;
function parse(
input: string | Buffer,
reviver?: (key: string, value: unknown) => unknown
): object {
const str = typeof input === 'string' ? input : input.toString();
return JSON.parse(str, reviver);
}
// TypeScript infere o tipo correto baseado no input
const obj1 = parse('{"name": "John"}'); // objeto
const obj2 = parse(Buffer.from('{"name": "John"}')); // objeto
Checklist de Boas Práticas
Configuração
- Habilitar strict mode no tsconfig.json
- Usar noUncheckedIndexedAccess
- Configurar paths aliases (@/)
- Setup de testes com tipos
Código
- Preferir interfaces para objetos
- Usar type para unions e intersections
- Evitar any - usar unknown quando necessário
- Usar const assertions para literais
- Documentar tipos complexos com JSDoc
Patterns
- Usar discriminated unions para estados
- Branded types para IDs e valores semânticos
- Result type para error handling
- Builder pattern para objetos complexos
Conclusão
Dominar patterns avançados de TypeScript transforma a forma como você escreve código. O investimento em aprender esses conceitos se paga rapidamente em forma de menos bugs, melhor autocompletion e código mais fácil de manter.
Recursos Adicionais
Documentação:
Ferramentas:
- ts-toolbelt - Utility types avançados
- zod - Schema validation com inferência de tipos
Este guia é atualizado conforme novas features do TypeScript são lançadas.

