Pular para o conteúdo
desenvolvimento

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.

Douglas M. Pereira4 min de leitura
typescriptboas práticastiposgenericsnodejsfrontend

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.