Lesson 19 / 23
Compiler Configuration
The structure of the configuration file, the strictness umbrella and its sub-flags, additional strictness options outside the umbrella, and target and library settings.
Contents
Up to here, every example ran as a single file with options given by hand. In a real project, options are gathered into a configuration file.
This file determines which files the compiler reads, how strict it behaves, and what it produces. Strictness level is not a preference, it is a decision about which error classes get caught; this lesson builds that decision with examples.
The Structure of the Configuration File
The configuration file sits at the project root under the name tsconfig.json:
{ "compilerOptions": { "target": "es2022", "module": "nodenext", "moduleResolution": "nodenext", "strict": true, "noUncheckedIndexedAccess": true, "exactOptionalPropertyTypes": true, "noImplicitOverride": true, "noUnusedLocals": true, "noEmit": true }, "include": ["src"] }
There are two sections. compilerOptions determines the compiler’s behavior,
include determines which files belong to the project. Subsets can be excluded with
exclude; another configuration file can be used as a base with extends.
What settings the compiler actually runs with can be asked. In the project directory,
tsc --showConfig writes the configuration completed with defaults:
{ "compilerOptions": { "exactOptionalPropertyTypes": true, "module": "nodenext", "moduleResolution": "nodenext", "noEmit": true, "noUncheckedIndexedAccess": true, "noUnusedLocals": true, "noImplicitOverride": true, "strict": true, "target": "es2022", "moduleDetection": "force" }, "files": [ "./src/measurement.ts" ], "include": [ "src" ] }
Two things are visible in the output. The files list is the resolved form of the
include pattern — which files are actually checked is confirmed here. The
moduleDetection option, meanwhile, has been added even though it was never written;
these are defaults filled in by the compiler and can vary by version. When
investigating where a behavior comes from, this output is the first place to look.
The Strictness Umbrella
strict is not a single option, it is an umbrella that turns on a group of
options together. Its effect is seen by compiling the same file with two settings:
interface Measurement { id: string; value: number; } function average(records) { return records.length; } const record: Measurement = null; console.log(average([record]), record.value);
Compiled with "strict": false, the compiler produces no diagnostic at all. With
"strict": true:
src/loose.ts(6,18): error TS7006: Parameter 'records' implicitly has an 'any' type. src/loose.ts(10,7): error TS2322: Type 'null' is not assignable to type 'Measurement'.
Same source, same compiler, two different results. With strictness off, the type
layer only checks types that were written; null fits into every type, untyped
parameters go unchecked. This is a consequence of the “gradual addability” design
mentioned in the first lesson, and it is meaningful when migrating an existing
JavaScript codebase. Leaving it off in a new project means giving up most of the
guarantee the type layer provides.
The main options under the umbrella and what they catch:
| Option | Catches |
|---|---|
noImplicitAny |
Parameters with no written and no inferable type |
strictNullChecks |
Silent use of null and undefined values |
strictFunctionTypes |
Violation of the direction rule in function parameters |
strictBindCallApply |
Wrong arguments in call, apply, and bind calls |
strictPropertyInitialization |
Class fields not given a value in the constructor |
noImplicitThis |
An ambiguously typed this context |
useUnknownInCatchVariables |
Assuming the type of a caught error |
alwaysStrict |
Output not produced in strict mode |
This list can grow across versions: with strict on, a compiler upgrade can bring
new diagnostics. This is wanted — new checks make visible errors that could not be
caught before.
The effect of the last two lines is seen in a single example:
class MeasurementLog { name: string; records: number[] = []; } try { throw new Error("record could not be read"); } catch (error) { console.log(error.message); }
src/t1.ts(2,3): error TS2564: Property 'name' has no initializer and is not definitely assigned in the constructor. src/t1.ts(9,15): error TS18046: 'error' is of type 'unknown'.
The second diagnostic recalls an important rule: in JavaScript, a throw statement
can throw any value, so the caught value cannot be assumed to be an Error. Under
strict configuration its type is unknown, and the rule from the any, unknown,
and never lesson applies — it cannot be used without narrowing.
The fixed form:
class MeasurementLog { records: number[] = []; constructor(readonly name: string) {} } try { throw new Error("record could not be read"); } catch (error) { console.log(error instanceof Error ? error.message : String(error)); } console.log(new MeasurementLog("boiler-2").name);
Output:
record could not be read boiler-2
Strictness Outside the Umbrella
Some strictness options are not inside strict, because they produce a large number
of diagnostics in existing code. Among these are the two that close a gap mentioned
twice already in the course:
interface Measurement { id: string; value: number; note?: string; } const records: Measurement[] = [{ id: "s-01", value: 21.4 }]; const first = records[0]; console.log(first.value); const patch: Measurement = { id: "s-02", value: 22.1, note: undefined };
Compiled with the configuration above:
src/idx.ts(10,13): error TS18048: 'first' is possibly 'undefined'.
src/idx.ts(12,7): error TS2375: Type '{ id: string; value: number; note: undefined; }' is not assignable to type 'Measurement' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties.
Types of property 'note' are incompatible.
Type 'undefined' is not assignable to type 'string'.
src/idx.ts(12,7): error TS6133: 'patch' is declared but its value is never read.
Three separate options produced three diagnostics.
noUncheckedIndexedAccess adds undefined to the result of an indexed access.
This is the gap pointed out in the Interfaces lesson: the expression records[0]
gives undefined on an empty array, and the type now records this. The cost is that a
check is needed after every indexed access; the gain is that places where the array
can be empty become visible.
exactOptionalPropertyTypes separates an optional field into “absent” and “value
is undefined.” With this setting, the declaration note?: string says the field
may not be present; explicitly writing undefined to the field is a separate
thing and is rejected. Separating the two matters in JSON serialization — an absent
field does not appear in the output, nor does an undefined-valued field, but after
parsing the two are indistinguishable.
noUnusedLocals reports unused local declarations. It overlaps with the job of
static analysis tools; which one is responsible is a project decision.
These three options can be turned on from the start in new projects. Adding them to an existing project may produce a high diagnostic count; the gradual-adoption strategy is covered in the JavaScript Interop lesson.
Target and Library
Three options determine which environment the code will run in.
target determines the language level of the produced JavaScript. When the value
is lowered, the compiler replaces syntax not present in the target with an equivalent
— class fields, async functions, and optional chaining are transformed. This is a
separate operation from the type erasure principle established in the first lesson:
types are erased at every target, but syntax transformation depends on the target.
lib determines which built-in type declarations are present. The target value
sets the default, but it can also be written separately. If browser interfaces are
used, DOM declarations are added; on the server side they are not needed.
module and moduleResolution determine how modules are produced and found.
This is the subject of the Module Resolution lesson.
What these three settings have in common is that they belong to the output and the environment, not the type layer. Strictness flags say which errors get caught; these three say where the code will run.
Summary
- The configuration file determines compiler options and project scope;
tsc --showConfigoutput gives the version completed with defaults and the resolved file list. strictis an umbrella; with it off,nullfits every type and untyped parameters go unchecked.- The umbrella’s contents can grow across versions; a compiler upgrade bringing new
diagnostics under
strictis expected behavior. noUncheckedIndexedAccessaddsundefinedto indexed access,exactOptionalPropertyTypesseparates a field’s absence from anundefinedvalue; both are outside the umbrella.- The
target,lib, andmodulesettings belong to the output and the environment the code runs in, not the type layer.
Next Step
Configuration makes it possible to check the project’s own files. But how is code coming from outside — a library with no type information, the global objects the runtime provides — typed? The next lesson covers declaration files, ambient declarations, and carrying type definitions separately from implementation.
To keep your progress and take notes, Log in
My notes
Log in to take notes.