TypeScript: Boas Práticas para Código Escalável e Manutenível
Padrões e práticas de TypeScript para código de produção: tipos utilitários, generics, discriminated unions, strict mode, organização e erros comuns a evitar.
Por que TypeScript é mais do que tipagem
TypeScript não é só "JavaScript com tipos". Em projetos de médio e grande porte, ele substitui boa parte da documentação, previne classes inteiras de bugs em runtime, e viabiliza refatorações seguras com autocomplete preciso.
Mas TypeScript mal usado — cheio de any, tipos desnecessariamente repetidos, sem strict mode — desperdiça todos esses benefícios.
Configuração strict mode
O ponto de partida é strict: true no tsconfig.json:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true,
"module": "NodeNext",
"target": "ES2022"
}
}
strict: true habilita: strictNullChecks, noImplicitAny, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization e mais. Nunca desative em projetos novos.
Tipos Utilitários — use-os
TypeScript tem tipos built-in que eliminam duplicação:
interface User {
id: string
name: string
email: string
password: string
role: 'admin' | 'user'
createdAt: Date
}
// Partial<T> — todos os campos opcionais
type UserUpdate = Partial<User>
// Omit<T, K> — exclui campos
type UserResponse = Omit<User, 'password'> // nunca retornar senha
// Pick<T, K> — seleciona campos
type UserSummary = Pick<User, 'id' | 'name' | 'email'>
// Required<T> — todos obrigatórios
type UserCreateRequired = Required<Omit<User, 'id' | 'createdAt'>>
// Readonly<T> — imutável
type ImmutableUser = Readonly<User>
// Record<K, V>
type UsersByRole = Record<User['role'], User[]>
Discriminated Unions para modelar estado
Evita o problema de "campos opcionais que dependem de outros campos":
// ❌ Ruim: campos que podem ou não existir dependendo do status
interface Order {
id: string
status: 'pending' | 'paid' | 'shipped' | 'cancelled'
paidAt?: Date // só existe se status === 'paid'
trackingCode?: string // só existe se status === 'shipped'
cancelReason?: string // só existe se status === 'cancelled'
}
// ✅ Bom: cada estado tem seu tipo exato
type Order =
| { id: string; status: 'pending' }
| { id: string; status: 'paid'; paidAt: Date }
| { id: string; status: 'shipped'; paidAt: Date; trackingCode: string }
| { id: string; status: 'cancelled'; cancelReason: string }
// O TypeScript force a verificar o discriminante antes de acessar campos
function processOrder(order: Order) {
if (order.status === 'shipped') {
console.log(order.trackingCode) // ✅ TypeScript sabe que existe
}
}
Generics para código reutilizável
// Wrapper genérico para respostas de API
interface ApiResponse<T> {
data: T
meta: {
total: number
page: number
perPage: number
}
}
// Repository pattern genérico
interface Repository<T, ID = string> {
findById(id: ID): Promise<T | null>
findAll(filters?: Partial<T>): Promise<T[]>
create(data: Omit<T, 'id' | 'createdAt'>): Promise<T>
update(id: ID, data: Partial<T>): Promise<T>
delete(id: ID): Promise<void>
}
// Função genérica com constraint
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
const user: User = { id: '1', name: 'Douglas', email: 'x@x.com', ... }
const name = getProperty(user, 'name') // string — inferido corretamente
Evitando armadilhas comuns
Non-null assertion — use com parcimônia
// ❌ Perigoso: silencia o erro, mas pode quebrar em runtime
const user = getUser()!
console.log(user.name)
// ✅ Melhor: tratar o caso null explicitamente
const user = getUser()
if (!user) throw new Error('Usuário não encontrado')
console.log(user.name)
Type predicates para type narrowing correto
interface Cat { type: 'cat'; purrs: boolean }
interface Dog { type: 'dog'; barks: boolean }
type Animal = Cat | Dog
// Type predicate: garante que o narrowing funciona
function isCat(animal: Animal): animal is Cat {
return animal.type === 'cat'
}
function makeSound(animal: Animal) {
if (isCat(animal)) {
console.log(animal.purrs ? 'Purrr' : 'Miau') // TypeScript sabe que é Cat
}
}
Template literal types
// Tipos para rotas de API
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
type ApiRoute = `/api/${string}`
type Endpoint = `${HttpMethod} ${ApiRoute}`
const endpoint: Endpoint = 'GET /api/users' // ✅
// const bad: Endpoint = 'PATCH /api/users' // ❌ Error
// CSS properties tipadas
type CSSUnit = 'px' | 'em' | 'rem' | '%' | 'vh' | 'vw'
type CSSLength = `${number}${CSSUnit}`
const width: CSSLength = '100px' // ✅
satisfies operator (TS 4.9+)
// Valida o tipo sem alargá-lo
const config = {
host: 'localhost',
port: 5432,
database: 'mydb',
} satisfies Record<string, string | number>
// Preserva os tipos literais:
config.port // inferido como 5432, não como number
Organização de tipos
src/
├── types/
│ ├── index.ts # re-exporta tudo
│ ├── domain/ # User, Order, Product, etc.
│ ├── api/ # Request/Response types das rotas
│ └── utilities/ # tipos utilitários do projeto
Evite: colocar tipos em arquivos .d.ts a menos que sejam extensões de módulos externos. Para tipos do seu código, use .ts normal.
