---
title: 'Compiler Configuration'
source: 'https://academia.sh/en/courses/typescript/compiler-configuration'
course: TypeScript
language: en
updated: '2026-08-17T18:09:55+00:00'
license: 'CC BY-SA 4.0'
---

# 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.

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`:

```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:

```json
{
    "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:

```typescript
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`:

```text
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:

```typescript
class MeasurementLog {
  name: string;
  records: number[] = [];
}

try {
  throw new Error("record could not be read");
} catch (error) {
  console.log(error.message);
}
```

```text
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:

```typescript
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:

```text
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:

```typescript
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:

```text
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.
