tsconfig.json controls how strict the compiler is — and the difference between a permissive and a strict configuration is enormous in practice, since most of TypeScript's real bug-catching power is opt-in.
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"strict": true,
"noUncheckedIndexedAccess": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}
"strict": true is a single flag that turns on a whole family of individually-toggleable checks:
Flag (bundled into strict) | What it catches |
|---|---|
noImplicitAny | A parameter/variable with no inferable type and no annotation silently becoming any |
strictNullChecks | null/undefined not being automatically assignable to every other type — arguably the single highest-value flag in the whole list |
strictFunctionTypes | Unsound function parameter variance in assignments between function types |
strictPropertyInitialization | A class field declared without ? or ! that's never assigned in the constructor |
alwaysStrict | Emits "use strict" and parses files in strict JS mode |
Without strictNullChecks, null and undefined are quietly assignable to any type, which defeats a huge fraction of TypeScript's usefulness — a variable typed string could secretly be null at runtime and the compiler would never warn you before a .toUpperCase() call blows up. Turning strict on for a codebase that started without it is usually a large, incremental migration in practice, not a single flip of a switch — which is exactly why it's worth starting every new project with strict: true from day one rather than adding it later.
noUncheckedIndexedAccess (not part of strict, but worth enabling separately) closes a related gap: without it, someArray[i] is typed as T, not T | undefined, even though indexing past the end of an array is completely legal JavaScript and returns undefined at runtime.