Configuring the Compiler
The tsconfig options that change how much the compiler protects you, what target and module actually control, and why strict should be on from the first day.
The tsconfig options that change how much the compiler protects you, what target and module actually control, and why strict should be on from the first day.
You have been told to set strict and noUncheckedIndexedAccess
and something about module resolution, and you have been setting
them without a proper explanation. Meanwhile tsc --init
produced a file with sixty commented-out options, most of which
mean nothing to you.
tsconfig.json decides how much the compiler protects you and
whether your imports resolve at all. By the end of this lesson
you will know the options that matter, what strict actually
turns on, and how to tighten an existing project without
producing four thousand errors on a Monday morning.
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"verbatimModuleSyntax": true,
"rootDir": "src",
"outDir": "dist",
"sourceMap": true,
"declaration": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}That is a complete configuration for a Node project. The
sections below explain each group. tsc --init produces
something far longer, and deleting what you do not understand is
a reasonable first move — every option has a default.
The nearest tsconfig.json above a file governs it, and
extends lets several projects share a base:
{ "extends": "../tsconfig.base.json" }"strict": true is a switch for several checks, and it is worth
knowing what they are:
strictNullChecks null and undefined are separate types
noImplicitAny a parameter with no type is an error
strictFunctionTypes function parameter types checked properly
strictBindCallApply call/apply/bind arguments checked
strictPropertyInitialization class fields must be assigned
noImplicitThis `this` with an unclear type is an error
alwaysStrict emit strict-mode output
useUnknownInCatchVariables a caught error is unknown, not anyTwo of those you have met as the point of whole lessons.
strictNullChecks is what makes absence part of the type.
useUnknownInCatchVariables is what makes a caught value
unknown.
Turn strict on in every new project, on the first day. The
cost of adding it later scales with the size of the codebase,
and each error it produces is a place that could fail at
runtime.
Three more are worth the friction, and this course has recommended each in passing:
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"exactOptionalPropertyTypes": truenoUncheckedIndexedAccess makes photos[0] and
record["key"] yield T | undefined. That is just true — the
array may be empty — and without it the checker confidently
hands you a value that is not there. It is the most valuable
option outside strict, and also the most annoying, because it
requires a check at every index.
noImplicitOverride requires the override keyword when a
method replaces a parent's, so renaming in the parent produces
an error rather than a subclass method that is silently never
called.
exactOptionalPropertyTypes distinguishes a property that is
absent from one explicitly set to undefined — which matters
when an update should leave a field alone rather than clear it.
Two more that catch mistakes for free:
An unused local is often a refactor that was never finished. If you would rather these were warnings than build failures, leave them to your linter instead.
These three cause the most confusion, and it is because they look related and answer completely different questions.
Which output language to emit.
Newer means less rewriting of your code into older equivalents, and requires a newer runtime.
What the checker believes exists.
If you use a recently-added method and the checker says it
does not exist, this is why. Browser code needs "DOM".
How imports are emitted and resolved.
The wrong pair is the cause of most "works in my editor, fails when I run it" problems.
target is which version of the output language to emit.
Newer means less rewriting of your code into older equivalents,
and requires a newer runtime. ES2022 is a safe modern choice
for Node and current browsers.
lib is what the checker believes exists. If you use a
method added recently and the checker says it does not exist,
lib is why. For browser code you need the DOM:
module and moduleResolution decide how imports are
emitted and how paths are resolved. This is where import errors
come from.
NodeNext reads "type": "module" in your package.json and
follows Node's actual rules, which is why it insists on
extensions. Bundler matches what bundlers do. Picking the
wrong pair is the cause of most "this import works in my editor
and fails when I run it" problems.
rootDir and outDir keep the compiled output beside your
source rather than mixed into it.
sourceMap is the one people leave off and regret. Without it,
a stack trace from production points at a line in compiled
output; with it, at your actual source. It costs a file per
module and turns an unreadable trace into a useful one.
declaration emits .d.ts files describing your types, which
is what other projects read when they import your package. Turn
it on for a library and leave it off for an application.
For checking without producing output:
That is the command from the setup lesson, and it belongs in CI.
Bad — turning everything on at once.
Good — one check at a time, in order of value.
Four thousand errors is not a task anyone will start, so the options get reverted and the project stays unchecked permanently. That is the real cost — not the errors, but the decision it forces.
Enabling one flag at a time gives you a number somebody can finish this week.
noImplicitAny
First, because untyped parameters hide the most. Every error it reports is a place the checker had given up entirely.
strictNullChecks
The large one and the valuable one. Expect this to be most of the total, and expect most of its errors to be real.
The rest of strict
By this point the remaining flags produce small numbers, and
you can turn strict: true on once they are all clean.
Then the non-strict options
noUncheckedIndexedAccess last, since it is the most
demanding and the codebase is in a much better state to
absorb it.
Two tools help when a subset of files can be strict already.
ts-strictify and similar packages check only changed files.
And Project References let one part of a repository be strict
while another catches up. Both are better than a single
@ts-nocheck at the top of a file, which turns off checking
entirely and tends to stay forever.
include selects what is compiled; exclude removes from that
selection. exclude does not stop a file being compiled if
something imported by an included file reaches it — imports win.
A common surprise: test files under src are compiled into
dist unless you exclude them, so a published package ships its
own tests.
And forceConsistentCasingInFileNames is worth its line. On
macOS and Windows the filesystem ignores case, so
./Captions.js resolves to captions.ts on your machine and
fails on a Linux build server. That option turns it into an
error where you wrote it.
You can now read a tsconfig.json and know which lines are load-
bearing, which turn on the checks this course has been relying
on, and which decide whether an import resolves. The two to act
on immediately: strict in anything new, and the incremental
path in anything old.
Next is Your First Real TypeScript Program, the closing lesson of this course. It assembles everything — modules, types at the edges, async work, error handling and a test — into one tool you can run and hand to someone else.
Before you move on, open the tsconfig.json of a project you
have and check whether strict is on. If it is not, turn on
noImplicitAny alone and see how many errors appear. That
number tells you how much the checker has not been doing for
you.
Node project, modern "module": "NodeNext"
"moduleResolution": "NodeNext"
-> requires .js extensions in imports
with a bundler "module": "ESNext"
"moduleResolution": "Bundler"
-> extensions optionalFound 4,182 errors in 291 files.A WORKING BASE
"target": "ES2022"
"module": "NodeNext", "moduleResolution": "NodeNext"
"lib": ["ES2022"] + "DOM", "DOM.Iterable" for browsers
"strict": true
"rootDir": "src", "outDir": "dist"
"sourceMap": true
"skipLibCheck": true
"forceConsistentCasingInFileNames": true
"include": ["src/**/*"]
WHAT strict TURNS ON
strictNullChecks absence is part of the type
noImplicitAny untyped parameters are an error
useUnknownInCatchVariables a caught error is unknown
strictFunctionTypes, strictBindCallApply,
strictPropertyInitialization, noImplicitThis, alwaysStrict
WORTH ADDING BEYOND strict
noUncheckedIndexedAccess arr[0] is T | undefined <- the big one
noImplicitOverride renaming a parent method errors
exactOptionalPropertyTypes absent vs explicitly undefined
noUnusedLocals/Parameters or leave these to the linter
THE THREE THAT CONFUSE
target which output language version to emit
lib what the checker believes EXISTS
module + moduleResolution how imports resolve
NodeNext + NodeNext -> .js extensions REQUIRED in imports
ESNext + Bundler -> extensions optional
the wrong pair = "works in my editor, fails when I run it"
OUTPUT
sourceMap: true production traces point at YOUR source
declaration: true .d.ts for a library; off for an app
tsc --noEmit check only - this is the CI command
ADOPTING ON AN EXISTING PROJECT
never all at once: 4,000 errors gets reverted, not fixed
noImplicitAny -> strictNullChecks -> the rest
Project References let one part be strict while another catches up
never @ts-nocheck - it turns everything off and stays forever
FILES
exclude does NOT beat an import from an included file
exclude "**/*.test.ts" or you ship your tests
forceConsistentCasingInFileNames: your Mac ignores case, Linux
does not"noUnusedLocals": true,
"noUnusedParameters": true"lib": ["ES2022", "DOM", "DOM.Iterable"]"rootDir": "src",
"outDir": "dist",
"sourceMap": true,
"declaration": truetsc --noEmit{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true
}
}{
"compilerOptions": {
"noImplicitAny": true
}
}"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]"exclude": ["node_modules", "dist", "**/*.test.ts"]