A few years of steadily raising TypeScript strictness on individual projects, one flag at a time, convinced us to stop treating strict mode as something to earn into gradually and make it the non-negotiable default for every new project starting now. The incremental approach had worked well enough on individual projects, but it meant every new project started from a permissive baseline and needed someone to actively decide, and then actively implement, tightening it later, a decision that predictably got deprioritized against more visible feature work often enough that several projects never fully caught up.
The upfront friction for new team members is real but small, and it's far cheaper than retrofitting strictness onto a codebase after a year of loose typing has already let real bugs through. Strict null checks in particular tend to surface a batch of "this could theoretically be undefined here" warnings the moment they're turned on, and on a codebase with a year of history, that batch can be large enough to feel discouraging. Turning strictness on from a project's very first commit means that batch never accumulates in the first place, since every line of code is written under the same rules from day one rather than needing a disruptive retrofit later. The new project template's `tsconfig.json` ships with the full set of strict flags on, no opt-out:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true
}
}A typical fix under strict null checks looks small in isolation, confirming a value that was already effectively guaranteed to exist but that the compiler couldn't previously prove:
- function getDisplayName(user) {
- return user.profile.displayName.toUpperCase();
- }
+ function getDisplayName(user: User) {
+ return user.profile?.displayName?.toUpperCase() ?? 'Unknown';
+ }Multiply that single fix by every place in a codebase where a value's presence was merely assumed rather than proven, and it's easy to see why retrofitting strictness onto a year-old codebase produces a much larger batch of these than writing the same code strict from day one.
Running `tsc --noEmit` against one of the older projects after simply flipping `strict: true` on, without touching any code yet, made that batch concrete:
$ tsc --noEmit
src/lib/pricing.ts:41:12 - error TS2532: Object is possibly 'undefined'.
src/lib/pricing.ts:58:29 - error TS18048: 'discount' is possibly 'undefined'.
src/routes/checkout.ts:103:5 - error TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string'.
Found 214 errors in 47 files.Two hundred and fourteen errors on a single flip of one flag is exactly the kind of number that makes a team quietly revert the change rather than work through it, which is precisely why we wanted new projects never to accumulate that batch in the first place.
New project templates now ship strict by default with no opt-out, which removes the decision entirely from a project's initial setup rather than leaving it as a checkbox someone might reasonably skip under early deadline pressure when strictness feels like it's slowing down getting an MVP shipped. We were deliberate about removing the opt-out rather than just changing the default, since a default that can be turned off under pressure tends to get turned off under exactly the kind of pressure that makes strictness most valuable in the long run.
We tracked bug reports across a sample of projects built under the new default compared against a similar sample of older projects that had strictness added later, and the pattern matched what we expected going in: projects strict from the start had meaningfully fewer bugs traceable to null or undefined handling reaching production, the exact category of bug strict null checking is specifically designed to catch at compile time rather than runtime.
The comparison we tracked across project samples looked like this, bugs per project traceable specifically to null or undefined handling reaching production over a comparable period:
project group projects null/undefined bugs (prod)
strict from first commit 6 2
strictness added later 6 11That gap, roughly a fifth as many production bugs in the strict-from-the-start group, is the number that ultimately made removing the opt-out an easy call rather than a contentious one.
We backed the policy with a check of its own, since a convention that isn't enforced tends to erode the same way an unrelated tagging convention did on another project: a CI step that fails if a new project's `tsconfig.json` doesn't match the required strict baseline:
- name: Verify tsconfig strictness
run: |
node scripts/check-strict-tsconfig.js tsconfig.jsonThat check is deliberately blunt, it doesn't try to evaluate whether a project's types are good, only whether the required flags are present and untouched, which keeps it simple enough to trust without much maintenance.
The team's initial reaction to the new default was mixed, a few engineers who'd built up real fluency working around looser typing found the stricter rules genuinely slowed down their first few weeks on a new strict project. That adjustment period was real, and we didn't pretend it wasn't when rolling this out, but it consistently shortened for each individual engineer as they built familiarity with the patterns strict mode rewards, and it's a one-time cost per engineer rather than a recurring cost the way retrofitting strictness onto an existing codebase would be. A few months in, feedback shifted from complaints about friction to engineers actively requesting the same strict defaults be backported to a couple of older projects that had never fully caught up.