Muita gente usa TypeScript sem nunca escrever um generic. O código compila, funciona, e parece suficiente. Mas quando você precisa de uma função que funciona com qualquer tipo de dado, mantendo type safety, generics deixam de ser teoria e viram necessidade.
Generics básicos: quando e por quê
Um generic é um parâmetro de tipo. Assim como uma função aceita parâmetros de valor, um generic aceita parâmetros de tipo. A razão de existir: escrever uma vez, tipar para qualquer tipo.
// Sem generic: precisa de uma função para cada tipo
function getFirstString(arr: string[]): string | undefined { return arr[0] }
function getFirstNumber(arr: number[]): number | undefined { return arr[0] }
// Com generic: uma função para todos
function getFirst<T>(arr: T[]): T | undefined {
return arr[0]
}
const a = getFirst(['a', 'b', 'c']) // tipo: string | undefined
const b = getFirst([1, 2, 3]) // tipo: number | undefined
O TypeScript infere o tipo do generic automaticamente. Quando você passa um array de strings, T vira string. Sem precisar declarar.
Constraints: limitar o que o generic aceita
Às vezes o generic precisa de uma garantia. "Funciona com qualquer tipo, mas esse tipo precisa ter essa propriedade." Constraints resolvem isso com extends:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
const user = { name: 'Marc', age: 30 }
getProperty(user, 'name') // ok: retorna string
getProperty(user, 'email') // erro: 'email' não existe em user
keyof T retorna um union dos nomes das propriedades de T. K extends keyof T garante que a chave passada existe no objeto. O erro acontece em tempo de compilação, não em runtime.
Conditional types: tipos que decidem
Conditional types usam a sintaxe T extends U ? X : Y. O tipo resultado depende de uma condição. Pense em ternário, mas para tipos.
type IsString<T> = T extends string ? true : false
type A = IsString<'hello'> // true
type B = IsString<42> // false
O caso de uso real: derivar tipos com base em inputs. Uma API que retorna tipos diferentes dependendo do parâmetro.
type ApiResponse<T extends 'user' | 'product'> =
T extends 'user' ? { id: string; name: string; email: string }
: T extends 'product' ? { id: string; title: string; price: number }
: never
type UserResponse = ApiResponse<'user'> // { id: string; name: string; email: string }
type ProductResponse = ApiResponse<'product'> // { id: string; title: string; price: number }
Mapped types: transformar tipos em massa
Mapped types iteram sobre as propriedades de um tipo e produzem um novo tipo. A sintaxe é { [K in Keys]: NewType }.
// Torna todas as propriedades opcionais
type MyPartial<T> = { [K in keyof T]?: T[K] }
// Torna todas as propriedades readonly
type MyReadonly<T> = { readonly [K in keyof T]: T[K] }
// Torna propriedades específicas obrigatórias
type WithRequired<T, K extends keyof T> =
T & Required<Pick<T, K>>
type User = { name?: string; email?: string; id?: string }
type UserWithId = WithRequired<User, 'id'>
// resultado: { name?: string; email?: string; id: string }
Utility types do TypeScript que você já usa (ou deveria)
TypeScript já vem com utility types prontos. Conhecer os mais usados evita re-inventar a roda:
type User = { id: string; name: string; email: string; role: 'admin' | 'user' }
// Pick: seleciona propriedades
type UserBasic = Pick<User, 'id' | 'name'>
// Omit: remove propriedades
type CreateUser = Omit<User, 'id'>
// Record: cria tipo com chaves e valores tipados
type Roles = Record<string, { permissions: string[] }>
// Extract: extrai tipos de um union
type AdminRole = Extract<User['role'], 'admin'>
// Exclude: remove tipos de um union
type NonAdmin = Exclude<User['role'], 'admin'> // 'user'
Pattern real: tipar uma função que aceita callbacks
Um pattern comum que combina generics com constraints: uma função que aceita callbacks com tipos relacionados ao input.
function mapWithFallback<TInput, TOutput>(
items: TInput[],
transform: (item: TInput) => TOutput,
fallback: TOutput
): TOutput[] {
return items.map(item => {
try {
return transform(item)
} catch {
return fallback
}
})
}
// TypeScript infere TInput = string, TOutput = number
const result = mapWithFallback(
['1', '2', 'abc'],
s => parseInt(s),
0
) // tipo: number[]
Quando parar de complicar
Generics e conditional types são poderosos, mas têm um custo de legibilidade. Se você está escrevendo um tipo que ninguém no time consegue ler em 10 segundos, simplifique. Type safety importa, mas manutenção importa mais.
Use generics quando precisa de flexibilidade de tipo. Use conditional types quando tem lógica de tipo genuína. Use mapped types quando precisa transformar estruturas. Quando nada disso se aplica, um any localizado e documentado é melhor que um tipo genial que ninguém entende.
Curtiu o conteúdo?
Construo produtos web e soluções com IA do jeito certo — arquitetura sólida, código sustentável e entrega real.
Vamos conversar