Todo projeto TypeScript que eu audito tem "strict": true no tsconfig.json. O mesmo projeto tem // @ts-ignore espalhado pelo código, any em pontos críticos e ninguém no time que consiga explicar o que a flag protege.

"strict": true ativa um grupo de flags do compilador. Cada flag existe porque uma classe de bug chegou em produção antes de alguém escrever o teste que a pegaria. O mapa de flags troca o reclame do compilador por um inventário de riscos do seu código.

O que "strict": true habilita

strictNullChecks

Sem ela, null e undefined entram em qualquer tipo. Com ela, o compilador cobra o tratamento da ausência no ponto em que o valor pode faltar.

// sem strictNullChecks: compila, quebra em runtime
function getUser(id: string): User {
  return db.find(id) // pode devolver undefined
}
const nome = getUser('123').name // TypeError: Cannot read property 'name' of undefined

// com strictNullChecks: o compilador aponta a linha
function getUser(id: string): User | undefined {
  return db.find(id)
}
const user = getUser('123')
const nome = user?.name

A referência da flag descreve a regra de atribuição. Os TypeError que sobrevivem em produção e que eu consigo rastrear até a origem quase sempre são leitura de propriedade em valor ausente.

noImplicitAny

Sem ela, parâmetro de função sem tipo vira any sem aviso e a checagem desliga para a função inteira. Com ela, o compilador exige o tipo declarado.

// sem noImplicitAny: param é any implícito
function process(data) {
  return data.value.toUpperCase() // nada é verificado
}

// com noImplicitAny: o compilador pede o tipo
function process(data: { value: string }) {
  return data.value.toUpperCase()
}

A documentação da flag é curta. Em migração, ela é a que gera erro em massa: qualquer arquivo sem tipos declarados quebra de uma vez.

strictFunctionTypes

Compara tipos de função pela variação correta dos parâmetros. Sem a flag, o TypeScript aceita qualquer direção; com ela, um handler que espera MouseEvent não sobe para um slot que recebe Event, porque o caller pode passar um Event puro e o handler quebra.

type Handler = (event: Event) => void
const myHandler: Handler = (event: MouseEvent) => {
  // strictFunctionTypes aponta o erro
}

Declaração de método (foo() {}) continua bivariante por regra da especificação. Escreva callback como propriedade de função quando quiser a checagem. A referência da flag explica a exceção dos métodos.

strictPropertyInitialization

Cobra inicialização de propriedade de classe no constructor. Evita leitura de campo que o compilador aceitava como pronto e que chega como undefined em runtime.

class UserService {
  private db: Database // erro: não recebe valor no constructor

  constructor() {}
}

// correto: dependência injetada no constructor
class UserService {
  private db: Database

  constructor(db: Database) {
    this.db = db
  }
}

// injeção fora do constructor, com definite assignment assertion
class UserService {
  private db!: Database
}

O operador ! registra a intenção e desliga a checagem daquele campo. A documentação da flag cobre o operador.

Duas flags fora do strict que valem o custo

noUncheckedIndexedAccess

Acesso por índice devolve T | undefined em vez de T. Cobre o bug de assumir que users[0] existe, comum quando a lista vem de filtro de API.

// tsconfig.json
{ "compilerOptions": { "noUncheckedIndexedAccess": true } }

const items = ['a', 'b', 'c']
const first = items[0] // string | undefined
const safe = first?.toUpperCase()

A flag gera ruído em for sobre array e em mapa com chave dinâmica. O time absorve esse custo uma vez e para de receber undefined em runtime. Documentação da flag.

exactOptionalPropertyTypes

Separa { prop?: string } (o campo pode faltar) de { prop: string | undefined } (o campo chega, com valor undefined). Sem a flag, o TypeScript trata as duas formas como iguais e você escreve guarda de null em campo que a API envia presente. Documentação da flag.

Migrar projeto legado para strict

Ligar "strict": true de uma vez em monorepo de 200 mil linhas trava o time por semanas. O caminho que eu uso:

  1. Ligue "strict": true e cubra os erros com // @ts-ignore. Aceite o débito como temporário e visível.
  2. Instale @typescript-eslint/ban-ts-comment e configure para aceitar @ts-expect-error e rejeitar @ts-ignore. O legado continua no repositório; código novo que silencia precisa justificar com o comentário de erro esperado, que o compilador valida. @ts-ignore não valida nada.
  3. Resolva os silenciadores arquivo por arquivo, começando pelos módulos com mais chamadas em produção.

O código novo recebe strict no dia um. O legado entra conforme cada arquivo é corrigido, e a contagem de @ts-expect-error no repositório vira o número de progresso que o time acompanha.

O custo de silenciar o compilador

Cada // @ts-ignore, cada as any, cada flag desligada é uma aposta: você confia no seu raciocínio mais do que na checagem daquele ponto. Em biblioteca externa com tipos ruins, a aposta faz sentido. Em código interno, o bug que a flag pegava volta em runtime, e o relatório de erro chega semanas depois, longe da linha que causou o problema.

Quando o compilador aponta uma linha e você não entende o motivo, o caminho é ler o erro e corrigir o tipo. O capítulo de narrowing do handbook cobre as situações de undefined e de tipo condicional que aparecem com mais frequência.

Referências