Strict mode no TypeScript: o que você ativa
1. Visão geral
"strict": true é a linha mais importante em tsconfig.json. Mas muita gente a ativa sem saber exatamente o que está sendo ativado. Este artigo passa por cada flag que strict ativa, os erros reais que cada um detecta, alguns flags que você deve ativar adicionalmente, e como migrar um projeto antigo para strict sem ter que corrigir milhares de erros de uma vez.
2. strict é um grupo de flags
strict não é uma verificação individual. É um switch mestre que ativa simultaneamente um grupo de flags:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"strict": true
}
}
Com TypeScript 5.x e 6.x, strict: true é equivalente a:
{
"compilerOptions": {
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"strictBuiltinIteratorReturn": true,
"noImplicitThis": true,
"useUnknownInCatchVariables": true,
"alwaysStrict": true
}
}
Há duas coisas que você precisa lembrar:
- Sempre que TypeScript adiciona uma nova verificação “strict”, por exemplo
useUnknownInCatchVariablesna 4.4 oustrictBuiltinIteratorReturnna 5.6, essa verificação é automaticamente ativada comstrict. Portanto, atualizar TypeScript pode fazer novos erros aparecerem mesmo que você não tenha modificado o código. - Você pode ativar
stricte depois desativar um flag específico. Flags específicas sempre vencem o flag geral.
TypeScript 6.0 mudou o valor padrão de strict para true. Ainda assim, você deve declarar explicitamente esse flag em tsconfig.json, para que quem ler a config não precise adivinhar.
Para experimentar os exemplos abaixo, você só precisa instalar TypeScript e executar tsc --noEmit:
npm install --save-dev typescript
npx tsc --noEmit
3. Os flags mais importantes
3.1. noImplicitAny
Quando TypeScript não consegue inferir o tipo, ele atribui implicitamente any. Esse flag transforma esse caso em um erro.
function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price, 0);
}
error TS7006: Parameter 'items' implicitly has an 'any' type.
Se você não tivesse esse flag, items seria any. Então item.prise (um typo) ainda compilaria normalmente. A solução é declarar o tipo explicitamente:
interface OrderItem {
name: string;
price: number;
}
function calculateTotal(items: OrderItem[]): number {
return items.reduce((sum, item) => sum + item.price, 0);
}
Esse flag só bloqueia any implícito. Você ainda pode escrever any explicitamente se realmente precisar.
3.2. strictNullChecks
Este é o flag mais valioso. Quando desativado, null e undefined podem ser atribuídos a qualquer tipo. Quando ativado, eles se tornam tipos próprios e você deve lidar com eles explicitamente.
interface Customer {
id: number;
email: string;
}
const customers: Customer[] = [{ id: 1, email: "an@example.com" }];
function getCustomerEmail(id: number): string {
const customer = customers.find((c) => c.id === id);
return customer.email;
}
error TS18048: 'customer' is possibly 'undefined'.
Array.prototype.find retorna Customer | undefined. Sem esse flag, TypeScript ignora undefined, e o erro Cannot read properties of undefined só aparece em runtime. Corrija assim:
function getCustomerEmail(id: number): string | undefined {
const customer = customers.find((c) => c.id === id);
return customer?.email;
}
A função agora retorna string | undefined. O tipo de retorno diz a verdade, então o código que chama a função também é forçado a lidar com o caso de não encontrado.
Nota: Não “corrija” o erro usando o operador non-null
customer!.email. O operador!apenas silencia o compilador, ele não faz nenhuma verificação. Use apenas quando você tem certeza de que o valor existe e há uma razão pela qual o compilador não consegue ver.
3.3. strictPropertyInitialization
Este flag exige que toda propriedade de classe seja atribuída a um valor, seja na declaração ou no construtor. Ele precisa que strictNullChecks esteja ativado para funcionar.
class PaymentService {
apiKey: string;
retries = 3;
constructor(private readonly baseUrl: string) {}
}
error TS2564: Property 'apiKey' has no initializer and is not definitely assigned in the constructor.
Você tem três formas de lidar com isso:
class PaymentService {
apiKey: string; // 1. atribuir no construtor
webhookSecret?: string; // 2. declarar como opcional
client!: HttpClient; // 3. definite assignment: "confie em mim, será atribuído"
constructor(apiKey: string) {
this.apiKey = apiKey;
}
}
interface HttpClient {
post(url: string, body: unknown): Promise<unknown>;
}
A forma 3 (!) é comum em frameworks que usam dependency injection ou decorators, como Angular ou TypeORM. Lá a propriedade é atribuída após o construtor terminar. Fora esses casos, prefira a forma 1 ou 2.
3.4. useUnknownInCatchVariables
Em JavaScript, você pode throw qualquer coisa, não necessariamente Error. Este flag muda o tipo da variável em catch de any para unknown.
async function chargeOrder(orderId: string): Promise<void> {
try {
await fetch(`/api/orders/${orderId}/charge`, { method: "POST" });
} catch (err) {
console.error(err.message);
}
}
error TS18046: 'err' is of type 'unknown'.
Para usar err, você precisa estreitar o tipo primeiro:
catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error(`Charge failed: ${message}`);
}
3.5. strictFunctionTypes
Este flag verifica os parâmetros de tipos de função de forma contravariante. Em termos simples: você não pode passar uma função que só consegue lidar com um tipo mais específico para um lugar que precisa de uma função que lida com um tipo mais amplo.
interface Animal { name: string; }
interface Dog extends Animal { bark(): void; }
const handleDog = (dog: Dog) => dog.bark();
const handleAnimal: (animal: Animal) => void = handleDog;
error TS2322: Type '(dog: Dog) => void' is not assignable to type '(animal: Animal) => void'.
Se essa atribuição fosse aceita, chamar handleAnimal({ name: "Mèo" }) falharia porque o objeto passado não tem bark. Há uma exceção que você deve saber: este flag não se aplica a métodos declarados com a sintaxe method(x: T): void em interface. Essa é uma escolha deliberada do TypeScript para manter compatibilidade com muitas APIs existentes.
3.6. Os três flags restantes
Esses três flags raramente causam erros em código novo, mas ainda vale a pena conhecê-los:
function formatPrice(amount: number, currency: string): string {
return `${amount.toFixed(2)} ${currency}`;
}
formatPrice.call(null, "100", "VND"); // strictBindCallApply
function onClick() {
console.log(this.id); // noImplicitThis
}
error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
error TS2683: 'this' implicitly has type 'any' because it does not have a type annotation.
strictBindCallApplyverifica os argumentos quando você chamacall,applyebind.noImplicitThisreporta um erro quandothistem tipoanyimplícito. Você corrige declarando um parâmetro falsofunction onClick(this: HTMLButtonElement).alwaysStrictemite"use strict"em cada arquivo de saída e faz parse do código no strict mode do JavaScript. Com módulos ES, strict mode já é padrão.
4. Flags que strict não ativa
Muita gente acha que strict: true é o nível máximo de rigor. Não é. Os flags abaixo ficam fora do grupo strict, mas são muito worthwhile ativar para projetos novos:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
}
}
Entre esses, noUncheckedIndexedAccess é o flag mais importante. Por padrão, TypeScript trata o acesso a arrays por índice ou acesso a objetos por chave dinâmica como sempre tendo um valor:
const stockBySku: Record<string, number> = { "SKU-1": 10 };
const quantity = stockBySku["SKU-999"];
console.log(quantity.toFixed(0));
Sem esse flag, quantity tem tipo number e o código acima falha em runtime. Quando o flag está ativado, quantity tem tipo number | undefined:
error TS18048: 'quantity' is possibly 'undefined'.
Os outros flags:
exactOptionalPropertyTypes:discount?: numbersignifica “a chave pode estar ausente”, não permite atribuir explicitamentediscount: undefined.noImplicitOverride: método que sobrescreve a classe pai deve ter a palavra-chaveoverride. Assim, quando a classe pai renomeia um método, a classe filha reportará um erro em vez de silenciosamente criar um novo método.noImplicitReturns: todo branch da função deve fazerreturn.noFallthroughCasesInSwitch: evita esquecerbreakemswitch.
Nota:
noUncheckedIndexedAccessé bastante “barulhento”. O loopfor (let i = 0; i < arr.length; i++)também faz com quearr[i]tenha tipoT | undefined. Mude parafor...ofouarr.forEach. Para projetos grandes existentes, ative esse flag por último.
5. Verifique comportamento com testes
Verificação de tipo roda em tempo de compilação, mas você ainda deve ter testes para comportamento runtime e tipo de retorno. Vitest suporta ambos:
npm install --save-dev vitest
// customer.ts
export interface Customer { id: number; email: string; }
const customers: Customer[] = [{ id: 1, email: "an@example.com" }];
export function getCustomerEmail(id: number): string | undefined {
return customers.find((c) => c.id === id)?.email;
}
// customer.test.ts
import { describe, it, expect, expectTypeOf } from "vitest";
import { getCustomerEmail } from "./customer";
describe("getCustomerEmail", () => {
it("retorna email quando encontra customer", () => {
expect(getCustomerEmail(1)).toBe("an@example.com");
});
it("retorna undefined quando não encontra", () => {
expect(getCustomerEmail(42)).toBeUndefined();
});
it("tipo de retorno inclui undefined", () => {
expectTypeOf(getCustomerEmail).returns.toEqualTypeOf<string | undefined>();
});
});
O terceiro teste é um type test. Suponha que depois alguém mude a assinatura da função para : string e use ! para forçar o tipo. Então vitest --typecheck (ou tsc --noEmit no arquivo de teste) reportará um erro, porque o tipo de retorno não corresponde mais a string | undefined.
npx vitest run --typecheck
✓ customer.test.ts (3 tests)
Test Files 1 passed (1)
Tests 3 passed (3)
6. Migrando um projeto antigo para strict
Ativar strict: true em uma base de código grande pode gerar milhares de erros. A abordagem prática é fazer isso passo a passo.
Passo 1: ative strict, mas temporariamente desative os flags mais “barulhentos”.
{
"compilerOptions": {
"strict": true,
"strictNullChecks": false,
"strictPropertyInitialization": false,
"useUnknownInCatchVariables": false
}
}
noImplicitAny geralmente é o mais fácil de corrigir, então comece com ele. Cada vez que corrigir um flag, remova a linha que o desativa. Código novo será automaticamente submetido a todas as verificações ativas.
Passo 2: conte erros por flag para estimar o esforço.
npx tsc --noEmit --strictNullChecks true | grep -c "error TS"
Passo 3: com strictNullChecks, corrija por diretório. Marque temporariamente os locais que você ainda não corrigiu com // @ts-expect-error, com uma nota explicando o motivo. Não use @ts-ignore. Razão: @ts-expect-error reportará automaticamente um erro quando a linha abaixo não tiver mais erro, assim você não deixará comentários desnecessários.
// @ts-expect-error TODO(strict): customer pode ser undefined, tratar em PR #482
const email: string = findCustomer(id).email;
Passo 4: evite regressão em CI. Adicione tsc --noEmit ao pipeline para que ninguém possa fazer merge de código que introduza novos erros de tipo.
Nota: Não use
skipLibCheckouanyem massa como uma forma de “ativar strict rapidamente”.skipLibCheck: trueapenas ignora a verificação de arquivos.d.ts, então é seguro manter. Espalharanypor todos os lugares anulará todos os benefícios da migração.
7. Conclusão
strict: true ativa nove verificações. Os dois com maior valor são noImplicitAny e strictNullChecks, porque elas bloqueiam a maioria dos erros undefined is not a function e Cannot read properties of undefined já no tempo de compilação. Para projetos novos, você deve ativar strict junto com noUncheckedIndexedAccess, noImplicitOverride e exactOptionalPropertyTypes desde o início. Para projetos existentes, ative strict e depois desative temporariamente cada flag específico, corrija gradualmente em ordem de dificuldade, e use CI para manter o número de erros em declínio, não aumentando.