Skip to content
academia.sh

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 --showConfig output gives the version completed with defaults and the resolved file list.
  • strict is an umbrella; with it off, null fits every type and untyped parameters go unchecked.
  • The umbrella’s contents can grow across versions; a compiler upgrade bringing new diagnostics under strict is expected behavior.
  • noUncheckedIndexedAccess adds undefined to indexed access, exactOptionalPropertyTypes separates a field’s absence from an undefined value; both are outside the umbrella.
  • The target, lib, and module settings 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.

Start typing to search.

↑↓ Esc navigate · open · close