CogniPrep's tsconfig.json is 30 lines long, and the only type-safety line in it is "strict": true. pnpm typecheck runs tsc --noEmit and passes clean on main.
That tells you less than it sounds like. strict is a bundle of eight checks (noImplicitAny, strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, useUnknownInCatchVariables, alwaysStrict). TypeScript ships at least nine more that it deliberately leaves out of that bundle, and a project with strict: true has made no decision at all about those nine. It has simply accepted the default.
So I ran each one individually against the whole repository and counted the errors:
npx tsc --noEmit --noUncheckedIndexedAccess 2>&1 | grep -c "error TS"
The results
| Flag | Errors |
|---|---|
noFallthroughCasesInSwitch |
0 |
noUncheckedIndexedAccess |
3 |
noImplicitOverride |
5 |
noImplicitReturns |
28 |
noUnusedParameters |
40 |
noUnusedLocals |
52 |
exactOptionalPropertyTypes |
431 |
verbatimModuleSyntax |
571 |
noPropertyAccessFromIndexSignature |
3,252 |
For scale: this is a Next.js 16 app with 26 database tables, 408 page.tsx files, 537 components, and 7,715 unit tests across 353 test files that run in 26 seconds.
The two that are free are the interesting ones
noFallthroughCasesInSwitch is zero. That surprised me more than any other number here, because this codebase routes by switch over an id in several places, and some of those statements have hundreds of cases. Hundreds of cases is exactly the shape where a missing break hides, and the compiler says there is not one. That flag costs nothing to turn on and it closes a bug class permanently, which makes leaving it off indefensible rather than merely untidy.
noUncheckedIndexedAccess is 3, and all three are the same line of the same one-off script under scripts/: a small numeric helper that indexes two arrays of fixed coefficients. The flag is right on its own terms. coefficients[0] is number | undefined as far as the type system knows, because TypeScript does not track array length. It is also wrong about this particular line, in the sense that the arrays are literals three lines above and never mutated.
That is the whole argument about this flag in miniature. It finds real holes in code that reads an array by an index it computed, and it also objects to code that reads a constant table by a constant index. Three errors is a price I will pay for the first category.
The two that look enormous are not the same kind of enormous
noPropertyAccessFromIndexSignature at 3,252 errors sounds like a disaster and is not a bug class at all. It asks you to write row['total'] instead of row.total whenever the type is an index signature, and almost every one of those 3,252 is a stylistic rewrite with no behavioural question attached. Turning that on would produce the largest diff in the history of the repository and catch nothing.
verbatimModuleSyntax at 571 is also mechanical, almost all of it import type annotations that a codemod writes. It is worth doing for a different reason than safety: it makes the import graph say out loud which imports survive compilation, which matters when you are trying to keep server-only modules out of a client bundle.
exactOptionalPropertyTypes at 431 is the one real decision in the table. It distinguishes "the key is absent" from "the key is present and set to undefined", which is a distinction that genuinely exists in our data layer, where optional columns, optional request fields and optional props all meet. 431 errors is a project, not an afternoon, and most of them would be answered by writing out which of the two cases the code means. That is useful work and it is also the kind of work that competes with shipping features.
What I did with the answer
The two free ones go on. The two mechanical ones are a codemod when something else makes me touch the import graph anyway. exactOptionalPropertyTypes is a maybe with a number attached, which is better than a maybe without one.
The method is what I would recommend to anyone with strict: true and no other type-checking line in their config. It is one command per flag, no code changes, no branches, and the output is a cost estimate for each decision you have been making by default. Mine took about fifteen minutes to produce, and two of the nine results said "this has been free the entire time".
See it running
Everything type checking protects here is a number that reaches a page. Open cogniprep.app/pricing and read the free-tier line: the figures in it (77 free practice games, 5 free plays per provider, the first 3 scores with each provider) are interpolated from one constants file rather than typed into the copy, which is why the sentence cannot drift away from the thing the app actually enforces. Then open the provider catalogue, which is generated from one array of providers, and an individual provider page under it. Pages built by indexing arrays and reading optional fields are exactly the surface those nine flags argue about.
Top comments (0)