---
title: TypeScript
source: 'https://academia.sh/en/courses/typescript'
language: en
updated: '2026-08-17T04:41:41+00:00'
license: 'CC BY-SA 4.0'
lessons: 23
outcomes:
    - 'Writes safe code using type inference and narrowing'
    - 'Defines reusable types with generics and conditional types'
    - 'Chooses the strictness level of compiler options deliberately'
---

# TypeScript

The static type layer built on top of JavaScript: type inference, narrowing, unions and intersections, generics, conditional and mapped types, declaration files, and compiler strictness options.

## Type System Fundamentals

The guarantee the static type layer provides, basic types, escape hatches, type inference, union and intersection types, and const types.

### The Purpose of TypeScript

Source: https://academia.sh/en/courses/typescript/purpose-of-typescript

The error class the static type layer catches, the erasure of types at compile time, and the distinction between type checking and runtime validation.

The Modules, Tooling and the Ecosystem course covered adapting source code to a target
environment, and how static analysis tools work with a rule set. All these tools ask
one shared question: what can be said about a program without running it? TypeScript
gives the most ambitious answer — it computes which set of values every expression in
the program can produce, and reports inconsistencies.

This lesson establishes three things: which error class the type layer catches, what
becomes of types at runtime, and what type checking **does not promise**. The third is
as important as the first two; the course returns to this distinction again and again
in later sections.

A single concrete example runs through the whole course: a **measurement record**
model. The model starts as an untyped JavaScript object and becomes one step safer in
every topic.

#### Anatomy of a Silent Error

Start with a function that computes the average of measurement records. It uses
nothing beyond the tools built in the JavaScript Fundamentals course:

```javascript
function average(measurements) {
  let total = 0;
  for (const m of measurements) {
    total += m.value;
  }
  return total / measurements.length;
}

const records = [{ value: 21.4 }, { value: 22.1 }, { value: "23.0" }];
console.log(average(records));
```

Run, the output is `NaN`. The program does not crash, throws no error message, it
produces a wrong number. The cause was established in the Type Conversion lesson of the
Programming Fundamentals course: the third record's `value` field is not a number but
a string; when `+` sees one side is a string, it concatenates instead of adding. The
expression `43.5 + "23.0"` gives the string `"43.523.0"`, and dividing that string by
three also gives `NaN`.

This is a typical example of the logic-error class: the program runs, throws no error,
and gives a wrong result. There is a distance between the error's source and its
symptom — the record was perhaps read from a file hours earlier, and the `NaN` only
shows up at the end of a report.

#### What the Type Layer Does

Build the same program again, this time writing out explicitly which set each value
belongs to:

```typescript
type Measurement = { value: number };

function average(measurements: Measurement[]): number {
  let total = 0;
  for (const m of measurements) {
    total += m.value;
  }
  return total / measurements.length;
}

const records: Measurement[] = [{ value: 21.4 }, { value: 22.1 }, { value: "23.0" }];
console.log(average(records));
```

The notations `: Measurement[]` and `: number` are called a **type annotation**. When
the compiler checks this file, it produces this diagnostic:

```text
a1.ts(11,69): error TS2322: Type 'string' is not assignable to type 'number'.
```

The diagnostic shows not where the error happens, but **where it starts**: not the
division that produces `NaN`, but the line and column where the string was placed
where a number was expected. The error is caught before it turns into a symptom.

The diagnostics in this course were produced with compiler version 6.0. Message text can
be rewritten from version to version; **error codes** like `TS2322` are more stable.
Searching for a diagnostic by code is more reliable than searching by text.

#### Types Are Erased at Compile Time

Now for the course's most important distinction. TypeScript source does not run
directly; it is translated to JavaScript first. What happens to type annotations in
that translation?

Let the following file be saved as `measurement.ts`:

```typescript
type Measurement = { id: string; value: number };

function average(measurements: Measurement[]): number {
  let total: number = 0;
  for (const m of measurements) {
    total += m.value;
  }
  return total / measurements.length;
}

const records: Measurement[] = [
  { id: "s-01", value: 21.4 },
  { id: "s-02", value: 22.1 },
];
console.log(average(records));
```

When the compiler is invoked with `tsc --target es2022 measurement.ts`, it writes a
`measurement.js` file next to it:

```javascript
"use strict";
function average(measurements) {
    let total = 0;
    for (const m of measurements) {
        total += m.value;
    }
    return total / measurements.length;
}
const records = [
    { id: "s-01", value: 21.4 },
    { id: "s-02", value: 22.1 },
];
console.log(average(records));
```

The output is the source with its type annotations stripped. The `type Measurement`
declaration is entirely gone; the `: Measurement[]` and `: number` notations are
removed. The only line the compiler added is `"use strict";`, and even that has
nothing to do with types. Running `node measurement.js` gives `21.75`.

This is called **type erasure**. Its consequence: **the type layer does not change
runtime behavior.** The typed and untyped versions of the same program, absent a type
error, do exactly the same work bit for bit. A type system is not a library, it is a
**layer of proof**: reasoning carried out on the source code that leaves nothing
behind once translation is done.

A direct consequence of this distinction is that a type error does not by itself stop
emission:

```typescript
// record.ts
type Measurement = { id: string; value: number };
const record: Measurement = { id: "s-01", value: "21.4" };
```

```text
record.ts(3,43): error TS2322: Type 'string' is not assignable to type 'number'.
```

The compilation that produces this diagnostic still writes a `record.js` file, and
that file runs. The compiler says "this program's types are inconsistent," not "this
program will not run." Whether an error blocks emission is a separate configuration
decision and will be covered in the Build and Run Flow lesson.

#### Type Checking Is Not Runtime Validation

Type erasure also explains a common false expectation. Writing a value's type does not
**guarantee** it will actually be that type at runtime. The guarantee only holds for
the piece of code the compiler saw, and only if the information there was correct.

Information may not be correct once a measurement record comes from a file or the
network:

```typescript
type Measurement = { id: string; value: number };

const raw = '{"id":"s-01","value":"23.0"}';
const measurement = JSON.parse(raw) as Measurement;

console.log(typeof measurement.value);
try {
  console.log(measurement.value.toFixed(1));
} catch (e) {
  console.log((e as Error).message);
}
```

This file **passes type checking without error**. When run, however, a `TypeError`
occurs and the `catch` block prints its message:

```text
string
measurement.value.toFixed is not a function
```

The return type of the `JSON.parse` call is `any`; the `as Measurement` notation tells
the compiler "treat this value as `Measurement`." The compiler does not question this
declaration, because it has no tool to question it with: the content of the string is
only known once the program runs.

Two rules follow from this and hold throughout the course:

1. **Validation happens at the system boundary.** Data coming from a file, the
   network, user input, or a database is accepted not by declaring its type, but by
   checking it field by field.
2. **A type assertion is not proof, it is a debt.** The `as` notation silences the
   checker; the programmer is responsible for the correctness of what was silenced.

The tools for writing boundary validation will be built in later lessons of this
course: the `unknown` type gives the correct starting point, and type predicates feed
the result of validation back into the type system.

#### What the Type System Proves

A type system proves properties of the program verifiable **without running** it.
What TypeScript proves is roughly this: every expression's value is within the set
expected of it. This proof eliminates an entire error class — access to a nonexistent
field, a call with a missing argument, assignment of the wrong type, an unhandled case
in a union type.

What it does not prove is just as clear:

- **Value range.** The declaration `value: number` does not say the temperature is
  within a physically possible range.
- **Termination and correctness.** That a function computes the average correctly
  cannot be expressed at the type level; this is the domain of the partial correctness
  and termination concepts from the Algorithms course.
- **The shape of the outside world.** As seen in the example above, what lies beyond
  the boundary is not checked.

TypeScript's type system is also deliberately **not fully sound**: `any`, type
assertions, and a few language features leave escape hatches that pierce the
guarantee. This is a design goal, not an oversight — the language has to be addable,
gradually, on top of existing JavaScript code. The escape hatches are covered one by
one in the `any`, `unknown`, and `never` lesson.

#### The Course's Shared Example

The same model will be worked on throughout the course. The starting point is this
object, carrying no guarantees at all:

```javascript
const measurement = {
  id: "s-01",
  sensor: "temperature",
  value: 21.4,
  unit: "C",
  time: 1706000000000,
};
```

Nothing is known about this object yet: the `unit` field could hold any string at all,
the `value` field could be a string, the `sensor` field could be deleted. Every topic
closes one of these uncertainties — first the fields' types, then the relations
between fields, then the states the record passes through, and finally how the model
gets published at the project boundary.

#### Summary

- The static type layer computes every expression's value set without running the
  program, and reports inconsistencies at compile time.
- Diagnostic messages can change with version; error codes like `TS2322` are a more
  stable reference.
- Types are erased at compile time: the compiled JavaScript output is the source with
  its type annotations removed, and runtime behavior is unchanged.
- A type error does not by itself stop emission; the compiler still writes the
  JavaScript file even when it produces a diagnostic.
- Declaring a type is not runtime validation; data from a system boundary has to be
  checked field by field.
- The type system does not prove value range, algorithm correctness, or the shape of
  the outside world.

#### Next Step

What the type layer does is now established; next comes this layer's vocabulary. To
write the measurement record's fields, the basic types available have to be known
first: primitive types, array and tuple notations, and the `enum` construct that names
a fixed set of values. The next lesson builds these and writes the model's first typed
version.

### Basic Types

Source: https://academia.sh/en/courses/typescript/basic-types

The first typed version of the measurement record, using primitive types, array and readonly array notations, tuples, enums, and object types.

The previous lesson established what the type layer does and does not do. Next comes
this layer's vocabulary: to write the measurement record's fields, the types available
first have to be known.

TypeScript's type vocabulary sits on top of JavaScript's value model. It adds no new
value kind; it names the kinds that already exist and gives ways to build compound
types from them. This lesson introduces primitive types, arrays, tuples, and enums,
and closes by writing the measurement record's first typed version.

#### Primitive Types

Every primitive value introduced in the JavaScript Fundamentals course has a type name.
Type names are written in lowercase:

```typescript
const id: string = "s-01";
const value: number = 21.4;
const valid: boolean = true;
const description: string | null = null;
const bigCounter: bigint = 9007199254740993n;

console.log(typeof id, typeof value, typeof valid, typeof description, typeof bigCounter);
console.log(0.1 + 0.2 === 0.3);
console.log(Number.isInteger(value), Number.isSafeInteger(9007199254740993));
```

Output:

```text
string number boolean object bigint
false
false false
```

Three observations are needed.

**`number` is a single number type.** There is no split between integer and real
number; both are held in the IEEE 754 double-precision representation. The limits
established in the Floating Point Numbers lesson of the How Computers Work course hold
here exactly as they are — the `false` on the second line is a direct consequence.
The type system does not remove this limit, it only guarantees that a number is a
number.

**`typeof null` is `"object"`.** This is an old inconsistency the language carries;
the type system recognizes a separate type for `null` but does not change the runtime
behavior. It is an example showing that types are erased: the `null` type the compiler
sees and the value `typeof` sees come from the same place but do not say the same
thing.

**`bigint` and `symbol` are separate types.** `bigint` is used where the safe-integer
limit of the number type is exceeded, `symbol` for unique property keys. Neither can be
confused with `number`; `bigint` and `number` cannot be added directly.

`null` and `undefined` are two separate types and, under the default strict
configuration, cannot be assigned to other types on their own. This is why `description`
is declared above as `string | null`; had it been declared as just `string`, the
compiler would reject the assignment. This behavior depends on the `strictNullChecks`
option and will be covered in detail in the Compiler Configuration lesson.

#### Arrays and Readonly Arrays

An array's type is written by appending `[]` to the element type. An equivalent
notation also exists:

```typescript
const values: number[] = [21.4, 22.1, 23.0];
const ids: Array<string> = ["s-01", "s-02"];
const readonlyValues: readonly number[] = [21.4, 22.1];

console.log(values.length, ids.length, readonlyValues.length);
readonlyValues.push(23.0);
```

The compiler produces this diagnostic for the last line:

```text
b2.ts(6,16): error TS2339: Property 'push' does not exist on type 'readonly number[]'.
```

`number[]` and `Array<number>` are the same type; the first is the short notation.
`readonly number[]` is a different type: an array whose elements can be read but not
changed. Mutating methods (`push`, `pop`, `sort`, `splice`) are not defined on this
type, so calling them produces a "no such property" diagnostic.

The `readonly` here is the type-level counterpart of the **immutable** concept from
the Programming Fundamentals course — but only at the type level. Nothing called
`readonly` survives in compiled output; the array is an ordinary array at runtime, and
code that has not passed type checking can mutate it. Real immutability requires
runtime tools like `Object.freeze`.

#### Tuples

A **tuple** is an array whose element count and each position's type are fixed. It
suits representing the measurement record's valid value range:

```typescript
type Interval = [low: number, high: number];

const validInterval: Interval = [-40, 85];
const [low, high] = validInterval;
console.log(low, high);

const invalid: Interval = [-40, 85, 120];
```

The diagnostic for the last line:

```text
b3.ts(7,7): error TS2322: Type '[number, number, number]' is not assignable to type 'Interval'.
  Source has 3 element(s) but target allows only 2.
```

The names `low:` and `high:` document the tuple's elements; they have no runtime
counterpart, and the array is still read by position. Their value is readability:
`[number, number]` does not say which number comes first, `[low: number, high:
number]` does.

The distinction between a tuple and an array is this: an array does not know **how
many** elements there are, it knows they are all the **same** type; a tuple knows how
many elements there are and knows the type of **each one separately**.

#### Fixed Value Sets: `enum`

The measurement record's `unit` field can only take certain values. One way to name
such a set is an **enum**:

```typescript
enum Unit {
  Celsius = "C",
  Pascal = "Pa",
  Percent = "%",
}

const unit: Unit = Unit.Celsius;
console.log(unit, Unit.Pascal);
```

When this file is compiled with `tsc --target es2022 --strict unit.ts`, the JavaScript
produced is:

```javascript
"use strict";
var Unit;
(function (Unit) {
    Unit["Celsius"] = "C";
    Unit["Pascal"] = "Pa";
    Unit["Percent"] = "%";
})(Unit || (Unit = {}));
const unit = Unit.Celsius;
console.log(unit, Unit.Pascal);
```

Run, the output is `C Pa`.

This output shows an exception to the previous lesson's rule: **an enum is not
erased.** The compiler produces an object that exists at runtime. Unlike a type
annotation, `enum` is not a type, it is a declaration producing both a type and a
value.

A numeric enum makes the produced object do even more:

```javascript
"use strict";
var Status;
(function (Status) {
    Status[Status["Valid"] = 0] = "Valid";
    Status[Status["Suspect"] = 1] = "Suspect";
    Status[Status["Invalid"] = 2] = "Invalid";
})(Status || (Status = {}));
```

This is a two-way mapping giving `1` for `Status.Suspect` and `"Suspect"` for `Status[1]`. It is
a convenience, but it has a cost: every enum adds code to the bundle output, and cannot be
produced correctly when a file is compiled on its own (without type information). For this
reason the compiler has an option that allows only erasable syntax: `erasableSyntaxOnly`. With
that option on, an `enum` declaration is rejected. Let the configuration be this:

```json
// tsconfig.json
{
  "compilerOptions": {
    "target": "es2022",
    "module": "nodenext",
    "strict": true,
    "erasableSyntaxOnly": true,
    "noEmit": true
  }
}
```

And let `src/status.ts` hold a numeric enum:

```typescript
// src/status.ts
enum Status {
  Valid,
  Suspect,
  Invalid,
}
```

The compiler rejects it with code `TS1294`, pointing at the declaration's name:

```text
src/status.ts(2,6): error TS1294: This syntax is not allowed when 'erasableSyntaxOnly' is enabled.
```

Which version the option is available from varies by compiler; its behavior is fixed
— it rejects all TypeScript syntax that produces runtime code.

#### The Union Alternative

The same constraint can also be expressed in a way that leaves no runtime trace at
all:

```typescript
type Unit = "C" | "Pa" | "%";

const unit: Unit = "C";
console.log(unit);

const invalidUnit: Unit = "F";
```

The diagnostic for the last line:

```text
b4.ts(6,7): error TS2322: Type '"F"' is not assignable to type 'Unit'.
```

The `"C"` here is not a string, it is a **type**: a single-element set containing only
the value `"C"`. Such types are called a **literal type** and will be covered in a
separate lesson. The union of three literal types defines a three-valued set.

Comparison of the two approaches:

| Criterion | `enum` | Union of literal types |
|---|---|---|
| Runtime code | Produces it | Does not produce it |
| Accessing a value | `Unit.Celsius` | `"C"` |
| Matching external data | Requires conversion | Direct |
| Enumerating all values | Via the object | Requires a separate array |

This course will use the union of literal types, since it does not break the type
erasure principle.

#### The Measurement Record's First Typed Version

With the tools gathered, the model's first typed version can be written. An object
type lists field names and types inside curly braces:

```typescript
type Measurement = {
  id: string;
  sensor: string;
  value: number;
  unit: "C" | "Pa" | "%";
  time: number;
};

const measurement: Measurement = {
  id: "s-01",
  sensor: "temperature",
  value: 21.4,
  unit: "C",
  time: 1706000000000,
};

console.log(measurement.id, measurement.value, measurement.unit);
console.log(measurement.location);
```

The diagnostic for the last line:

```text
b5.ts(18,25): error TS2339: Property 'location' does not exist on type 'Measurement'.
```

The model now knows three things: which fields exist, each field's type, and the
`unit` field's value set. What it does not know is also visible: the `sensor` field
can hold any string, there is no range for the `value` field, and it is unclear
whether the `time` field is a timestamp or a duration. These gaps will be closed in
later topics.

#### Summary

- Primitive types name JavaScript's value model; `number` is a single number type and
  carries the limits of floating-point representation.
- `T[]` and `Array<T>` are the same type; `readonly T[]` is a separate type that does
  not include mutating methods and only holds at compile time.
- A tuple is an array whose element count and each position's type are fixed; element
  names are for documentation.
- An `enum` declaration produces runtime code and is the exception to the type erasure
  principle; a numeric enum also produces a two-way mapping.
- A union of literal types expresses the same constraint without leaving a runtime
  trace.

#### Next Step

The measurement record is now typed, but data coming from the outside world has not
entered this type yet. What is the type of the value `JSON.parse` returns, and how is
that value safely converted to the model? The next lesson covers the type system's two
endpoints — `any`, which allows everything, and `unknown`, which allows nothing — and
the `never` type, which has no values at all.

### any, unknown, and never

Source: https://academia.sh/en/courses/typescript/any-unknown-never

The assignability rules and use sites of any, which turns off checking, unknown, which forces it, and never, which has no values.

The previous lesson wrote the measurement record's first typed version, but the record
was still a hand-written object. Real data comes from a file or the network and comes
out of a `JSON.parse` call. What type is that call's return value?

The answer introduces the type system's two endpoints. The `any` type says "this could
be anything, do not check"; the `unknown` type says "this could be anything, so check
first." The difference between the two is one of the most important decisions for a
record's safety. At the end of the lesson comes a third endpoint: the `never` type,
which has no values at all.

#### `any`: Turning Off Checking

`any` tells the type checker to assert nothing about a value:

```typescript
const raw: any = JSON.parse('{"id":"s-01","value":"23.0"}');

try {
  console.log(raw.value.toFixed(1));
} catch (e) {
  console.log((e as Error).message);
}
try {
  console.log(raw.missing.field);
} catch (e) {
  console.log((e as Error).message);
}
const num: number = raw;
```

```text
raw.value.toFixed is not a function
Cannot read properties of undefined (reading 'field')
```

This file **passes type checking without error**; when run, the first two lines throw. All
three lines are wrong: the `value` field is a string, there is no field called `missing`,
and `raw` is not a number. The compiler reports none of them, because it has no assertion
about which fields an `any` value has.

`any` behaves in both directions: a value of type `any` can be assigned to any type,
and any type can be assigned to `any`. This takes the type out of the checking
network. Once a field is `any`, every expression derived from it also stays unchecked
— the lack of checking spreads.

The declared return type of the `JSON.parse` function is `any`. The reason is clear:
the content of the string is only known at runtime, the compiler has nothing to say.
The result resembles what was seen in the first lesson — type checking falls silent,
the error is deferred to runtime.

The compiler can also refuse to infer `any` when no type is written:

```typescript
function average(measurements) {
  return measurements.length;
}
console.log(average([1, 2, 3]));
```

```text
c6.ts(1,18): error TS7006: Parameter 'measurements' implicitly has an 'any' type.
```

This diagnostic comes from the `noImplicitAny` option and is part of the strict
configuration. The distinction has to be seen: **implicit** `any` is an omission, the
compiler warns; **explicit** `any` is a decision, the compiler stays silent. Searching
a codebase for `any` notations means searching for where checking was turned off.

#### `unknown`: Deferring Checking

`unknown` covers the same value set as `any` — every value can be assigned to type
`unknown` — but the reverse direction is closed:

```typescript
const raw: unknown = JSON.parse('{"id":"s-01","value":"23.0"}');

console.log(raw.value);
const num: number = raw;
```

```text
c2.ts(3,13): error TS18046: 'raw' is of type 'unknown'.
c2.ts(4,7): error TS2322: Type 'unknown' is not assignable to type 'number'.
```

**Nothing can be done** with a value of type `unknown`: its fields cannot be
accessed, it cannot be called, it cannot be assigned to another type. The only thing
that can be done is narrowing its type.

The relationship between the two can be described with an ordering. In the direction
of assignability, `unknown` sits at the top: every type is assignable to it, it is
assignable to none. This is why it is called the **top type**. `any` sits outside
this ordering; assignable in both directions, and precisely for that reason it
carries no guarantee.

Practical rule: **`unknown` is used at the system boundary.** External data is
received as `unknown`, checked, and only enters the model after that check.

#### Validation at the Boundary

Reading the measurement record from a file is an application of this rule:

```typescript
type Measurement = { id: string; value: number };

function read(raw: string): Measurement | null {
  const parsed: unknown = JSON.parse(raw);
  if (
    typeof parsed === "object" &&
    parsed !== null &&
    "id" in parsed &&
    typeof parsed.id === "string" &&
    "value" in parsed &&
    typeof parsed.value === "number"
  ) {
    return { id: parsed.id, value: parsed.value };
  }
  return null;
}

console.log(read('{"id":"s-01","value":21.4}'));
console.log(read('{"id":"s-01","value":"23.0"}'));
console.log(read('42'));
```

Output:

```text
{ id: 's-01', value: 21.4 }
null
null
```

Every part of the condition does one step of narrowing. After `typeof parsed ===
"object"`, the compiler knows the value is an object; `parsed !== null` eliminates the
possibility of `null`; `"id" in parsed` proves that field exists; the `typeof` check
determines the field's type. By the time the body of the condition is entered, the
compiler has **proven** that `parsed.id` is a `string` — not on the programmer's word,
but based on the checks in the code.

This is the course's axis: the type system is a layer of proof, and checks are the
steps of the proof. The rules of narrowing and less repetitive ways to write it are
covered in the Type Narrowing lesson.

The same job could also have been done by writing `as Measurement`, and it would have
misbehaved as seen in the first lesson. The difference is this: with `as`, the
guarantee comes from the programmer's assertion; in the code above, it comes from
checks that actually run.

#### `never`: The Empty Type

`never` is the type with no values. It is the empty set. It shows up in two places.

First, as the return type of functions that never return by the normal path:

```typescript
function halt(message: string): never {
  throw new Error(message);
}

const anything: string = halt("record could not be read");
const nothing: never = "s-01";
console.log(anything, nothing);
```

```text
c5.ts(6,7): error TS2322: Type '"s-01"' is not assignable to type 'never'.
```

The fifth line does not error, the sixth does. The reason is `never`'s place in the
assignability ordering: `never` can be assigned to every type, no type can be assigned
to `never` — much like the empty set being a subset of every set. This is why `never`
is called the **bottom type**. Because the `halt` function returns no value at all,
claiming that the value it returns has to be `string` is a vacuous claim, and the
compiler allows it.

The second appearance is more useful: **exhaustiveness checking**. Let the measurement
record's states be defined with a union:

```typescript
type Status = "valid" | "suspect" | "invalid";

function label(status: Status): string {
  switch (status) {
    case "valid":
      return "measurement valid";
    case "suspect":
      return "measurement suspect";
    default: {
      const unhandled: never = status;
      return unhandled;
    }
  }
}

console.log(label("valid"));
```

```text
c4.ts(10,13): error TS2322: Type '"invalid"' is not assignable to type 'never'.
```

The diagnostic names the unhandled case directly. The mechanism is this: after the two
`case` branches, the compiler knows only `"invalid"` can reach the `default` branch.
Attempting to assign to `never` tests whether the remaining set is empty. If the set is
not empty, a diagnostic appears, and the value inside it is written into the
diagnostic's text.

This pattern's value shows up when a new case is added to the union. If a fourth value
is added to the `Status` type, **every** `switch` block that does not handle that value
errors at compile time. The type system becomes a maintenance tool here: changing the
model forces a review of all code that uses it.

#### Where the Three Types Sit

The relationship among the three can be gathered into a single table:

| Type | What can be assigned to it | Where an assignment from it can go | Where used |
|---|---|---|---|
| `any` | Every type | Every type | Temporary, during gradual migration |
| `unknown` | Every type | Nowhere (narrowing required) | System boundary |
| `never` | Nowhere | Every type | Exhaustiveness checking, non-returning functions |

The table's first row shows why `any` is not a type but a gate: it is the only row
with both columns filled, and that means it imposes no constraint at all. `unknown`
and `never` are each other's opposite — one covers everything and allows nothing, the
other covers nothing and goes everywhere.

`any` is not banned outright. As will be seen in the JavaScript Interop lesson, it is
temporarily necessary during gradual migration of a codebase with no type
information. The rule is: `any` is a **transition** marker, not a **solution**; where
it remains is documented and narrowed.

#### Summary

- `any` turns off type checking; being assignable in both directions, it carries no
  guarantee and spreads the lack of checking.
- Implicit `any` is an omission and is reported by the `noImplicitAny` option; explicit
  `any` is a decision.
- `unknown` is the top type: every value is assignable to it, it is assignable to no
  type; it has to be narrowed before use.
- Data from a system boundary is received as `unknown` and enters the model only after
  being checked field by field; the `as` notation does not substitute for this check.
- `never` is the bottom type: it has no values, it is assignable to every type; it is
  used for exhaustiveness checking in `switch` blocks.

#### Next Step

Up to here, types were written by hand. Yet in most of the examples above, the
compiler already knew types that were never written: the narrowed types inside `read`,
the object literal's field types, the type of array elements. The next lesson covers
where this knowledge comes from — the rules and limits of type inference.

### Type Inference

Source: https://academia.sh/en/courses/typescript/type-inference

The compiler's rules for inferring types from an initial value, context, and return expressions; widening behavior and where a type annotation is still required.

In the previous lessons' examples, the compiler already knew many types that were
never written: the types of fields after narrowing, the type of array elements, the
parameters of a `reduce` callback. Where did this knowledge come from?

The answer is **type inference**: the compiler computes an expression's type from the
information around it. Inference is what makes TypeScript usable — if every variable
needed a written type, the type layer's cost would exceed its benefit. This lesson
covers the rules of inference, its limits, and the points where a type annotation is
still required.

#### Seeing the Inferred Type

The most direct way to examine inference is to ask the compiler to produce a
**declaration file**. A declaration file writes the types of the names a module
exports; inferred types show up there explicitly.

```typescript
export const unit = "C";
export let variable = "C";
export const value = 21.4;
export const measurement = { id: "s-01", value: 21.4, unit: "C" };
export const values = [21.4, 22.1];
export const mixed = [21.4, "none"];

export function average(measurements: number[]) {
  return measurements.reduce((t, d) => t + d, 0) / measurements.length;
}
```

Saved under the name `inference.ts` and compiled with
`tsc --strict --target es2022 --declaration --emitDeclarationOnly inference.ts`, an
`inference.d.ts` is written next to it:

```typescript
export declare const unit = "C";
export declare let variable: string;
export declare const value = 21.4;
export declare const measurement: {
    id: string;
    value: number;
    unit: string;
};
export declare const values: number[];
export declare const mixed: (string | number)[];
export declare function average(measurements: number[]): number;
```

This file shows six rules of inference in a single glance. They are taken in order below.

#### Inference From the Initial Value, and Widening

The type of constant `unit` is not `string` but `"C"` — a set containing only that one
value. The type of constant `value` is likewise not `number` but `21.4`. The
declaration of `variable`, by contrast, has its type **widened** to `string`.

The rule is this: a declaration whose value can change cannot hold a literal type,
because the declaration can later take another value of the same type. After the
declaration `let variable = "C"`, the statement `variable = "Pa"` can be written; if
the type stayed `"C"`, this assignment would be rejected. A declaration whose value
cannot change has no need to widen.

The same reasoning holds for object fields. Although the constant `measurement` is
declared with `const`, the `unit` field's type is inferred as `string`, not `"C"`. The
reason is the distinction established in the JavaScript Fundamentals course: a `const`
declaration fixes the **binding**, not the object's content. The expression
`measurement.unit = "Pa"` is a valid JavaScript statement, so the field's type is
widened.

This behavior has a direct consequence:

```typescript
type Unit = "C" | "Pa" | "%";
type Measurement = { id: string; value: number; unit: Unit };

const raw = { id: "s-01", value: 21.4, unit: "C" };
const record: Measurement = raw;
console.log(record);
```

```text
d4.ts(5,7): error TS2322: Type '{ id: string; value: number; unit: string; }' is not assignable to type 'Measurement'.
  Types of property 'unit' are incompatible.
    Type 'string' is not assignable to type 'Unit'.
```

The assignment fails because the type of `raw.unit` has widened to `string`, and
`string` does not fit the three-valued `Unit` set. Preventing widening is the subject
of the Const Types and `as const` lesson.

For arrays, inference takes the union of the elements' types: `number[]` for `values`,
`(string | number)[]` for the mixed-element `mixed`. Because an array is mutable too,
element types are widened as well.

#### Contextual Typing

Inference is not one-directional. An expression's type can also come from where the
expression is **expected**. This is called **contextual typing**:

```typescript
const values: number[] = [21.4, 22.1, 23.0];

values.forEach((v) => console.log(v.toFixed(1)));
values.forEach((v) => console.log(v.toUpperCase()));
```

```text
d1.ts(4,37): error TS2339: Property 'toUpperCase' does not exist on type 'number'.
```

No type is written on parameter `v`. The compiler takes the type from what the
`forEach` method's callback expects: since `values` is a `number[]`, the callback's
first parameter is `number`. The third line is valid; the fourth is rejected because
it calls a string method.

Contextual typing also explains why the implicit-`any` check does not trigger on
callbacks: even though the parameter looks untyped, it has a type coming from context.

#### Return Type Inference and Where the Error Lands

Function return types are inferred too. In the declaration file above, the `average`
function's return type shows up as `number` even though it was never written.

Inference's convenience has a cost, and that cost shows up in **where** the error is
reported. First, the version with no return type written:

```typescript
function createMeasurement(id: string, raw: string) {
  return { id, value: raw };
}

const record = createMeasurement("s-01", "21.4");
const rounded: string = record.value.toFixed(1);
console.log(rounded);
```

```text
d2.ts(6,38): error TS2551: Property 'toFixed' does not exist on type 'string'. Did you mean 'fixed'?
```

The error is reported on the sixth line, that is, where the function is **used**. Yet
what is actually wrong is on the second line: a string was placed in the `value`
field. The same function, written together with the model's type:

```typescript
type Measurement = { id: string; value: number };

function createMeasurement(id: string, raw: string): Measurement {
  return { id, value: raw };
}

console.log(createMeasurement("s-01", "21.4"));
```

```text
d3.ts(4,16): error TS2322: Type 'string' is not assignable to type 'number'.
```

The diagnostic is now on the fourth line — at the error's source. The difference looks
trivial for a single function; in a call chain running through dozens of modules, it
determines how long it takes to find the error's source.

A practical rule follows from this: **write types at the boundaries a module exposes,
leave internal detail to inference.** The parameter and return types of a module's
exported functions, a class's public methods, and a module's data types are written by
hand. Local variables, callback parameters, and private helper functions are left to
inference.

The second reason for this rule is that a type annotation is a **contract**. A written
return type tests, when the body changes, whether the contract has been broken. An
inferred return type changes silently along with the body — and the fact that the
contract changed is only noticed once an error appears in the code that uses it.

#### The Limits of Inference

Inference cannot fill every gap. There are three typical situations.

**An accumulator starting from an empty container.** For a local declaration `const
valid = []`, the compiler gathers the element type from later additions. This, again,
means the error is carried far away:

```typescript
type Measurement = { id: string; value: number };

function filter(records: Measurement[]): Measurement[] {
  const valid = [];
  for (const r of records) {
    if (r.value > 0) {
      valid.push(r.id);
    }
  }
  return valid;
}

console.log(filter([{ id: "s-01", value: 21.4 }]));
```

```text
empty2.ts(10,3): error TS2322: Type 'string[]' is not assignable to type 'Measurement[]'.
  Type 'string' is not assignable to type 'Measurement'.
```

What is wrong is the seventh line — the record's id was pushed instead of the record
itself — but the diagnostic appears on the tenth. Had it been written as `const valid:
Measurement[] = []`, the diagnostic would have shown up on the line where the push
happens.

**Data coming from outside.** The return type of a `JSON.parse` call is `any`; there
is no information for inference to use. The rule established in the previous lesson
applies here — the return is received as `unknown` and checked.

**Overly wide inference.** The type inferred from an object literal is usually looser
than intended; the `unit: string` example above is the typical case. When the intended
type is written, the compiler both narrows it and checks that the literal fits that
type.

#### Summary

- Type inference computes an expression's type from its initial value, its context, and
  return expressions; inferred types can be viewed by producing a declaration file.
- Literal types widen in declarations whose value can change; a `const` declaration
  fixes the binding, so its own literal type is kept, but object fields still widen.
- Contextual typing takes an expression's type from where it is expected; callback
  parameters are typed this way.
- When the return type is not written, the error is reported where the function is
  used; when it is written, it is reported at the source.
- Types are written at exposed boundaries, internal detail is left to inference; a
  type annotation is also a contract tested when the body changes.

#### Next Step

The measurement record's `unit` field could take one of three values, and that was
written by combining types that are alternatives to one another. The same operation
also has an intersection direction: values that carry the requirements of two types at
once. The next lesson covers the rules of union and intersection types, which
operations can be done on a union, and when an intersection collapses to nothing.

### Union and Intersection Types

Source: https://academia.sh/en/courses/typescript/union-and-intersection-types

The shared-member rule in a union type, the discriminated union pattern, combining types with intersection, and the collapse of conflicting fields to the empty type.

The measurement record's `unit` field was written as `"C" | "Pa" | "%"`: a **union**
of three types that are alternatives to one another. The same operation also has an
intersection direction — values that carry the requirements of two types at once.

These two operators are the type-level counterpart of set algebra, and they frame the
measurement record's development from here on. This lesson covers which operations
can be done on a union, the discriminant field pattern, and when an intersection comes
up empty.

#### Union: The Sum of Values

Type `A | B` covers all values of type `A` **or** type `B`. At the level of value
sets, this is a union operation:

$$
\text{values}(A \mid B) = \text{values}(A) \cup \text{values}(B)
$$

Code holding a union value does not know which member it came from. This is why it
can only perform operations that exist **in both members**:

```typescript
type NumericMeasurement = { id: string; value: number };
type TextualMeasurement = { id: string; text: string };
type Entry = NumericMeasurement | TextualMeasurement;

function write(entry: Entry): void {
  console.log(entry.id);
  console.log(entry.value);
}

write({ id: "s-01", value: 21.4 });
```

```text
e1.ts(7,21): error TS2339: Property 'value' does not exist on type 'Entry'.
  Property 'value' does not exist on type 'TextualMeasurement'.
```

Access to `id` is valid, because it exists in both members. Access to `value` is
rejected, because the value could be a `TextualMeasurement`. The diagnostic's second
line names which member is blocking it.

An inverse relationship follows from this, and it is surprising at first: **as the
value set grows, the usable member set shrinks.** Adding a new member to a union
reduces the operations that can be done with that type. In the language of sets: the
union of the values is the intersection of the guaranteed members.

#### Discriminated Union

To access a member that exists in only one of a union's members, it has to be
**proven** which member the value came from. The most useful form of proof is putting
a field with the same name but a different literal type on every member:

```typescript
type Measurement =
  | { status: "valid"; id: string; value: number }
  | { status: "invalid"; id: string; code: string };

function summarize(measurement: Measurement): string {
  if (measurement.status === "valid") {
    return `${measurement.id}: ${measurement.value}`;
  }
  return `${measurement.id}: error ${measurement.code}`;
}

console.log(summarize({ status: "valid", id: "s-01", value: 21.4 }));
console.log(summarize({ status: "invalid", id: "s-02", code: "E17" }));
```

This file passes type checking and its output is:

```text
s-01: 21.4
s-02: error E17
```

The `status` field is called the **discriminant**; this pattern is called a
**discriminated union**. Three conditions are needed for it to work: the field's name
has to be the same across all members, its type has to be a literal type, and the
literals have to differ between members.

When these conditions hold, the compiler accepts the check
`measurement.status === "valid"` as proof and narrows the type to a single member
inside the `if` body. This narrowing is called **narrowing**, and its rules are the
subject of a separate lesson. Since `"invalid"` is the only member left after the `if`
block, access to `measurement.code` is valid there too.

At this point the measurement record's model makes its first structural decision: a
valid and an invalid record are not two states of the same type, they are two separate
types. This makes the `value` field accessible only in the state where it genuinely
exists — the question "does a value exist" has moved from runtime to compile time.

#### Intersection: The Sum of Requirements

Type `A & B` covers values that are both `A` and `B`. For object types, this means
merging the fields:

```typescript
type Measurement = { id: string; value: number };
type Timestamped = { time: number };
type Entry = Measurement & Timestamped;

const entry: Entry = { id: "s-01", value: 21.4, time: 1706000000000 };
console.log(entry.id, entry.value, entry.time);

const missing: Entry = { id: "s-02", value: 22.1 };
```

```text
e2.ts(8,7): error TS2322: Type '{ id: string; value: number; }' is not assignable to type 'Entry'.
  Property 'time' is missing in type '{ id: string; value: number; }' but required in type 'Timestamped'.
```

The inverse relationship from the union case reverses here: an intersection's value
set is smaller, and its usable member set is larger. An `Entry` value carries every
field of both `Measurement` and `Timestamped`.

The typical use of intersection is keeping independently meaningful attributes in
separate types and combining them where needed. In the measurement record, a
timestamp, source information, and a validation result could each be defined
separately; different contexts use different combinations. This is the type-level
counterpart of the **abstract data type** idea introduced in the Data Structures
course: a type is defined less by the fields it carries than by the contracts it
satisfies.

#### When an Intersection Collapses

Intersection does not always produce a meaningful type. Two primitive types have no
common value in their intersection:

```typescript
type Impossible = string & number;

const empty: Impossible = "s-01";
console.log(empty);
```

```text
e6.ts(3,7): error TS2322: Type '"s-01"' is not assignable to type 'never'.
```

Type `string & number` has been reduced to `never` — no value can be both a string and
a number. The diagnostic speaking directly of `never` is the result of this collapse.

The same thing shows up in object types too, through a conflicting field:

```typescript
type CelsiusRecord = { unit: "C"; value: number };
type PascalRecord = { unit: "Pa"; value: number };
type Both = CelsiusRecord & PascalRecord;

declare const record: Both;
const check: null = record;
const field: null = record.unit;
```

```text
e4.ts(7,28): error TS2339: Property 'unit' does not exist on type 'never'.
  The intersection 'Both' was reduced to 'never' because property 'unit' has conflicting types in some constituents.
```

The diagnostic states the reason for the collapse outright: because the types of the
`unit` field do not agree, the intersection has become `never` entirely. That the
sixth line raises no error is a consequence of the same thing — `never` is assignable
to every type.

This behavior is a design warning. If two types carrying conflicting fields need to be
combined, the right tool is not intersection but union: values follow the rule of one
or the other, not both at once.

#### Comparing the Two Operators

| Criterion | Union `A \| B` | Intersection `A & B` |
|---|---|---|
| Value set | Union of sets `A` and `B` | Intersection of sets `A` and `B` |
| Accessible members | Only shared members | All members |
| Before use | Narrowing required | Not required |
| Conflicting field | Used as a discriminant | Collapses to the empty type |
| Typical use | The states a value can take | Combining independent attributes |

The table shows the two operators do not substitute for each other. Union is for
modeling a **choice**: a record is either valid or invalid. Intersection is for
modeling a **sum**: a record has both an id and a timestamp.

#### Summary

- A union type's value set is the union of its members; its accessible member set is
  the intersection of its members.
- A discriminated union puts a field with the same name and a different literal type
  on every member; a check on that field enables narrowing.
- An intersection type merges object types' fields and imposes all requirements at
  once.
- The intersection of two primitive types, and of object types carrying a conflicting
  field, collapses to `never`; the compiler states the reason in the diagnostic.
- Union models a choice, intersection models a sum; for conflicting fields, the right
  tool is union.

#### Next Step

For the discriminated union to work, the fields had to be of a literal type. Yet as
seen in the type inference lesson, an object literal's fields are widened on their
own and the literal type is lost. The next lesson covers the tools that stop this
widening — literal types, the `as const` notation, and the `satisfies` operator that
checks a type without narrowing it.

### Const Types and as const

Source: https://academia.sh/en/courses/typescript/const-types-and-as-const

Single-value literal types, stopping widening with as const, deriving a union type from a fixed list, and how the satisfies operator differs from a type annotation.

For a discriminated union to work, its fields had to be a literal type. The type
inference lesson, however, set a rule in the opposite direction: an object field's
type widens on its own and the literal type is lost.

This lesson resolves that tension. First it clarifies what literal types are, then
covers the `as const` notation that stops widening, and finally the `satisfies`
operator that checks a type without narrowing it.

A terminology note: **literal type**, as meant here, is a type containing a single
value. It is a relative of the **constant** concept from the Programming Fundamentals
course — a name bound to the same value throughout the program — but it is not the
same thing; one concerns a value's type, the other the immutability of a binding.

#### What a Literal Type Is

Every value has a type containing only itself. `"C"` is a string value, and `"C"` is
also a single-element type. Such types are called literal types, and they are defined
for strings, numbers, and booleans.

Literal types are not very useful on their own — the declaration `const x: "C" = "C"`
says nothing extra. Their value shows up in a union: type `"C" | "Pa" | "%"` is a
three-valued set that rejects a fourth value.

#### Widening and `as const`

Declaring the same object literal in two forms lets the inferred types be compared:

```typescript
export const direct = { id: "s-01", unit: "C", value: 21.4 };
export const frozen = { id: "s-01", unit: "C", value: 21.4 } as const;
export const units = ["C", "Pa", "%"];
export const unitsFixed = ["C", "Pa", "%"] as const;
```

Saved under the name `const-types.ts` and compiled with
`tsc --strict --target es2022 --declaration --emitDeclarationOnly const-types.ts`,
the produced `const-types.d.ts` file is:

```typescript
export declare const direct: {
    id: string;
    unit: string;
    value: number;
};
export declare const frozen: {
    readonly id: "s-01";
    readonly unit: "C";
    readonly value: 21.4;
};
export declare const units: string[];
export declare const unitsFixed: readonly ["C", "Pa", "%"];
```

The `as const` notation does three things at once:

1. It **does not widen** the fields' types; each field keeps its own literal type.
2. It marks every field **readonly**.
3. It types an array literal as a **tuple**, not an array: `readonly ["C", "Pa", "%"]`
   instead of `string[]`.

The third point matters. The `units` array's type has forgotten the element count;
the `unitsFixed` tuple's type knows the count, the order, and the value at every
position.

The readonly marking gives a guarantee at the type level too:

```typescript
const units = ["C", "Pa", "%"] as const;
units.push("F");
```

```text
f2.ts(2,7): error TS2339: Property 'push' does not exist on type 'readonly ["C", "Pa", "%"]'.
```

#### What `as const` Leaves at Runtime

The course's axis has to be tested again. When the following file is compiled with
`tsc --strict --target es2022 ac.ts`:

```typescript
const units = ["C", "Pa", "%"] as const;
const measurement = { id: "s-01", unit: "C" } as const;
console.log(units, measurement);
```

The JavaScript produced:

```javascript
"use strict";
const units = ["C", "Pa", "%"];
const measurement = { id: "s-01", unit: "C" };
console.log(units, measurement);
```

The output of `node ac.js` is `[ 'C', 'Pa', '%' ] { id: 's-01', unit: 'C' }`.

`as const` has been **entirely erased**. The array is an ordinary array at runtime;
the `push` call is accepted by JavaScript. Readonlyness is a valid statement in the
code the compiler sees — not a runtime protection. Real immutability requires runtime
tools like `Object.freeze`, and even that is shallow.

The same boundary also shows up when a value marked `as const` is assigned to a type
that is not readonly:

```typescript
type Unit = "C" | "Pa" | "%";
type Measurement = { id: string; value: number; unit: Unit };

const raw = { id: "s-01", value: 21.4, unit: "C" } as const;
const record: Measurement = raw;
console.log(record.unit);

record.value = 22.1;
raw.value = 22.1;
```

```text
f6.ts(9,5): error TS2540: Cannot assign to 'value' because it is a read-only property.
```

The eighth line does not error. The `record` declaration's type is `Measurement`, and
`Measurement` is not readonly; a non-readonly reference to the same object has been
obtained. Readonlyness is a property not of the object, but of the **reference**.

The fifth line being accepted shows the actual problem `as const` solves: without
`as const`, the `unit` field would widen to `string` and the assignment would be
rejected.

#### Deriving a Type From a Fixed List

From a tuple declared with `as const`, a type can be derived that is the union of its
elements:

```typescript
const units = ["C", "Pa", "%"] as const;
type Unit = (typeof units)[number];

const valid: Unit = "Pa";
console.log(valid, units.length);

const invalid: Unit = "F";
```

```text
f1.ts(7,7): error TS2322: Type '"F"' is not assignable to type '"%" | "C" | "Pa"'.
```

There are two steps. The `typeof units` notation takes the type of a **value** — here,
`readonly ["C", "Pa", "%"]`. Then the `[number]` index gives the union of the types of
all the tuple's numerically indexed elements. The diagnostic writes the union of the three
constants, with the members in the compiler's own order (`"%" | "C" | "Pa"`) rather than the
array's order. The union itself is the result of this derivation.

This pattern's value is that it prevents the same information from being written
twice. The value list stays in a single place; it can be used both at runtime (walked
in a loop, used in validation) and at the type level. The "count all values" ability
an enum gives is obtained here without producing any runtime code.

#### `satisfies`: Checking Without Narrowing

Writing a type on an object literal does two things at once: it checks the literal
**and** it fixes the declaration's type to that type. The second is not always
wanted:

```typescript
type Interval = { low: number; high: number };

const annotated: Record<string, Interval> = {
  temperature: { low: -40, high: 85 },
};

const checked = {
  temperature: { low: -40, high: 85 },
} satisfies Record<string, Interval>;

console.log(annotated.humidity.high);
console.log(checked.humidity.high);
```

```text
f4.ts(12,21): error TS2339: Property 'humidity' does not exist on type '{ temperature: { low: number; high: number; }; }'.
```

The eleventh line does not error. The `annotated` declaration's type is
`Record<string, Interval>`; this type says "an interval for every string key," so a
`humidity` key counts as valid too. No such key exists at runtime, and the program
crashes.

The `satisfies` operator **checks** that the literal fits the given type, but does not
change the declaration's type. The `checked` declaration's type is the narrow type
inferred from the literal; access to a nonexistent key is caught on the twelfth line.

The checking direction works too:

```typescript
type Interval = { low: number; high: number };

const intervals = {
  temperature: { low: -40, high: "85" },
} satisfies Record<string, Interval>;

console.log(intervals.temperature.high);
```

```text
f5.ts(4,28): error TS2322: Type 'string' is not assignable to type 'number'.
```

Comparison of the three notations:

| Notation | Checks the literal | Type of the declaration |
|---|---|---|
| `const x: T = {...}` | Yes | `T` |
| `const x = {...} as T` | No (forces it) | `T` |
| `const x = {...} satisfies T` | Yes | The narrow type inferred from the literal |

The middle row is the type assertion introduced in the first lesson, and it is a debt.
The bottom row preserves both the check and the detail inference gives.

#### Summary

- A literal type is a type containing a single value; its usefulness shows up inside a
  union, and it is distinct from the "constant" concept, which is about the
  immutability of a binding.
- The `as const` notation stops widening, marks fields readonly, and types array
  literals as tuples.
- `as const` is entirely erased at runtime; readonlyness is a property of the
  reference, not the object, and does not provide real immutability.
- From a fixed tuple, the union of its elements is derived with `(typeof x)[number]`;
  the same information is kept in one place at both the value and type levels.
- The `satisfies` operator checks a literal against a given type but keeps the
  declaration's narrow type; a type annotation checks and widens, a type assertion
  only forces.

#### Next Step

This topic built the types of individual values. Next comes naming and sharing these
types: how is the contract a module exposes written, how are different
implementations satisfying the same contract defined? The next topic starts with
interfaces and turns the measurement record's model into a reusable contract.

## Structures and Contracts

Interfaces, type aliases, classes and access modifiers, function types, and the rules of structural type compatibility.

### Interfaces

Source: https://academia.sh/en/courses/typescript/interfaces

Defining a structural contract with an interface, optional and readonly fields, extension, index signatures, and declaration merging.

The previous topic established the types of individual values. This topic moves on to
naming and sharing those types: how is the contract a module exposes written, and how
are different implementations that satisfy the same contract defined?

An **interface** is a declaration that names the fields and methods a value has to
carry. It is the direct counterpart of the **abstract data type** idea from the Data
Structures course: a stack is defined not by its internal representation but by its
`push`, `pop`, `peek`, and `is_empty` operations. An interface likewise defines a type
by the operations it offers, not by its internal structure.

#### Interface Declaration

The measurement record's model can be written as an interface:

```typescript
type Unit = "C" | "Pa" | "%";

interface Measurement {
  readonly id: string;
  sensor: string;
  value: number;
  unit: Unit;
  time: number;
  note?: string;
}

const record: Measurement = {
  id: "s-01",
  sensor: "temperature",
  value: 21.4,
  unit: "C",
  time: 1706000000000,
};

console.log(record.note);
record.value = 22.1;
record.id = "s-02";
```

```text
g1.ts(22,8): error TS2540: Cannot assign to 'id' because it is a read-only property.
```

Two new markers appear in the declaration.

The **`readonly id`** field blocks assignment at compile time: the `value` assignment
on line 21 is valid, the one on line 22 is not. The boundary from the previous lesson
holds here too — `readonly` is erased at runtime and does not protect the object
itself.

The **`note?: string`** field is declared **optional**. Assignment is accepted even
though it is missing from the object literal; accessing `record.note` has the type
`string | undefined` and has to be narrowed before it can be used directly. Line 20
only prints it, so nothing goes wrong there — its output is `undefined`.

An optional field carries the "this information may not be present" case up to the
type level. The alternative — making the field required and putting an empty string
there — loses information: an empty string could mean "not measured" or "the note was
empty."

#### Extension

An interface can inherit all of another interface's requirements:

```typescript
interface Measurement {
  id: string;
  value: number;
}

interface LocatedMeasurement extends Measurement {
  lat: number;
  lon: number;
}

const record: LocatedMeasurement = {
  id: "s-01",
  value: 21.4,
  lat: 41.0,
  lon: 29.0,
};
console.log(record.id, record.lat);

const missing: LocatedMeasurement = { id: "s-02", value: 22.1 };
```

```text
g2.ts(19,7): error TS2739: Type '{ id: string; value: number; }' is missing the following properties from type 'LocatedMeasurement': lat, lon
```

The `extends` syntax produces the same result as an intersection type: the
`LocatedMeasurement` type requires both its own fields and `Measurement`'s fields.
There are two differences.

First, `extends` checks compatibility **at declaration time**:

```typescript
interface Base {
  unit: string;
}

interface Derived extends Base {
  unit: number;
}

declare const x: Derived;
console.log(x.unit);
```

```text
g6.ts(5,11): error TS2430: Interface 'Derived' incorrectly extends interface 'Base'.
  Types of property 'unit' are incompatible.
    Type 'number' is not assignable to type 'string'.
```

The diagnostic points to the declaration where the conflict lives. The same conflict
written with an intersection type would silently collapse to `never`, as seen in the
previous lesson, with the error surfacing only at the point of use.

Second, the `extends` chain appears by name in diagnostic messages and improves
readability.

This inheritance is separate from the class inheritance in the Programming
Fundamentals course: what is inherited here is not behavior, only the contract. An
interface contains no implementation.

#### Declaration Merging

Interfaces have a property type aliases lack: they can be declared more than once
under the same name. The declarations get merged.

```typescript
interface Measurement {
  id: string;
}

interface Measurement {
  value: number;
}

const record: Measurement = { id: "s-01", value: 21.4 };
console.log(record.id, record.value);

const missing: Measurement = { id: "s-02" };
```

```text
g3.ts(12,7): error TS2741: Property 'value' is missing in type '{ id: string; }' but required in type 'Measurement'.
```

The two declarations formed a single interface; the twelfth line being rejected proves
it.

This looks like a convenience, but its real purpose is different: it enables
**extending an existing type from the outside**. A field can be added to an interface
a library defines without modifying that library. The Declaration Files lesson shows
this capability's real use case.

The same capability also carries a risk: an interface's full definition may not be
visible in a single file. Relying on declaration merging in your own code makes the
definition harder to trace.

#### Index Signatures

For cases where field names are not known in advance, an **index signature** is
written:

```typescript
interface TaggedMeasurement {
  id: string;
  value: number;
  [tag: string]: string | number;
}

const record: TaggedMeasurement = {
  id: "s-01",
  value: 21.4,
  location: "boiler-2",
  shift: 3,
};

console.log(record.location, record["shift"]);
console.log(record.missingTag);
```

This file passes type checking. Its output:

```text
boiler-2 3
undefined
```

Two rules are at work. First, with an index signature present, the named fields'
types **have to fit** the signature's type; `id: string` and `value: number` are
accepted because they fall inside the `string | number` union. A `boolean` field
would trigger a diagnostic.

Second, and more importantly: **an index signature does not treat access to a
nonexistent key as an error.** The `record.missingTag` expression on the last line has
type `string | number` as far as the compiler is concerned, and `undefined` at
runtime. Here the type system makes a promise beyond reality.

A compiler option closes this gap — a strictness flag that adds `undefined` to the
result of indexed access — covered in the Compiler Configuration lesson. General rule:
if the key set is known, writing the fields out individually or using a mapped type is
safer than an index signature.

#### Interface as a Contract

An interface's real power shows up less in defining a data shape and more in
establishing a **behavioral contract**. The next step for the measurement record model
is to abstract away where records come from:

```typescript
type Measurement = { id: string; value: number };

interface MeasurementSource {
  readonly name: string;
  next(): Measurement | null;
}

function arraySource(name: string, records: readonly Measurement[]): MeasurementSource {
  let index = 0;
  return {
    name,
    next() {
      return index < records.length ? records[index++] : null;
    },
  };
}

function constantSource(name: string, value: number): MeasurementSource {
  return {
    name,
    next() {
      return { id: name, value };
    },
  };
}

function firstTwo(source: MeasurementSource): string {
  const a = source.next();
  const b = source.next();
  return `${source.name}: ${a?.value ?? "-"} ${b?.value ?? "-"}`;
}

console.log(firstTwo(arraySource("file", [{ id: "s-01", value: 21.4 }])));
console.log(firstTwo(constantSource("constant", 20)));
```

Output:

```text
file: 21.4 -
constant: 20 20
```

`firstTwo` knows neither source; it knows only the `MeasurementSource` contract. This
is the type-level counterpart of the **interface–implementation** distinction from the
Programming Fundamentals course: the contract and the code satisfying it can change
independently.

Worth noticing: no implementation writes the name `MeasurementSource`. The object
literal `arraySource` returns carries no declaration saying "this is a measurement
source"; it is accepted only because it has the required fields. This is the
foundation of TypeScript's type compatibility, spelled out in the Structural Type
Compatibility lesson.

The return type being `Measurement | null` is also a design decision. The `a?.value`
and `?? "-"` syntax inside `firstTwo` has to handle that union; the compiler does not
allow the `null` possibility to be skipped.

#### Summary

- An interface names the fields and methods a value has to carry; it is the
  type-level counterpart of the abstract data type idea.
- `readonly` fields block assignment at compile time; optional fields carry the
  possibility of `undefined` into the type.
- The `extends` syntax merges contracts and checks conflicts at declaration time; it
  does not pass on implementation.
- Interface declarations under the same name get merged; this capability is for
  extending external types, and it makes tracing a definition harder in your own
  code.
- An index signature types unknown keys but does not treat access to a nonexistent
  key as an error; if the key set is known, the fields are written out individually.

#### Next Step

The same measurement model could also have been written with the `type` keyword
instead of `interface` — and indeed, that is how it was written in the previous topic.
The two declaration forms are interchangeable in most cases, but they are not exactly
equivalent. The next lesson will list the differences and build a criterion for which
one to choose in which situation.

### Type Aliases

Source: https://academia.sh/en/courses/typescript/type-aliases

The differences between a type declaration and an interface: naming any type, the absence of declaration merging, circular-reference rules, a class contract written with a type alias, and a selection criterion.

The same measurement model was written with `type` in the previous topic and with
`interface` in this topic's first lesson. The two declaration forms are interchangeable
in most cases; they are not equivalent.

A **type alias** is a declaration that gives a name to an existing type. It does not
create a new type — after `type Id = string`, `Id` and `string` are the same type and
interchangeable. An interface, on the other hand, **defines** a named object type. This
lesson establishes where the difference shows up and when to choose which.

#### Naming Any Type

An alias can name any type. An interface can only define object and callable shapes:

```typescript
type Unit = "C" | "Pa" | "%";
type Id = string;
type Interval = [low: number, high: number];
type Converter = (value: number) => number;

const unit: Unit = "C";
const interval: Interval = [-40, 85];
const toKelvin: Converter = (v) => v + 273.15;

console.log(unit, interval, toKelvin(21.4));
```

Output:

```text
C [ -40, 85 ] 294.54999999999995
```

None of these four can be written with an interface. A union, a primitive type, and a
tuple are not object types; a function type can be written with an interface, but it
needs much longer syntax. The mapped, conditional, and template literal types from the
next topic can likewise only be named with an alias.

Notice the output's last value: `21.4 + 273.15` gives `294.54999999999995`, not
`294.55`. The type system correctly counted the `number` type; floating-point
representation's precision limit is not its concern. This is a concrete example of the
"what it does not prove" list from the first lesson.

#### No Declaration Merging

Interfaces could be redeclared under the same name and merged. Type aliases have no
such thing:

```typescript
type Measurement = { id: string };
type Measurement = { value: number };
```

```text
h2.ts(1,6): error TS2300: Duplicate identifier 'Measurement'.
h2.ts(2,6): error TS2300: Duplicate identifier 'Measurement'.
```

The diagnostic appears twice, because both declarations are party to the conflict.

This is not a limitation of the alias but a guarantee: a type alias's definition is
complete where it is written. It is impossible for another file to add a field to that
type. In your own codebase's data models, this predictability is usually what you want.

In the other direction, an interface can be derived from a type alias through
**extension**:

```typescript
type NumericMeasurement = { id: string; value: number };

interface TimedMeasurement extends NumericMeasurement {
  time: number;
}

const m: TimedMeasurement = { id: "s-01", value: 21.4, time: 1 };
console.log(m);
```

This file compiles without error. But if the alias does not name an object type,
extension fails:

```typescript
type NumericMeasurement = { id: string; value: number };
type TextMeasurement = { id: string; text: string };
type Entry = NumericMeasurement | TextMeasurement;

interface Extended extends Entry {
  time: number;
}
```

```text
h7.ts(5,28): error TS2312: An interface can only extend an object type or intersection of object types with statically known members.
```

Extending a union has no meaning: which member `Extended` inherits from is ambiguous.
Writing the same operation with an intersection, though, is valid — `type Extended =
Entry & { time: number }` produces a union that adds a `time` field to both members.

#### Circular Reference

When measurement records turn into a nested structure, the type has to refer to
itself. A reference inside an object field is valid:

```typescript
type MeasurementNode = {
  id: string;
  value: number;
  children: MeasurementNode[];
};

const root: MeasurementNode = {
  id: "boiler-2",
  value: 21.4,
  children: [{ id: "s-01", value: 21.4, children: [] }],
};

function sumNodes(node: MeasurementNode): number {
  return 1 + node.children.reduce((t, c) => t + sumNodes(c), 0);
}

console.log(sumNodes(root));
```

The file compiles without error; its output is `2`.

A **direct** circular reference, by contrast, is rejected:

```typescript
type Chain = Chain | null;
declare const z: Chain;
console.log(z);
```

```text
h3.ts(1,6): error TS2456: Type alias 'Chain' circularly references itself.
```

The distinction: the `MeasurementNode` reference sits inside an object field, and the
compiler resolves that field's type only when needed. The `Chain` declaration, though,
needs itself again to resolve its own definition — an expansion that never terminates.
The rules for recursive type definitions will be covered in detail in the Advanced
Types topic.

#### A Class Contract Written with a Type Alias

The contract a class satisfies can also be written with a type alias. As long as the
alias names an object type, the `implements` declaration works exactly as it does with
an interface:

```typescript
type Measurement = { id: string; value: number };

class FileRecord implements Measurement {
  constructor(
    readonly id: string,
    readonly value: number,
  ) {}
}

const record: Measurement = new FileRecord("s-01", 21.4);
console.log(record.id, record.value);
```

Output is `s-01 21.4`.

The limit is the same as with extension: if the alias names a union, it cannot be
implemented.

```typescript
type NumericMeasurement = { id: string; value: number };
type TextMeasurement = { id: string; text: string };
type Entry = NumericMeasurement | TextMeasurement;

class FileRecord implements Entry {
  constructor(readonly id: string) {}
}
```

```text
u2.ts(5,29): error TS2422: A class can only implement an object type or intersection of object types with statically known members.
```

The reasoning is the same: which member the class satisfies is ambiguous. A class
being "either this shape or that one" cannot be expressed with a single declaration.
In such a model, the contract is satisfied by separate classes for each union member,
and the union is only used at the value level.

#### Selection Criterion

The differences between the two declaration forms can be gathered into a table:

| Criterion | `interface` | `type` |
|---|---|---|
| Defines an object type | Yes | Yes |
| Names a union, tuple, primitive | No | Yes |
| Redeclaration under the same name | Merges | `TS2300` |
| Extension | `extends` (checked at declaration) | `&` (silently collapses) |
| Implemented by a class | `implements` | `implements` (if object type) |
| Mapped, conditional type | No | Yes |

An actionable criterion follows from the table:

**An interface for contracts shaped like an object.** A data record's fields, the
methods a service offers, the contract a class satisfies. Because extension is
checked, errors are caught at declaration time; diagnostic messages carry the name.

**A type alias for everything else.** Unions, discriminated unions, tuples, function
types, types computed at the type level. These either have no interface equivalent or
forcing one is awkward.

In this course, the measurement record itself is written with an interface, and its
statuses and derived types are written with type aliases:

```typescript
interface Measurement {
  readonly id: string;
  sensor: string;
  value: number;
  unit: "C" | "Pa" | "%";
  time: number;
}

type MeasurementStatus =
  | { status: "valid"; measurement: Measurement }
  | { status: "failed"; id: string; code: string };

function summarize(record: MeasurementStatus): string {
  return record.status === "valid"
    ? `${record.measurement.id}=${record.measurement.value}${record.measurement.unit}`
    : `${record.id}: ${record.code}`;
}

console.log(
  summarize({
    status: "valid",
    measurement: {
      id: "s-01",
      sensor: "temperature",
      value: 21.4,
      unit: "C",
      time: 1706000000000,
    },
  }),
);
console.log(summarize({ status: "failed", id: "s-02", code: "E17" }));
```

Output:

```text
s-01=21.4C
s-02: E17
```

The division of labor between the two declaration forms shows here: `Measurement` is a
record's shape and can grow; `MeasurementStatus` is a closed set of options, and its
growth should be a deliberate decision.

#### Summary

- A type alias names an existing type; it can name any type, including unions,
  tuples, primitives, and function types.
- Interfaces get redeclared under the same name and merge; a type alias's second
  declaration produces a `TS2300` diagnostic, which guarantees the definition is
  complete in one place.
- An interface can extend a type alias that names an object type; it cannot extend a
  union type.
- A circular reference inside an object field is valid; a directly circular type
  alias produces a `TS2456` diagnostic.
- A type alias naming an object type can be implemented with `implements`; one
  naming a union produces a `TS2422` diagnostic.
- Criterion: object-shaped contracts are written with an interface; unions and types
  computed at the type level are written with a type alias.

#### Next Step

The contracts so far have defined only data and operation signatures; the
implementation came from a separate function each time. Classes hold the two
together, and TypeScript adds visibility control to classes. The next lesson takes up
access modifiers, abstract classes, and what they leave behind in the compiled output.

### Classes and Access Modifiers

Source: https://academia.sh/en/courses/typescript/classes-and-access-modifiers

Typing class fields, the compile-time and runtime counterparts of visibility modifiers, parameter properties, abstract classes, and the implements declaration.

The contracts so far have defined only data and operation signatures; the
implementation came from a separate function each time. Classes hold the two
together.

The Objects and Functions in JavaScript course established class syntax's counterpart
in the prototype model. TypeScript adds three things to that syntax: types for fields
and methods, **visibility modifiers** (access modifier), and abstract declarations.
Some of these additions are erased, some generate code; that distinction is this
lesson's center.

#### Typing Fields and Visibility

A log class that collects measurement records shows all three visibility forms
together:

```typescript
interface Measurement {
  readonly id: string;
  value: number;
}

class MeasurementLog {
  private entries: Measurement[] = [];
  #hiddenCounter = 0;

  constructor(readonly name: string) {}

  add(measurement: Measurement): void {
    this.entries.push(measurement);
    this.#hiddenCounter += 1;
  }

  get count(): number {
    return this.entries.length;
  }
}

const log = new MeasurementLog("boiler-2");
log.add({ id: "s-01", value: 21.4 });
console.log(log.name, log.count);
```

Compiled with `tsc --strict --target es2022 log.ts`, the file named `log.ts` produces
this JavaScript:

```javascript
"use strict";
class MeasurementLog {
    name;
    entries = [];
    #hiddenCounter = 0;
    constructor(name) {
        this.name = name;
    }
    add(measurement) {
        this.entries.push(measurement);
        this.#hiddenCounter += 1;
    }
    get count() {
        return this.entries.length;
    }
}
const log = new MeasurementLog("boiler-2");
log.add({ id: "s-01", value: 21.4 });
console.log(log.name, log.count);
```

`node log.js` outputs `boiler-2 1`. Three things stand out in the output.

**`private` is erased.** The `private entries` field remains `entries` in the compiled
code and is readable from outside. **`#hiddenCounter` stays.** This is a feature
belonging to JavaScript, not TypeScript, and it is enforced at runtime.

**The `constructor(readonly name: string)` syntax generates code.** This is called a
**parameter property**: when a visibility or `readonly` modifier is placed on a
constructor parameter, the compiler declares a field with the same name and adds the
assignment. Like enums, this is an exception to the type-erasure principle.

The consequence of this difference is observed when the compiled class is used
directly from JavaScript. The file below takes the class body generated above and adds
three queries:

```javascript
class MeasurementLog {
    name;
    entries = [];
    #hiddenCounter = 0;
    constructor(name) {
        this.name = name;
    }
    add(measurement) {
        this.entries.push(measurement);
        this.#hiddenCounter += 1;
    }
    get count() {
        return this.entries.length;
    }
}

const log = new MeasurementLog("boiler-2");
log.add({ id: "s-01", value: 21.4 });

console.log(Object.keys(log));
console.log(log.entries);
console.log(log.hiddenCounter);
```

```text
[ 'name', 'entries' ]
[ { id: 's-01', value: 21.4 } ]
undefined
```

The `entries` field is both enumerable and readable; `#hiddenCounter`, on the other
hand, gives `undefined` when accessed under the name `hiddenCounter`, because no such
public field exists.

At compile time, both are protected, but with different diagnostics:

```typescript
class MeasurementLog {
  private records: number[] = [];
  #counter = 0;

  add(value: number): void {
    this.records.push(value);
    this.#counter += 1;
  }
}

const log = new MeasurementLog();
log.add(21.4);
console.log(log.records);
console.log(log.#counter);
```

```text
i1.ts(13,17): error TS2341: Property 'records' is private and only accessible within class 'MeasurementLog'.
i1.ts(14,17): error TS18013: Property '#counter' is not accessible outside class 'MeasurementLog' because it has a private identifier.
```

The selection criterion: **`private` if visibility is a design rule, `#` if it is a
security boundary.** Within a team, `private` is enough to prevent misuse, and it
catches it at compile time. At a library boundary, where unaudited code must not have
access, `#` is used.

The `protected` modifier is likewise valid only at compile time, and it opens the
field to the class itself and its subclasses.

#### Abstract Classes

An **abstract class** is a class that can declare unimplemented members and cannot be
instantiated directly. If the measurement source contract is also going to carry
shared behavior, it is written with an abstract class instead of an interface:

```typescript
interface Measurement {
  id: string;
  value: number;
}

abstract class MeasurementSource {
  abstract readonly name: string;
  protected abstract next(): Measurement | null;

  all(): Measurement[] {
    const collected: Measurement[] = [];
    let record = this.next();
    while (record !== null) {
      collected.push(record);
      record = this.next();
    }
    return collected;
  }
}

class ArraySource extends MeasurementSource {
  readonly name = "array";
  private index = 0;

  constructor(private readonly records: readonly Measurement[]) {
    super();
  }

  protected next(): Measurement | null {
    return this.index < this.records.length ? this.records[this.index++] : null;
  }
}

const source = new ArraySource([
  { id: "s-01", value: 21.4 },
  { id: "s-02", value: 22.1 },
]);
console.log(source.name, source.all().length);
```

Output is `array 2`.

The structure separates two kinds of members. The `next` method is abstract: every
source implements it differently. The `all` method is concrete and carries the shared
behavior built on top of `next`. The subclass only writes the part that changes.

If a line that tries to instantiate the abstract class directly is tried with a shortened
definition of the same abstract class in a separate file (`i2.ts`), the compiler gives
this diagnostic:

```typescript
// i2.ts — instantiating an abstract class
interface Measurement {
  id: string;
  value: number;
}

abstract class MeasurementSource {
  abstract readonly name: string;
  protected abstract next(): Measurement | null;
}

const impossible = new MeasurementSource();
```

```text
i2.ts(12,20): error TS2511: Cannot create an instance of an abstract class.
```

The `abstract` modifier is erased too — an ordinary class remains in the compiled
output, and it can be instantiated by JavaScript. The prohibition belongs to compile
time.

#### `implements`

That a class satisfies a specific contract is declared with `implements`:

```typescript
interface MeasurementSource {
  readonly name: string;
  next(): number | null;
}

class EmptySource implements MeasurementSource {
  readonly name = "empty";
}
```

```text
i4.ts(6,7): error TS2420: Class 'EmptySource' incorrectly implements interface 'MeasurementSource'.
  Property 'next' is missing in type 'EmptySource' but required in type 'MeasurementSource'.
```

`implements` is a **check** declaration, not an inheritance. It adds no member to the
class; it only verifies conformance to the contract and gives the diagnostic on the
class's declaration line. Without this declaration the class would still compile, and
the mismatch would only surface at the point of use.

The `implements` syntax is erased entirely; no trace of it remains in the compiled
output.

#### Abstract Class or Interface

The two tools overlap; the criterion is this:

| Criterion | Interface | Abstract class |
|---|---|---|
| Carries a shared implementation | No | Yes |
| Generates runtime code | No | Yes |
| A type satisfies more than one | Yes | No (single superclass) |
| Carries state (field values) | No | Yes |

**An interface when only a contract is needed.** It is erased, more than one can be
used together, and it does not bind the implementation.

**An abstract class when shared behavior is needed along with the contract.** The
`all` method above is an example: an algorithm written in one place and valid across
every source.

The **class invariant** concept introduced in the Programming Fundamentals course
finds type-level support here: an invariant held over `private` fields cannot be
broken from outside — because only the class's own methods can access those fields,
and the compiler enforces it.

#### Summary

- Class field and method types are erased; `private` and `protected` are valid only
  at compile time, while fields declared with `#` are protected at runtime too.
- A parameter property (`constructor(readonly name: string)`) generates code: the
  compiler adds the field declaration and the assignment.
- An abstract class declares unimplemented members and cannot be instantiated
  directly; the `abstract` modifier does not remain in the compiled output.
- `implements` is a check declaration; it adds no member to the class, gives the
  diagnostic on the declaration line, and is erased entirely.
- An interface is chosen for a contract alone; an abstract class is chosen for a
  contract together with shared behavior.

#### Next Step

Class methods and standalone functions were only used in this lesson; how their types
are written was not covered. A function's type is not just parameter and return
types: optional parameters, rest parameters, overloading, and the `this` context are
also part of the type. The next lesson builds function types in detail.

### Function Types

Source: https://academia.sh/en/courses/typescript/function-types

Function type syntax, optional and rest parameters, the void return type's special behavior, the parameter-count rule, overloading, and the this parameter.

The previous lesson used class methods and standalone functions, but how their types
are written was not covered. A function's type is not just parameter and return types:
optional parameters, rest parameters, return types that vary with the call form, and
the `this` context are also part of the type.

This lesson builds these components in order and applies them to a parser that reads
measurement records.

#### Function Type Syntax

A function type is written with a parameter list, an arrow, and a return type:

```typescript
type Measurement = { id: string; value: number };
type Filter = (measurement: Measurement) => boolean;

function select(records: readonly Measurement[], filter: Filter): Measurement[] {
  return records.filter(filter);
}

const positive: Filter = (m) => m.value > 0;
const records: Measurement[] = [
  { id: "s-01", value: 21.4 },
  { id: "s-02", value: -3 },
];

console.log(select(records, positive).length);

const wrong: Filter = (m) => m.value;
```

```text
j1.ts(16,30): error TS2322: Type 'number' is not assignable to type 'boolean'.
```

The `positive` declaration writes no type for the `m` parameter; the type comes from
the `Filter` definition through contextual typing. The last line is rejected because
its return type does not match — a number, usable as a truthy value in JavaScript,
cannot be placed where `boolean` is expected.

Parameter names in a function type (`measurement`) are for documentation; they play no
role in compatibility. What matters is order and type.

#### Optional, Default, and Rest Parameters

```typescript
type Measurement = { id: string; value: number; note?: string };

function build(
  id: string,
  value: number,
  unit: string = "C",
  ...tags: string[]
): string {
  return `${id}=${value}${unit} [${tags.join(",")}]`;
}

console.log(build("s-01", 21.4));
console.log(build("s-02", 101325, "Pa", "boiler", "shift-3"));

function printNote(measurement: Measurement): string {
  return measurement.note.trim();
}
console.log(printNote({ id: "s-03", value: 1 }));
```

```text
j2.ts(16,10): error TS18048: 'measurement.note' is possibly 'undefined'.
```

The `unit` parameter has a default value; its type is inferred as `string`, and it can
be omitted at the call site. `tags` is a rest parameter; its type is an array, and it
is always defined — if omitted, it becomes an empty array.

The last diagnostic shows the cost of optional fields: `note?: string` makes the
field's type `string | undefined`, and `trim` cannot be called on `undefined`. The
compiler reports the skipped check. The fix is narrowing:
`measurement.note?.trim() ?? ""` or an explicit `if` block.

This behavior depends on the `strictNullChecks` option. With it off, `undefined` is
assignable to every type and this diagnostic never appears — optional fields then
carry no guarantee at all.

#### The void Return Type's Special Behavior

`void` means "the return value will not be used." It has a looser rule than expected:

```typescript
type Measurement = { id: string; value: number };
type Recorder = (measurement: Measurement) => void;

const records: Measurement[] = [];
const add: Recorder = (measurement) => records.push(measurement);

add({ id: "s-01", value: 21.4 });
console.log(records.length);

const result = add({ id: "s-02", value: 22.1 });
console.log(result.toFixed(0));
```

```text
j3.ts(11,20): error TS2339: Property 'toFixed' does not exist on type 'void'.
```

Line 5 does **not** error. `records.push(measurement)` returns a number, while
`Recorder` declares a `void` return. The rule: **a function with a return value can be
assigned to a type that returns `void`.** The reasoning: `void` does not say "return
nothing," it says "what you return will not be used."

The consequence shows up on line 11: viewed from the call site, the return type is
`void`, and nothing can be done with it. The value exists in reality; the type system
refuses to see it.

This rule makes functions that take callbacks easier to use. `forEach`'s callback
returns `void`; if every callback had to skip a return value, short forms like
`(m) => records.push(m)` would be rejected.

#### The Parameter-Count Rule

In function compatibility, parameter count works in one direction:

```typescript
type Measurement = { id: string; value: number };
type Callback = (measurement: Measurement, index: number) => void;

const recordOnly: Callback = (measurement) => console.log(measurement.id);
recordOnly({ id: "s-01", value: 21.4 }, 0);

const extra: Callback = (measurement, index, extra: string) => console.log(extra);
```

```text
j4.ts(7,7): error TS2322: Type '(measurement: any, index: any, extra: string) => void' is not assignable to type 'Callback'.
  Target signature provides too few arguments. Expected 3 or more, but got 2.
j4.ts(7,26): error TS7006: Parameter 'measurement' implicitly has an 'any' type.
j4.ts(7,39): error TS7006: Parameter 'index' implicitly has an 'any' type.
```

A function taking **fewer** parameters can be assigned to a type that declares more;
it ignores the extra arguments that come in. A function taking **more** parameters
cannot be assigned — the caller will not supply that argument.

This directly reflects JavaScript's call rule: an extra argument causes no error, a
missing one becomes `undefined`. The type system allows the former and blocks the
latter.

Notice the last two diagnostics: on line 7, `measurement` and `index` lost their
contextual types. Because the assignment failed, context could not be established,
and the parameters remained implicit `any`. A single error producing multiple
diagnostics is common; the source diagnostic is the first one.

#### Overloading

If a function gives different return types depending on the argument's type, it is
declared with **overloading**:

```typescript
type Measurement = { id: string; value: number };

function parseOne(raw: string): Measurement | null {
  const parts = raw.split("=");
  if (parts.length !== 2) {
    return null;
  }
  const value = Number(parts[1]);
  return Number.isFinite(value) ? { id: parts[0], value } : null;
}

function read(raw: string): Measurement | null;
function read(raw: readonly string[]): Measurement[];
function read(raw: string | readonly string[]): Measurement | Measurement[] | null {
  if (typeof raw === "string") {
    return parseOne(raw);
  }
  return raw.map(parseOne).filter((m): m is Measurement => m !== null);
}

const single = read("s-01=21.4");
const many = read(["s-01=21.4", "broken", "s-02=22.1"]);

console.log(single?.value);
console.log(many.length, many.map((m) => m.id).join(","));
```

Output:

```text
21.4
2 s-01,s-02
```

There are three signatures, but only two can be called. The first two lines are **call
signatures**; the third is the **implementation signature** and is not visible from
outside. `single`'s declared type is `Measurement | null`, `many`'s is
`Measurement[]` — a precise type based on the call form, rather than one single union
type.

That the implementation signature cannot be called shows up with a union-typed
argument. Tried with the same definitions in a separate file (`j8.ts`):

```typescript
// j8.ts — call with a union-typed argument
type Measurement = { id: string; value: number };

function parseOne(raw: string): Measurement | null {
  const parts = raw.split("=");
  if (parts.length !== 2) {
    return null;
  }
  const value = Number(parts[1]);
  return Number.isFinite(value) ? { id: parts[0], value } : null;
}

function read(raw: string): Measurement | null;
function read(raw: readonly string[]): Measurement[];
function read(raw: string | readonly string[]): Measurement | Measurement[] | null {
  if (typeof raw === "string") {
    return parseOne(raw);
  }
  return raw.map(parseOne).filter((m): m is Measurement => m !== null);
}

declare const ambiguous: string | readonly string[];
const ambiguousResult = read(ambiguous);
console.log(ambiguousResult);
```

```text
j8.ts(23,30): error TS2769: No overload matches this call.
  The last overload gave the following error.
    Argument of type 'string | readonly string[]' is not assignable to parameter of type 'readonly string[]'.
      Type 'string' is not assignable to type 'readonly string[]'.
```

The diagnostic reports only why the last call signature does not fit; it never
mentions the implementation signature. The caller has to narrow the argument before
the call.

Overloading is purely a type-level declaration. A single function remains in the
compiled output, and the implementation body does the branching — that is where the
`typeof raw === "string"` check lives.

#### The this Parameter

The context a function is called in can be typed too. When the first parameter's name
is `this`, that parameter is not a real parameter but a context declaration:

```typescript
type Ledger = { name: string; records: number[] };

function summarize(this: Ledger): string {
  return `${this.name}: ${this.records.length}`;
}

const ledger: Ledger & { summarize: () => string } = {
  name: "boiler-2",
  records: [21.4, 22.1],
  summarize,
};

console.log(ledger.summarize());
```

Output is `boiler-2: 2`. If a line that calls the function detached from its object is
tried with the same definitions in a separate file (`this2.ts`):

```typescript
// this2.ts — call that loses its context
type Ledger = { name: string; records: number[] };

function summarize(this: Ledger): string {
  return `${this.name}: ${this.records.length}`;
}

console.log(summarize());
```

```text
this2.ts(8,13): error TS2684: The 'this' context of type 'void' is not assignable to method's 'this' of type 'Ledger'.
```

The `this`-binding rules from the Objects and Functions in JavaScript course are
checked here at the type level: a method called after being detached from its object
loses its context, and the compiler reports it.

The `this` parameter is erased entirely too. In the compiled output of the file above,
the function signature is empty:

```javascript
function summarize() {
    return `${this.name}: ${this.records.length}`;
}
```

#### Summary

- A function type is written with a parameter list and a return type; parameter
  names play no role in compatibility.
- Optional fields and parameters add `undefined` to the type; they have to be
  narrowed before use.
- A function with a return value can be assigned to a type that returns `void`, but
  the return value cannot be used at the call site.
- A function taking fewer parameters can be assigned to a type with more
  parameters; the reverse cannot.
- In overloading, call signatures are visible from outside, the implementation
  signature is not; branching happens in the body at runtime.
- The `this` parameter types the context and is erased entirely.

#### Next Step

In none of this topic's examples did a value write the name of the type it fit: object
literals were accepted into interfaces, functions into function types, only because
their **shapes** matched. This is TypeScript's fundamental type-compatibility rule,
and it has unexpected consequences. The next lesson takes up structural typing's
rules, excess property checking, and the situations where structural compatibility has
to be overridden.

### Structural Type Compatibility

Source: https://academia.sh/en/courses/typescript/structural-type-compatibility

The rules of duck typing, excess property checking on object literals, the direction rule for function parameters, and ways to break structural compatibility.

In none of this topic's examples did a value write the name of the type it fit. Object
literals were accepted into interfaces, functions into function types, only because
their **shapes** matched.

This is TypeScript's fundamental compatibility rule. **Structural typing** — popularly
known as **duck typing** — determines whether a value fits a type by looking at the
members it carries, not the name it declares. Its opposite is **nominal typing**: type
compatibility holds only when explicitly declared.

This lesson takes up structural typing's rules, the problems it causes, and how to
break it when necessary.

#### The Basic Rule

A type is compatible if it carries every member the target type requires. Carrying
extra members does not break compatibility:

```typescript
interface Measurement {
  id: string;
  value: number;
}

const matching = { id: "s-01", value: 21.4, location: "boiler-2" };
const viaVariable: Measurement = matching;
console.log(viaVariable.id);

const direct: Measurement = { id: "s-02", value: 22.1, location: "boiler-3" };
```

```text
k1.ts(10,56): error TS2353: Object literal may only specify known properties, and 'location' does not exist in type 'Measurement'.
```

Line 7 is valid: the `matching` object carries the two fields `Measurement`
requires; the third field is extra and gets ignored. Line 10, though, is rejected
when an object of the same shape is written directly.

The difference is the **excess property check**. The compiler treats unknown fields
as an error in **fresh object literals** assigned directly to a type. The reasoning
is practical: an extra field in a literal written directly is almost always a typo or
a misunderstood contract. In an object coming through a variable, an extra field is
normal — the object may be serving another purpose too.

The check's narrow scope is deliberate and does not provide full protection.
Assigning to an intermediate variable bypasses the check; this is the rule's scope,
not a loophole in it.

#### The Name Plays No Role

Structural typing's most striking consequence is that two unrelated types can be
compatible:

```typescript
interface Measurement {
  id: string;
  value: number;
}

interface Product {
  id: string;
  value: number;
}

const measurement: Measurement = { id: "s-01", value: 21.4 };
const product: Product = measurement;
console.log(product.value);
```

This file compiles without error. `Measurement` and `Product` are different
concepts, their `value` fields measure different things; as far as the type system is
concerned, they are the same type.

This behavior is not a flaw, it is a design decision. TypeScript is layered on top of
existing JavaScript; in JavaScript, objects travel by the properties they own, not by
declared types. If a library can be handed "an object carrying these fields," the type
system has to recognize the same flexibility. Structural typing is also why the
previous lesson could implement an interface without any class writing its name.

The cost is that types with different meanings can get mixed up. A tool against this
will be built later in this lesson.

#### The Direction Rule for Functions

In function types, compatibility works in the **reverse** direction for parameters:

```typescript
interface Measurement {
  id: string;
  value: number;
}

interface LocatedMeasurement extends Measurement {
  location: string;
}

type Handler = (measurement: Measurement) => void;

const byLocation = (measurement: LocatedMeasurement): void => console.log(measurement.location);
const handler: Handler = byLocation;

handler({ id: "s-01", value: 21.4 });
```

```text
k4.ts(13,7): error TS2322: Type '(measurement: LocatedMeasurement) => void' is not assignable to type 'Handler'.
  Types of parameters 'measurement' and 'measurement' are incompatible.
    Property 'location' is missing in type 'Measurement' but required in type 'LocatedMeasurement'.
```

The reasoning shows up on line 15: the `handler` type promises a function taking
`Measurement`; the caller sends a record with no `location` field; `byLocation`
accesses that field. Had the assignment been accepted, the program would operate on
`undefined` at runtime.

The rule can be summed up: a function can be assigned to a type that accepts a
**wider** parameter; a function requiring a **narrower** parameter cannot. For the
return type, the direction is straight — a narrower return type fits where a wider
one is expected.

This rule applies only to function types declared with arrow syntax. For members
declared with **method syntax**, the compiler allows both directions:

```typescript
interface Measurement {
  id: string;
  value: number;
}

interface LocatedMeasurement extends Measurement {
  location: string;
}

interface HandlerMethod {
  handle(measurement: Measurement): void;
}

const obj: HandlerMethod = {
  handle(measurement: LocatedMeasurement): void {
    console.log(measurement.location);
  },
};

obj.handle({ id: "s-01", value: 21.4 });
```

This file **passes** type checking and prints `undefined` when run.

This is a gap the type system deliberately leaves open. The reason is how array
methods are typed: `Array<T>`'s methods are written with method syntax, and applying
the strict rule would reject many commonly used patterns. It is a concrete example of
the "not fully trustworthy" situation mentioned in the first lesson.

Practical takeaway: declaring callback types with **arrow syntax** is safer.

#### Breaking Structural Compatibility

Compatibility might be broken deliberately for two reasons: separating same-shaped
types that carry different meanings, and recording at the type level that a value has
passed a specific validation.

Having a `private` or `protected` member in a class breaks compatibility on its own:

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

class ProductLog {
  private records: number[] = [];
}

const log: MeasurementLog = new ProductLog();
console.log(log);
```

```text
k3.ts(9,7): error TS2322: Type 'ProductLog' is not assignable to type 'MeasurementLog'.
  Types have separate declarations of a private property 'records'.
```

The two fields carry the same name and the same type; they're incompatible anyway,
because private members are only considered compatible when they come from **the same
declaration**. This gives classes behavior close to nominal typing.

The corresponding technique for object types is the **branded type**: a field that
does not exist at runtime is added to the type.

```typescript
declare const brand: unique symbol;

type SensorId = string & { readonly [brand]: "sensor" };

function sensorId(raw: string): SensorId {
  if (!/^s-\d{2}$/.test(raw)) {
    throw new Error(`invalid sensor id: ${raw}`);
  }
  return raw as SensorId;
}

function readMeasurement(id: SensorId): string {
  return `${id} read`;
}

console.log(readMeasurement(sensorId("s-01")));

const text: string = sensorId("s-02");
console.log(text.toUpperCase());
```

Output:

```text
s-01 read
S-02
```

If the line `readMeasurement("s-03");` is added to the end of the file, after a blank
line:

```text
marka2.ts(21,17): error TS2345: Argument of type 'string' is not assignable to parameter of type 'SensorId'.
  Type 'string' is not assignable to type '{ readonly [brand]: "sensor"; }'.
```

The structure has three parts. `declare const brand: unique symbol` produces a unique
type-level key — since it is written with `declare`, it has no runtime counterpart.
`SensorId` is the intersection of a string with an object carrying that key; no real
string carries this field, so entry into the type is only possible through a type
assertion. The `sensorId` function makes that assertion in exactly one place, **after**
validation.

The result is a proof carried in the type system: a value of type `SensorId` means it
has passed the format check. That the `text` declaration is valid is also part of the
design — a branded type is still a string, and string operations can be used on it;
the reverse direction is closed.

This pattern follows the rule set in the first lesson: validation happens at runtime,
and the type system only records the result.

#### Summary

- In structural typing, compatibility is determined by looking at carried members,
  not the declared name; two unrelated types with the same shape are compatible.
- Excess property checking applies only to object literals written directly; it does
  not apply to objects coming through an intermediate variable.
- Compatibility for function parameters works in reverse: a function accepting a
  wider parameter can be assigned to a type declaring a narrower one.
- This rule is relaxed for members declared with method syntax, leaving a gap that
  can produce a runtime error.
- Private members in classes break compatibility; for object types, a branded type
  is used, and this pattern records at the type level that a validation has taken
  place.

#### Next Step

Every type in this topic was specific and fixed: a log holding `Measurement` records,
a filter taking `Measurement`. But a log structure needs to be independent of its
record type — the same code should be able to collect measurements, warnings, or
events. The next topic introduces type parameters and builds that independence without
losing type safety.

## Advanced Types

Generics and constraints, type narrowing, mapped and conditional types, template literal types, utility types, and recursive type definitions.

### Generics

Source: https://academia.sh/en/courses/typescript/generics

Writing shape-independent structures with type parameters, type argument inference, constraints, the keyof constraint, default type parameters, and generic misuse.

Every type in the previous topic was fixed: a `Log` holding `Measurement` records, a
`select` function filtering `Measurement`. Yet a log's structure is independent of the
record type — the same code should be able to collect measurements, warnings, or
events.

There are two bad ways to get this. Writing a separate log for every record type
duplicates code; letting the log use `any[]` removes type safety. **Generics** give a
third way: writing the structure with a **type parameter** and supplying the type at
the point of use.

#### Type-Parameterized Structures

A type parameter is written in angle brackets after the declaration's name and used
like a type throughout the body:

```typescript
interface Measurement {
  id: string;
  value: number;
}

class Log<T> {
  private records: T[] = [];

  add(record: T): void {
    this.records.push(record);
  }

  last(): T | undefined {
    return this.records.at(-1);
  }

  get count(): number {
    return this.records.length;
  }
}

const measurements = new Log<Measurement>();
measurements.add({ id: "s-01", value: 21.4 });
console.log(measurements.last()?.value, measurements.count);

const warnings = new Log<string>();
warnings.add("sensor not responding");
console.log(warnings.last()?.toUpperCase());
```

Output:

```text
21.4 1
SENSOR NOT RESPONDING
```

A single class was used with two different record types, and type information was kept
at every use: the call `measurements.last()` gives `Measurement | undefined`, the call
`warnings.last()` gives `string | undefined`. If the line
`measurements.add("s-02");` is added at the end of the file, after a blank line:

```text
m1.ts(30,18): error TS2345: Argument of type 'string' is not assignable to parameter of type 'Measurement'.
```

An `any[]` solution would not have produced this diagnostic. Generics are the way to
share a common structure without losing type safety.

The return type being `T | undefined` is also part of the design: an `at(-1)` call on
an empty array gives `undefined`, and the type records that. The caller is forced to
handle that possibility with `?.`.

#### Type Argument Inference

The type argument is often left unwritten; the compiler infers it from the arguments:

```typescript
function first<T>(records: readonly T[]): T | undefined {
  return records[0];
}

const n = first([21.4, 22.1]);
const s = first(["s-01", "s-02"]);
console.log(n?.toFixed(1), s?.toUpperCase());
```

The output is `21.4 S-01`.

There is no need to write `first<number>` or `first<string>`. The declared type of `n`
is inferred as `number | undefined`, the type of `s` as `string | undefined`, and each
one's methods are available accordingly.

For inference to work, the type parameter must **appear in the arguments**. A type
parameter that appears only in the return type cannot be inferred; the danger of this
case is covered at the end of the lesson.

#### Constraints

A type parameter is a type about which nothing is known; no operation can be performed
on it. To access specific members, a **constraint** is written on the parameter:

```typescript
interface Identified {
  id: string;
}

function collectIds<T extends Identified>(records: readonly T[]): string[] {
  return records.map((r) => r.id);
}

console.log(collectIds([{ id: "s-01", value: 21.4 }]));
console.log(collectIds([{ name: "boiler-2" }]));
```

```text
m4.ts(10,27): error TS2353: Object literal may only specify known properties, and 'name' does not exist in type 'Identified'.
```

The declaration `T extends Identified` does two things: it makes the `r.id` access in
the body possible, and it rejects non-conforming arguments at the call site. The
acceptance of the ninth line also matters — the `value` field is extra, and a
constraint only states the minimum requirement.

Without the constraint, writing `r.id` would have been rejected by the compiler: `T`
could be any type, a string as much as a number.

#### Constraining with `keyof`

Constraints can also refer to other type parameters. The most useful form is built with
the `keyof` operator, which gives a type's field names:

```typescript
interface Measurement {
  id: string;
  value: number;
  unit: "C" | "Pa" | "%";
}

function field<T, A extends keyof T>(record: T, name: A): T[A] {
  return record[name];
}

const record: Measurement = { id: "s-01", value: 21.4, unit: "C" };
const v: number = field(record, "value");
const u: "C" | "Pa" | "%" = field(record, "unit");
console.log(v, u);

field(record, "location");
```

```text
m5.ts(16,15): error TS2345: Argument of type '"location"' is not assignable to parameter of type 'keyof Measurement'.
```

Three new notations appear here. `keyof T` is a union made of literal types of `T`'s
field names — here `"id" | "value" | "unit"`. The constraint `A extends keyof T` limits
the second argument to those names. `T[A]` is an **indexed access type**: it gives the
type of the field named `A`.

The result is a return type that varies by field name. The call `field(record,
"value")` has type `number`, the call `field(record, "unit")` has type `"C" | "Pa" |
"%"`. Without writing an overload, as many distinct signatures as there are fields have
been obtained.

#### Default Type Parameters

A type parameter can be given a default value. A type carrying the result of a
boundary validation is a typical use of this:

```typescript
type Result<D, E = string> =
  | { status: "success"; value: D }
  | { status: "error"; error: E };

interface Measurement {
  id: string;
  value: number;
}

function parse(raw: string): Result<Measurement> {
  const parts = raw.split("=");
  if (parts.length !== 2) {
    return { status: "error", error: `invalid format: ${raw}` };
  }
  const value = Number(parts[1]);
  if (!Number.isFinite(value)) {
    return { status: "error", error: `not a number: ${parts[1]}` };
  }
  return { status: "success", value: { id: parts[0], value } };
}

function format(result: Result<Measurement>): string {
  return result.status === "success"
    ? `${result.value.id}=${result.value.value}`
    : `error: ${result.error}`;
}

console.log(format(parse("s-01=21.4")));
console.log(format(parse("broken")));
console.log(format(parse("s-02=abc")));
```

Output:

```text
s-01=21.4
error: invalid format: broken
error: not a number: abc
```

In `Result<Measurement>`, the second type argument is not given; the default `string`
is used. When the error type needs detail, `Result<Measurement, ErrorCode>` is written
and the code outside `format` does not change.

This type is the matured form of the boundary rule set in the first lesson. The `parse`
function neither throws an exception nor returns `null`; it carries failure as **a
member of the type**, and the compiler forces the caller to handle it. The
discriminated union has turned into an error-handling tool here.

#### Misusing Generics

Generics do not improve every situation. There are two counter-examples.

**A type parameter that appears only once.** In the signature `function
print<T>(value: T): void`, `T` appears in only one parameter and forms no relationship;
writing `unknown` does the same job more explicitly. The value of a generic lies in
forming a **link** between two points: an input type and an output type, or two
parameters.

**A type parameter that appears only in the return type.** This is a hidden type
assertion:

```typescript
function unsafe<T>(raw: unknown): T {
  return raw as T;
}

const label = unsafe<string>(42);
try {
  console.log(label.toUpperCase());
} catch (e) {
  console.log((e as Error).message);
}
```

The file **passes** the type check; when run, however, a `TypeError` occurs and the
`catch` block prints its message:

```text
label.toUpperCase is not a function
```

Whatever the caller writes for `T`, the compiler accepts it, because the `as T` in the
body silences the check. This signature looks like it promises validation while
validating nothing — it is more dangerous than returning `any`, because it looks type
safe.

Rule: **a type parameter must appear in at least two places.** If it appears only in
the return type, the signature should return `unknown` and the caller should narrow it
themselves.

#### Summary

- A type parameter lets a structure's shape be written independently of the record
  type, and unlike an `any` solution, it preserves type safety.
- Type arguments are usually inferred from call arguments; inference requires the
  parameter to appear in the argument list.
- A constraint (`T extends K`) allows operations on a type parameter and rejects
  non-conforming arguments at the call site.
- `keyof T` gives the union of field names, `T[A]` the indexed access type; together
  they build return types that vary by field name.
- A default type parameter shortens the common case; a result type built with a
  discriminated union carries failure as a member of the type.
- A type parameter must appear in at least two places; a parameter appearing only in
  the return type is a hidden type assertion.

#### Next Step

Several times in this lesson, a test was performed on a union type and the compiler
narrowed it down to a single member. The rules of this narrowing have not yet been laid
out: which tests count as proof, how far the proof carries, how the result of an
assertion function you write yourself is communicated to the compiler. The next lesson
covers narrowing in detail.

### Type Narrowing

Source: https://academia.sh/en/courses/typescript/type-narrowing

Type narrowing through control flow analysis; the typeof, in, and instanceof guards, the trap of truthiness narrowing, type predicates, assertion functions, and situations where narrowing is lost.

In earlier lessons, a union type was tested several times and the compiler reduced it
to a single member. The rules of this reduction have not yet been laid out.

**Narrowing** is the compiler shrinking a value's type by looking at the tests in the
control flow. The mechanism behind it is called **control flow analysis**: the
compiler tracks, at every point in the program, which conditions have been proven.
This lesson covers which tests count as proof, how your own validation is communicated
to the compiler, and where the proof gets lost.

#### Built-in Guards

A **type guard** is a test that narrows a value's type. The most common one is the
`typeof` operator:

```typescript
function format(value: string | number | null): string {
  if (value === null) {
    return "-";
  }
  if (typeof value === "number") {
    return value.toFixed(1);
  }
  return value.trim();
}

console.log(format(21.4), format("  s-01  "), format(null));
```

The output is `21.4 s-01 -`.

Three branches are three narrowing steps. The `value === null` test narrows by
equality; after the first `return`, the remaining type is `string | number`. After the
`typeof value === "number"` test, the type in the body is `number` and `toFixed` can be
called. By the time the last line is reached, the only remaining possibility is
`string`; the `trim` call needs no further test.

The last step deserves attention: the compiler tracks **early returns**. A branch that
ends with a `return` statement eliminates that possibility from the code that follows.
The guard clause pattern introduced in the Programming Fundamentals course pays off at
the type level too.

Object types have two more guards. The `in` operator checks for field presence:

```typescript
type NumericMeasurement = { id: string; value: number };
type TextMeasurement = { id: string; text: string };

function write(record: NumericMeasurement | TextMeasurement): string {
  if ("value" in record) {
    return record.value.toFixed(1);
  }
  return record.text.trim();
}

console.log(write({ id: "s-01", value: 21.4 }));
console.log(write({ id: "s-02", text: "  broken  " }));
```

Output:

```text
21.4
broken
```

The `instanceof` operator distinguishes class instances:

```typescript
class FileSource {
  constructor(readonly path: string) {}
}

class NetworkSource {
  constructor(readonly address: string) {}
}

function describe(source: FileSource | NetworkSource): string {
  if (source instanceof FileSource) {
    return `file: ${source.path}`;
  }
  return `network: ${source.address}`;
}

console.log(describe(new FileSource("/data/measurement.jsonl")));
console.log(describe(new NetworkSource("10.0.0.7:9000")));
```

Output:

```text
file: /data/measurement.jsonl
network: 10.0.0.7:9000
```

What these three have in common is that all of them are tests that **actually run at
run time**. Narrowing is not an assumption of the type system; it is a test in the code
reflected at the type level.

#### The Trap of Truthiness Narrowing

Checking a value's truthiness also narrows, but it eliminates more than expected:

```typescript
function label(value: number | undefined): string {
  if (!value) {
    return "no measurement";
  }
  return value.toFixed(1);
}

console.log(label(21.4));
console.log(label(0));
console.log(label(undefined));
```

Output:

```text
21.4
no measurement
no measurement
```

This file passes the type check, but the second output is wrong: `0` is a valid
measurement. The list of falsy values established in the JavaScript Fundamentals
course comes into play here — `0`, `""`, and `NaN` also count as falsy, and the
`!value` test eliminates them too.

The type system does not catch this mistake, because the narrowing was done correctly:
after the `!value` block, the type really is `number`. The bug is not in the type, it
is in the choice of test.

Rule: **write an equality test for `undefined` and `null`.** `if (value === undefined)`
or `if (value == null)` — the latter eliminates both the `null` and the `undefined`
case, and this is the one defensible use of loose equality.

#### Type Predicates

When validation is moved into a separate function, narrowing is lost: the compiler
does not know what a function returning `boolean` has proven. A **type predicate**
carries this information in the signature:

```typescript
interface Measurement {
  id: string;
  value: number;
  unit: "C" | "Pa" | "%";
}

function isMeasurement(value: unknown): value is Measurement {
  if (typeof value !== "object" || value === null) {
    return false;
  }
  const candidate = value as Record<string, unknown>;
  return (
    typeof candidate.id === "string" &&
    typeof candidate.value === "number" &&
    (candidate.unit === "C" || candidate.unit === "Pa" || candidate.unit === "%")
  );
}

function parse(raw: string): string {
  const parsed: unknown = JSON.parse(raw);
  if (!isMeasurement(parsed)) {
    return "invalid record";
  }
  return `${parsed.id}=${parsed.value}${parsed.unit}`;
}

console.log(parse('{"id":"s-01","value":21.4,"unit":"C"}'));
console.log(parse('{"id":"s-02","value":"22.1","unit":"C"}'));
console.log(parse('{"id":"s-03","value":22.1,"unit":"K"}'));
```

Output:

```text
s-01=21.4C
invalid record
invalid record
```

The return type is written not as `boolean` but as `value is Measurement`. This tells
the compiler: "when this function returns `true`, accept that its argument is
`Measurement`." The result is that, inside `parse`, `parsed` stops being `unknown` and
becomes `Measurement`.

This is where the course's central point comes together: validation is done at run
time, and a type predicate lets that be recorded **as proof**. The model can now be
safely built from external data.

The predicate has a cost. The compiler does not check that the body actually proves
what it claims:

```typescript
interface Measurement {
  id: string;
  value: number;
}

function isMeasurement(value: unknown): value is Measurement {
  return typeof value === "object";
}

const candidate: unknown = null;
try {
  if (isMeasurement(candidate)) {
    console.log(candidate.value.toFixed(1));
  }
} catch (e) {
  console.log((e as Error).message);
}
```

The file passes the type check; when run, a `TypeError` occurs and the `catch` block
prints its message:

```text
Cannot read properties of null (reading 'value')
```

The body gives `typeof null === "object"` as `true` and produces a false proof. A type
predicate is a **debt**, much like `as`: whoever writes it is responsible for its
correctness. This is why predicate bodies are kept short and tested.

#### Assertion Functions

The predicate's second form throws on a value that fails the test:

```typescript
interface Measurement {
  id: string;
  value: number;
}

function assertIsMeasurement(value: unknown): asserts value is Measurement {
  if (
    typeof value !== "object" ||
    value === null ||
    typeof (value as Record<string, unknown>).id !== "string" ||
    typeof (value as Record<string, unknown>).value !== "number"
  ) {
    throw new TypeError("not a measurement record");
  }
}

const parsed: unknown = JSON.parse('{"id":"s-01","value":21.4}');
assertIsMeasurement(parsed);
console.log(parsed.id, parsed.value.toFixed(1));
```

The output is `s-01 21.4`.

The `asserts value is Measurement` declaration narrows the type in the case where the
call **returns normally**. No `if` block is needed; in all the code after the call,
`parsed` is a `Measurement`.

The choice between the two forms depends on whether invalid data is an expected
situation. The same distinction was made in the Type Conversion lesson of the
Programming Fundamentals course: invalidity in user input is ordinary and is handled
with a predicate; invalidity in data the program produces itself is exceptional and an
assertion function is appropriate.

There is one constraint: the name an assertion function is called through must be an
explicitly typed declaration. If the function is first assigned to an untyped constant
and called through that constant, the compiler gives the `TS2775` diagnostic: every
name at the target of an assertion call must carry an explicit type annotation.

#### Where Narrowing Is Lost

Control flow analysis works as long as it can assume the value has not changed since
the test. That assumption does not hold inside a closure:

```typescript
interface Measurement {
  id: string;
  last?: number;
}

function write(record: Measurement): void {
  if (record.last !== undefined) {
    console.log(record.last.toFixed(1));
    [1, 2].forEach(() => {
      console.log(record.last.toFixed(1));
    });
  }
}

write({ id: "s-01", last: 21.4 });
```

```text
n6.ts(10,19): error TS18048: 'record.last' is possibly 'undefined'.
```

The eighth line is valid, the tenth is not. The difference is that it is not known
**when** the callback will run: `forEach` runs synchronously, but the compiler cannot
know that, and the `record.last` field could have been deleted in between. The same
problem is even more visible in timers and asynchronous code.

The fix is to capture the narrowed value in a local constant:

```typescript
interface Measurement {
  id: string;
  last?: number;
}

function write(record: Measurement): void {
  const last = record.last;
  if (last !== undefined) {
    console.log(last.toFixed(1));
    [1, 2].forEach(() => {
      console.log(last.toFixed(1));
    });
  }
}

write({ id: "s-01", last: 21.4 });
```

A name bound with `const` cannot change; the compiler can carry the narrowing into the
closure. The output is three lines of `21.4`.

#### Summary

- Narrowing is control-flow tests reflected at the type level; `typeof`, `in`,
  `instanceof`, and equality tests are built-in guards.
- A truthiness test also eliminates valid values like `0` and `""`; an equality test
  should be written for `null` and `undefined`.
- A type predicate (`value is T`) communicates the result of validation moved into a
  separate function to the compiler; the body's correctness is not checked — the
  predicate is a debt.
- An assertion function (`asserts value is T`) throws on an invalid value and narrows
  the type when it returns normally.
- Narrowing does not carry into a closure; capturing the narrowed value in a local
  constant crosses that boundary.

#### Next Step

The various shapes of a measurement record have so far been written out by hand: a
read-only version, an update version with optional fields, a summary version carrying
only a few fields. Each is a transform of the original type, and they need to be
updated together whenever the type changes. The next lesson builds mapped types, which
compute these transforms at the type level.

### Mapped Types

Source: https://academia.sh/en/courses/typescript/mapped-types

Producing a new type from an existing one; adding and removing field modifiers, key remapping, filtering fields, and how mapping behaves on arrays and tuples.

The various shapes of a measurement record have so far been written out by hand: a
read-only version for archived records, a version with optional fields for an update
request, a summary carrying only a few fields for listing.

Each is a transform of the original type. When written by hand, they need to be
updated together whenever the original type changes, and that is a maintenance debt
easy to forget. A **mapped type** computes the transform at the type level: the field
list stays in one place, and the derived types update themselves.

#### Basic Notation

A mapped type resembles a loop iterating over a set of keys:

```typescript
interface Measurement {
  id: string;
  value: number;
  unit: "C" | "Pa" | "%";
}

type Locked<T> = { readonly [A in keyof T]: T[A] };
type Patchable<T> = { [A in keyof T]?: T[A] };

const archived: Locked<Measurement> = {
  id: "s-01",
  value: 21.4,
  unit: "C",
};
console.log(archived.value);

const patch: Patchable<Measurement> = { value: 22.1 };
console.log(patch.value, patch.id);
```

Output:

```text
21.4
22.1 undefined
```

If the line `archived.value = 22.1;` is added at the end of the file, after a blank
line:

```text
o2.ts(20,10): error TS2540: Cannot assign to 'value' because it is a read-only property.
```

The notation has these parts. The `[A in keyof T]` part binds each member of the
`keyof T` union to the name `A` in turn — the type-level counterpart of the `for` loop
from the Programming Fundamentals course. `T[A]` after the colon is that field's
original type. The `readonly` written at the front and the `?` appended to the name
are **modifiers** applied to every field.

Because every field of `Patchable<Measurement>` is optional, the literal
`{ value: 22.1 }` is accepted; the `patch.id` access has type `string | undefined`, and
`undefined` appears in the output.

The counterparts of these two transforms already exist in the standard library
(`Readonly` and `Partial`); they are covered in the Utility Types lesson. The reason
they are written by hand here is to make the mechanism visible.

#### Removing a Modifier

Just as modifiers can be added, they can be removed. Removal is written by putting a
minus sign in front of the modifier:

```typescript
interface Measurement {
  readonly id: string;
  readonly value?: number;
}

type Resolved<T> = { -readonly [A in keyof T]-?: T[A] };

const full: Resolved<Measurement> = { id: "s-01", value: 21.4 };
full.value = 22.1;
console.log(full.value.toFixed(1));
```

The output is `22.1`. If an assignment that omits the required field is tried with the
same definitions in a separate file (`o4.ts`):

```typescript
// o4.ts — missing field in Resolved
interface Measurement {
  readonly id: string;
  readonly value?: number;
}

type Resolved<T> = { -readonly [A in keyof T]-?: T[A] };

const missing: Resolved<Measurement> = { id: "s-02" };
```

```text
o4.ts(9,7): error TS2741: Property 'value' is missing in type '{ id: string; }' but required in type 'Resolved<Measurement>'.
```

In type `Measurement`, `id` is read-only and `value` was both read-only and optional.
In type `Resolved<Measurement>`, both are writable and required. The result is that the
`full.value.toFixed(1)` call needs no narrowing: the field is now `number`, not
`number | undefined`.

This transform's use case is representing the completed form of a partial record.
Rather than writing a separate type for the record obtained after defaults have been
applied, it is computed from the original type.

#### Key Remapping

The `as` notation lets the name of the produced field be changed:

```typescript
interface Measurement {
  id: string;
  value: number;
  unit: "C" | "Pa" | "%";
}

type Readers<T> = {
  [A in keyof T as `read${Capitalize<string & A>}`]: () => T[A];
};

const reader: Readers<Measurement> = {
  readId: () => "s-01",
  readValue: () => 21.4,
  readUnit: () => "C",
};

console.log(reader.readValue().toFixed(1), reader.readId());
```

The output is `21.4 s-01`.

The produced type declares a reader function for every field and takes the return type
from the original field: the call `reader.readValue()` gives `number`, the call
`reader.readUnit()` gives `"C" | "Pa" | "%"`. The template literal notation used to
produce the name, along with the `Capitalize` transform, is the subject of the next
lesson; here it should only be seen that the name can be computed.

Remapping's second and more powerful use is **filtering out** a field. When the
produced name is `never`, that field does not enter the output:

```typescript
interface Measurement {
  id: string;
  value: number;
  unit: "C" | "Pa" | "%";
  time: number;
}

type NumericFields<T> = {
  [A in keyof T as T[A] extends number ? A : never]: T[A];
};

const numeric: NumericFields<Measurement> = { value: 21.4, time: 1706000000000 };
console.log(numeric.value + numeric.time);
```

The output is `1706000000021.4`. If an assignment that also supplies a filtered-out
field is tried with the same definitions in a separate file (`o7.ts`):

```typescript
// o7.ts — extra field in NumericFields
interface Measurement {
  id: string;
  value: number;
  unit: "C" | "Pa" | "%";
  time: number;
}

type NumericFields<T> = {
  [A in keyof T as T[A] extends number ? A : never]: T[A];
};

const wrong: NumericFields<Measurement> = { value: 21.4, time: 1, id: "s-01" };
```

```text
o7.ts(13,67): error TS2353: Object literal may only specify known properties, and 'id' does not exist in type 'NumericFields<Measurement>'.
```

The resulting type carries only the `value` and `time` fields; `id` and `unit` have
been filtered out. The notation `T[A] extends number ? A : never` is a **conditional
type** and is the subject of one of the next lessons; here its job is to keep the name
based on the field's type or produce `never` and filter it out.

This type can be used in the signature of code that extracts a numeric summary of a
measurement record. When a new numeric field is added to the record, the summary type
grows on its own; when a string field is added, it is unaffected.

#### Mapping over Arrays and Tuples

When a mapping iterating over `keyof T` is applied to an array, nothing unexpected
**happens**: the result is still an array.

```typescript
type Locked<T> = { readonly [A in keyof T]: T[A] };

const values: Locked<number[]> = [21.4, 22.1];
const range: Locked<[min: number, max: number]> = [-40, 85];

console.log(values.length, values[0], range[1]);
console.log(values.map((d) => d * 2));
```

Output:

```text
2 21.4 85
[ 42.8, 44.2 ]
```

`Locked<number[]>` is the type `readonly number[]`, `Locked<[number, number]>` is a
read-only tuple. Array methods (`map`, `length`, indexed access) remain usable, mutating
methods are gone:

```typescript
type Locked<T> = { readonly [A in keyof T]: T[A] };

const values: Locked<number[]> = [21.4, 22.1];
values.push(23.0);
```

```text
u4.ts(4,8): error TS2339: Property 'push' does not exist on type 'readonly number[]'.
```

This behavior holds when the mapping's source is **directly** `keyof T`. The compiler
recognizes this form and keeps arrays and tuples in their own kind. When the source is
written as any other expression, the recognition breaks:

```typescript
type MapA<T> = { readonly [A in keyof T]: T[A] };
type MapB<T> = { readonly [A in keyof T & string]: T[A] };

declare const a: MapA<number[]>;
declare const b: MapB<number[]>;

const x: null = a;
const y: null = b;
```

```text
u11.ts(7,7): error TS2322: Type 'readonly number[]' is not assignable to type 'null'.
u11.ts(8,7): error TS2322: Type 'MapB<number[]>' is not assignable to type 'null'.
```

The difference between the two diagnostics is decisive. `MapA<number[]>` resolved to
`readonly number[]` — the compiler is printing the result as an array. `MapB<number[]>`
stayed an unresolved mapping — the array structure was lost, replaced by an ordinary
object type carrying the array's property names.

Practical rule: **if a type's structure needs to be preserved, the mapping source is
written as `keyof T`.** If keys need to be filtered, the filtering is done with an
`as` remapping — the source stays `keyof T`, and the name computation lives on the
right side.

#### Mapping over Any Union

The mapping source does not have to be `keyof T`; any string union can be used:

```typescript
type Unit = "C" | "Pa" | "%";

type Intervals = {
  [B in Unit]: [min: number, max: number];
};

const intervals: Intervals = {
  C: [-40, 85],
  Pa: [0, 200000],
  "%": [0, 100],
};

console.log(intervals.C[1], intervals["%"][1]);
```

The output is `85 100`. If an assignment that omits the `"%"` key is tried with the same
definitions in a separate file (`o9.ts`):

```typescript
// o9.ts — missing key in Intervals
type Unit = "C" | "Pa" | "%";

type Intervals = {
  [B in Unit]: [min: number, max: number];
};

const missing: Intervals = { C: [-40, 85], Pa: [0, 200000] };
```

```text
o9.ts(8,7): error TS2741: Property '"%"' is missing in type '{ C: [number, number]; Pa: [number, number]; }' but required in type 'Intervals'.
```

This is the correct alternative to the index signature criticized in the Interfaces
lesson. An index signature says "every string key is valid" and would not have
counted access to a nonexistent key as an error. A mapped type, by contrast, fixes the
key set **exactly**: a missing key produces a diagnostic, and so does an extra one.

When a new unit is added to the `Unit` union, updating the interval table becomes
mandatory. This is the data-table counterpart of the exhaustiveness checking built with
`never` in a discriminated union.

#### Summary

- A mapped type iterates over a key set and produces a new object type; derived types
  update together with the original type.
- The `readonly` and `?` modifiers can be added during mapping and removed with the
  `-readonly` and `-?` notations.
- The `as` notation computes the produced field's name; when the name is `never`, the
  field is filtered out of the output.
- The mapping source does not have to be `keyof T`; mapping can be done over any
  string union.
- When the mapping source is directly `keyof T`, arrays and tuples stay in their own
  kind; when the source is written as another expression, this structure is lost.
- For a fixed key set, a mapped type is safer than an index signature: both a missing
  and an extra key are reported.

#### Next Step

In the previous two examples, a notation of the form `T[A] extends number ? A : never`
appeared and branched at the type level. This is the structure at the center of the
type system's computing ability. The next lesson covers conditional types, distribution
rules, and the `infer` notation that extracts a piece from within a type.

### Conditional Types

Source: https://academia.sh/en/courses/typescript/conditional-types

Type-level branching, the distribution rule over unions and closing off distribution, extracting a type piece with infer, and the limit of conditional return types.

The previous lesson used a notation of the form `T[A] extends number ? A : never` and
branched at the type level. A **conditional type** is the structure at the center of
the type system's computing ability: it picks one of two types depending on whether one
type is assignable to another.

Its notation is the type-level counterpart of the ternary operator:

$$
T \text{ extends } U \;?\; X : Y
$$

This lesson covers how the condition is evaluated, its special behavior over unions,
extracting a piece with `infer`, and the limits of the structure.

#### Branching at the Type Level

```typescript
type IsNumber<T> = T extends number ? "number" : "not a number";

const a: IsNumber<number> = "number";
const b: IsNumber<string> = "not a number";
console.log(a, b);

const c: IsNumber<number> = "not a number";
```

```text
p1.ts(7,7): error TS2322: Type '"not a number"' is not assignable to type '"number"'.
```

`IsNumber<number>` resolves to `"number"`, `IsNumber<string>` to `"not a number"`. The
seventh line's diagnostic shows the resolution really happened: the compiler reports
the error by writing `"number"` in place of `IsNumber<number>`.

The word `extends` here does not mean inheritance but an **assignability** test: "can
`T` be put where `U` is expected?" The rules established in the Structural Type
Compatibility lesson apply here.

#### Distribution

If a conditional type's type parameter is a union, the condition is evaluated
**separately** for each member and the results are combined into a union. This is
called **distribution**:

```typescript
type IsNumber<T> = T extends number ? "number" : "not a number";
type NonDistributive<T> = [T] extends [number] ? "number" : "not a number";

declare const distributed: IsNumber<number | string>;
declare const sealed: NonDistributive<number | string>;

const x: null = distributed;
const y: null = sealed;
```

```text
p2.ts(7,7): error TS2322: Type '"not a number" | "number"' is not assignable to type 'null'.
  Type '"not a number"' is not assignable to type 'null'.
p2.ts(8,7): error TS2322: Type '"not a number"' is not assignable to type 'null'.
```

The diagnostics spell out both types explicitly. `IsNumber<number | string>` has become
`"not a number" | "number"`; the condition was evaluated separately for each member and
the results were combined. The order of the members in the diagnostic is the compiler's
own order. `NonDistributive<number | string>` gave a single result: `"not a number"`.

What closes off distribution is **wrapping the type parameter in a tuple**. In the
notation `[T] extends [U]`, what is being tested is no longer `T` but a single-element
tuple; it does not split into union members, and the condition is evaluated once.

Which of the two behaviors is wanted depends on intent. If every member of a union
needs to be transformed, distribution is needed. If the question being asked is "does
**all** of this type satisfy this condition?", it should be closed off — otherwise a
meaningless result like "partly a number" comes out for the `number | string` union.

Distribution only applies to **bare** type parameters: the left side of the condition
must be directly `T`. There is no distribution in notations like `T[]`, `[T]`, or
`{ value: T }`.

#### Filtering: `Drop`

Distribution's most common use is eliminating a member from a union. `never` is
produced for the member to be filtered; `never` takes up no space in a union:

```typescript
type Drop<T, U> = T extends U ? never : T;

type Unit = "C" | "Pa" | "%";
type MeasurableUnit = Drop<Unit, "%">;

const units: MeasurableUnit[] = ["C", "Pa"];
console.log(units.join(","));
```

The output is `C,Pa`. If the line that tries to assign the filtered-out member is tried
with the same definitions in a separate file (`p5.ts`):

```typescript
// p5.ts — member removed by Drop
type Drop<T, U> = T extends U ? never : T;

type Unit = "C" | "Pa" | "%";
type MeasurableUnit = Drop<Unit, "%">;

const wrong: MeasurableUnit = "%";
```

```text
p5.ts(7,7): error TS2322: Type '"%"' is not assignable to type 'MeasurableUnit'.
```

The steps of the computation are as follows: the condition is evaluated separately for
the three members; `"C"` and `"Pa"` are not assignable to `"%"` so they produce
themselves, and `"%"` produces `never`. The result is `"C" | "Pa" | never`, which is
`"C" | "Pa"`.

This transform's standard-library counterpart is `Exclude`, covered in the Utility
Types lesson.

#### Extracting a Piece with `infer`

A type variable can be declared on the left side of the condition with the `infer`
keyword. During matching, the compiler finds the type corresponding to that variable:

```typescript
interface Measurement {
  id: string;
  value: number;
}

type ElementType<T> = T extends readonly (infer E)[] ? E : never;

declare const element: ElementType<Measurement[]>;
declare const stringElement: ElementType<readonly string[]>;
declare const nonArray: ElementType<number>;

const a: null = element;
const b: null = stringElement;
const c: null = nonArray;
```

```text
p3.ts(12,7): error TS2322: Type 'Measurement' is not assignable to type 'null'.
p3.ts(13,7): error TS2322: Type 'string' is not assignable to type 'null'.
```

`ElementType<Measurement[]>` has become `Measurement`, `ElementType<readonly
string[]>` has become `string`. The fourteenth line producing no diagnostic also
carries information: `ElementType<number>` is `never`, and `never` is assignable to
every type — including `null`.

The same technique is used to extract a payload type from a course-wide result type:

```typescript
interface Measurement {
  id: string;
  value: number;
}

type Result<D, E = string> =
  | { status: "success"; value: D }
  | { status: "error"; error: E };

type SuccessType<S> = S extends { status: "success"; value: infer D } ? D : never;
type ErrorType<S> = S extends { status: "error"; error: infer E } ? E : never;

declare const success: SuccessType<Result<Measurement>>;
declare const error: ErrorType<Result<Measurement, number>>;

const a: null = success;
const b: null = error;
```

```text
p4.ts(16,7): error TS2322: Type 'Measurement' is not assignable to type 'null'.
p4.ts(17,7): error TS2322: Type 'number' is not assignable to type 'null'.
```

Distribution is at work here too: `Result<Measurement>` is a two-member union;
computing `SuccessType` gives `Measurement` for the success member, `never` for the
error member, and the union reduces to `Measurement`.

The value of this pattern is being able to reach into a piece of a type **without
repeating its definition**. If `Result`'s structure changes, `SuccessType` changes with
it; no manually maintained link remains between the two.

#### The Limit of Conditional Return Types

Conditional types can be used in function signatures, but they do not resolve inside
the body:

```typescript
type Formatted<T> = T extends number ? string : number;

function format<T extends number | boolean>(value: T): Formatted<T> {
  if (typeof value === "number") {
    return value.toFixed(1);
  }
  return 0;
}

console.log(format(21.4), format(true));
```

```text
p7.ts(5,5): error TS2322: Type 'string' is not assignable to type 'Formatted<T>'.
p7.ts(7,3): error TS2322: Type 'number' is not assignable to type 'Formatted<T>'.
```

Inside the body, `Formatted<T>` cannot be resolved because `T` is not yet known; the
compiler cannot infer that the `typeof value` test also narrows `T`. The diagnostic
comes on both branches.

This is a known limit of the type system. There are two fixes: using a single type
assertion in the body — a debt that makes the limit visible and accepts that the body
honors the promise made in the signature — or writing an overload instead of a
conditional type. The second is the structure built in the Function Types lesson, and
gives callers the same result.

General principle: conditional types are powerful for **computing a type**, not for
**driving an implementation**.

#### Summary

- A conditional type picks one of two types based on an assignability test; `extends`
  here means assignability, not inheritance.
- If a bare type parameter is a union, the condition is evaluated separately for each
  member and the results are combined into a union; the notation `[T] extends [U]`
  closes off this distribution.
- Distribution together with `never` builds filters that eliminate a member from a
  union.
- `infer` declares a type variable on the left side of the condition and extracts the
  type corresponding to it during matching; it lets a piece of a type be reached
  without repeating its definition.
- Conditional return types do not resolve inside the function body; this case needs
  either a type assertion or an overload.

#### Next Step

The key remapping lesson used a notation of the form `` `read${Capitalize<string &
A>}` `` and computed a field name. Combining and parsing strings at the type level is
a separate toolset. The next lesson covers template literal types and how they are
used together with `infer` to parse string patterns.

### Template Literal Types

Source: https://academia.sh/en/courses/typescript/template-literal-types

Expressing string formats as types, the cross product over unions, built-in string transforms, parsing a format with infer, open-ended patterns, and a literal type dissolving inside string.

The mapped types lesson used the notation `` `read${Capitalize<string & A>}` `` and
computed a field name. A **template literal type** combines string literal types to
produce new literal types.

Its notation is the same as JavaScript's template literals, except what is combined is
types, not values. This lesson covers how string formats are typed with this tool, its
behavior over unions, and its limits.

#### Defining a Format

Measurement records are often referred to by a channel name: which sensor, in which
unit. This name carries a format, and the format can be typed:

```typescript
type Sensor = "temperature" | "pressure";
type Unit = "C" | "Pa";

type Channel = `${Sensor}.${Unit}`;

const channels: Channel[] = ["temperature.C", "temperature.Pa", "pressure.C", "pressure.Pa"];
console.log(channels.length, channels[0]);
```

The output is `4 temperature.C`. If an assignment that does not match the format is
tried with the same definitions in a separate file (`q1.ts`):

```typescript
// q1.ts — channel name that breaks the format
type Sensor = "temperature" | "pressure";
type Unit = "C" | "Pa";

type Channel = `${Sensor}.${Unit}`;

const wrong: Channel = "temperature.%";
```

```text
q1.ts(7,7): error TS2820: Type '"temperature.%"' is not assignable to type '"pressure.C" | "pressure.Pa" | "temperature.C" | "temperature.Pa"'. Did you mean '"temperature.C"'?
```

The diagnostic writes out the computation's result explicitly: type `Channel` is a
union of four literal types. When every substituted type is a union, the result is the
**cross product** of those unions — two sensors and two units give four channels.

The suggestion at the end of the diagnostic is also a convenience the compiler offers
for literal type unions: for a value that is likely a typo, it shows the nearest valid
literal.

This type records that the string is not just any string, but carries a specific
format. It serves the same purpose as the branded type from the Structural Type
Compatibility lesson, but because the format can actually be defined, no type
assertion is needed.

#### Built-in String Transforms

The compiler provides four string transforms built in: `Uppercase`, `Lowercase`,
`Capitalize`, and `Uncapitalize`.

```typescript
type Sensor = "temperature" | "pressure";

type UppercaseSensor = Uppercase<Sensor>;
type EventName = `measurement${Capitalize<Sensor>}Taken`;

const uppercase: UppercaseSensor = "TEMPERATURE";
const eventName: EventName = "measurementPressureTaken";
console.log(uppercase, eventName);
```

The output is `TEMPERATURE measurementPressureTaken`. If an event name with the wrong
letter case is tried with the same definitions in a separate file (`q3.ts`):

```typescript
// q3.ts — event name with the wrong letter case
type Sensor = "temperature" | "pressure";

type UppercaseSensor = Uppercase<Sensor>;
type EventName = `measurement${Capitalize<Sensor>}Taken`;

const wrongEvent: EventName = "measurementpressureTaken";
```

```text
q3.ts(7,7): error TS2820: Type '"measurementpressureTaken"' is not assignable to type '"measurementPressureTaken" | "measurementTemperatureTaken"'. Did you mean '"measurementPressureTaken"'?
```

These transforms are built into the compiler; they cannot be written at the type level.
Case conversion follows a fixed, locale-independent rule — so locale-specific rules,
such as Turkish's distinction between dotless `ı` and dotted `i`, do not apply here.
This is one reason to stay in ASCII for channel and event names.

#### Building a Key Table

A template literal type can be the key source of a mapped type. The result is a table
with a defined and complete format:

```typescript
type Sensor = "temperature" | "pressure";
type Unit = "C" | "Pa";
type Channel = `${Sensor}.${Unit}`;

type ChannelTable = {
  [K in Channel]: number;
};

const lastValues: ChannelTable = {
  "temperature.C": 21.4,
  "temperature.Pa": 0,
  "pressure.C": 0,
  "pressure.Pa": 101325,
};

console.log(lastValues["pressure.Pa"], Object.keys(lastValues).length);
```

The output is `101325 4`.

The table's keys were not counted by hand; when a third sensor is added to the `Sensor`
union, the table stays incomplete and the compiler gives a diagnostic. Expanding the
measurement model forces the data structures that use the model to be updated.

#### Parsing a Format with `infer`

Template literal types can also be used on the left side of a conditional type.
`infer` extracts the pieces of the format:

```typescript
type SensorName<K> = K extends `${infer S}.${string}` ? S : never;
type UnitName<K> = K extends `${string}.${infer U}` ? U : never;

type Channel = "temperature.C" | "pressure.Pa";

declare const s: SensorName<Channel>;
declare const u: UnitName<Channel>;

const x: null = s;
const y: null = u;
```

```text
q5.ts(9,7): error TS2322: Type '"pressure" | "temperature"' is not assignable to type 'null'.
  Type '"pressure"' is not assignable to type 'null'.
q5.ts(10,7): error TS2322: Type '"C" | "Pa"' is not assignable to type 'null'.
  Type '"C"' is not assignable to type 'null'.
```

Parsing worked together with distribution: each member of the `Channel` union was
matched separately, and the results were combined into a union.

This type can be used in the signature of a run-time parse:

```typescript
type Sensor = "temperature" | "pressure";
type Unit = "C" | "Pa";
type Channel = `${Sensor}.${Unit}`;

type SensorName<K extends Channel> = K extends `${infer S}.${string}` ? S : never;

function sensorName<K extends Channel>(channel: K): SensorName<K> {
  return channel.split(".")[0] as SensorName<K>;
}

const result = sensorName("pressure.Pa");
console.log(result);
```

The output is `pressure`. If an assignment that pairs the return type with the wrong
literal type is tried with the same definitions in a separate file (`q6.ts`):

```typescript
// q6.ts — assignment that contradicts the return type
type Sensor = "temperature" | "pressure";
type Unit = "C" | "Pa";
type Channel = `${Sensor}.${Unit}`;

type SensorName<K extends Channel> = K extends `${infer S}.${string}` ? S : never;

function sensorName<K extends Channel>(channel: K): SensorName<K> {
  return channel.split(".")[0] as SensorName<K>;
}

const wrongAssign: "temperature" = sensorName("pressure.Pa");
```

```text
q6.ts(12,7): error TS2322: Type '"pressure"' is not assignable to type '"temperature"'.
```

The call `sensorName("pressure.Pa")` has type `"pressure"`, not `string`. The return
type was computed from the literal type given at the call site.

Notice the `as SensorName<K>` in the body: the limit established in the previous lesson
applies here too, a conditional return type does not resolve inside the body. This is a
debt that accepts the body honors the promise made in the signature, and because it is
collected in a single line, it stays testable.

#### Open-Ended Patterns

The substituted type does not have to be a union; `string` can be written too. The
result is not a fixed set but a **pattern type**:

```typescript
type MeasurementEvent = `measurement:${string}`;
type WarningEvent = `warning:${string}`;
type Occurrence = MeasurementEvent | WarningEvent;

function route(occurrence: Occurrence): string {
  if (occurrence.startsWith("measurement:")) {
    return `logged: ${occurrence}`;
  }
  return `reported: ${occurrence}`;
}

console.log(route("measurement:received"));
console.log(route("warning:threshold-exceeded"));
```

Output:

```text
logged: measurement:received
reported: warning:threshold-exceeded
```

The type `` `measurement:${string}` `` covers **every** string starting with
`"measurement:"`. Because it is not a closed set, it cannot be enumerated one by one
like in the previous section; in exchange, the prefix requirement is preserved. If the
line `const wrong: MeasurementEvent = "warning:received";` is added at the end of the
file, after a blank line:

```text
u12.ts(15,7): error TS2322: Type '"warning:received"' is not assignable to type '`measurement:${string}`'.
```

This pattern is used for strings whose body is free but whose format is binding, such
as event names or key prefixes. There is no need to keep a fixed list; the compiler
reports it when the prefix is misspelled.

#### A Literal Type Dissolving Inside `string`

Open-ended patterns come with a trap. A literal type **disappears** when it is put in
the same union as `string`:

```typescript
type Channel = "temperature.C" | string;

declare const channel: Channel;
const x: null = channel;
```

```text
u8.ts(4,7): error TS2322: Type 'string' is not assignable to type 'null'.
```

The diagnostic writes `string` instead of `Channel`: since the `"temperature.C"`
member is already inside the `string` set, the union has reduced to `string`. The
result is a type carrying no constraint at all — while the way it is written gives the
impression of offering a suggestion list.

This is a mistake often made when trying to model "known values plus free text." The
right tool is a closed union or an open-ended pattern, used exactly where the
constraint is actually wanted:

```typescript
type Channel = `${"temperature" | "pressure"}.${"C" | "Pa"}`;

function parseUnit(channel: Channel): string {
  return channel.split(".")[1];
}

const channels: Channel[] = ["temperature.C", "pressure.Pa"];
console.log(channels.map(parseUnit).join(","));
```

The output is `C,Pa`. If a free string is passed to this function after the same
definitions (separate file: `u13.ts`):

```typescript
// u13.ts — passing a free string to a closed set
type Channel = `${"temperature" | "pressure"}.${"C" | "Pa"}`;

function parseUnit(channel: Channel): string {
  return channel.split(".")[1];
}

const free: string = "temperature.C";
parseUnit(free);
```

```text
u13.ts(9,11): error TS2345: Argument of type 'string' is not assignable to parameter of type '"pressure.C" | "pressure.Pa" | "temperature.C" | "temperature.Pa"'.
```

The literal types have not dissolved here: the type is still a closed four-value set,
and a free string is not accepted. Adding `string` to a literal type union is exactly
the operation that removes that guarantee.

#### The Union Size Limit

The cross product grows fast. Two three-member unions give nine results; every extra
component multiplies the factor. The compiler has an upper limit:

```typescript
type Letter = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j";
type Combination = `${Letter}${Letter}${Letter}${Letter}${Letter}`;
declare const x: Combination;
const y: null = x;
```

```text
q9.ts(2,20): error TS2590: Expression produces a union type that is too complex to represent.
```

Ten letters combined five at a time is a hundred thousand literal types, and the
compiler rejects it. The limit can change by version; the real lesson is that a
template literal type is suitable when the format is **closed**, and unsuitable when
it is open.

The combinatorial explosion concept from the Algorithms course shows up here in the
type checker itself. For freely formatted strings, the right tool is not a literal
type union but run-time validation and a branded type.

#### Summary

- A template literal type defines string formats at the type level by combining
  literal types; when the substituted types are unions, the result is the cross
  product of those unions.
- `Uppercase`, `Lowercase`, `Capitalize`, and `Uncapitalize` are built into the
  compiler and work independently of locale.
- When a template literal type is used as a mapped type's key source, a table with a
  defined and complete format is obtained; on the left side of a conditional type,
  `infer` extracts format pieces and the result distributes over unions.
- When `string` is substituted in, the type becomes not a closed set but an
  open-ended pattern type; the prefix requirement is preserved, values cannot be
  enumerated.
- A literal type dissolves when placed in the same union as `string`, and the union
  reduces to `string`; "known values plus free text" cannot be modeled this way.
- The cross product grows fast, and past a certain size the compiler gives the
  `TS2590` diagnostic; closed literal type sets can only be built at a limited size.

#### Next Step

Most of the transforms written by hand in this topic — a read-only version, an
optional version, filtering a member out of a union — already exist as ready-made
types in the standard library. The next lesson introduces these utility types, shows
which mechanism each is built on, and rewrites the measurement model's derivatives
using them.

### Utility Types

Source: https://academia.sh/en/courses/typescript/utility-types

The standard library's ready-made type transforms; field modifiers, picking and omitting fields, building a table, filtering unions, and tools that derive types from signatures.

Most of the transforms written by hand in this topic — a read-only version, an
optional version, filtering a member out of a union — are already available in the
standard library. These are called **utility types**.

All of them are written with the mechanisms built in earlier lessons: mapped types,
conditional types, `keyof`, and `infer`. This lesson covers what each one does, how it
is built, and how it is used to write the derivatives of the measurement model.

#### Field Modifiers and Field Selection

The four most commonly used transforms produce four derivatives of the measurement
model in a single line:

```typescript
interface Measurement {
  id: string;
  sensor: string;
  value: number;
  unit: "C" | "Pa" | "%";
  time: number;
}

type MeasurementSummary = Pick<Measurement, "id" | "value" | "unit">;
type MeasurementInput = Omit<Measurement, "id" | "time">;
type MeasurementPatch = Partial<Omit<Measurement, "id">>;
type ArchivedRecord = Readonly<Measurement>;

const summary: MeasurementSummary = { id: "s-01", value: 21.4, unit: "C" };
const input: MeasurementInput = { sensor: "temperature", value: 21.4, unit: "C" };
const patch: MeasurementPatch = { value: 22.1 };
const archived: ArchivedRecord = {
  id: "s-01",
  sensor: "temperature",
  value: 21.4,
  unit: "C",
  time: 1706000000000,
};

console.log(summary.id, input.sensor, patch.value, archived.time);
```

The output is `s-01 temperature 22.1 1706000000000`.

The four types mean the following. `MeasurementSummary` carries the three fields a
listing screen needs. `MeasurementInput` is the fields given when creating a new
record — the id and timestamp are produced by the system. `MeasurementPatch` is a
partial update request; every field except id can be given, and none is required.
`ArchivedRecord` is a record that cannot be changed.

Because all four are computed from the original type, the corresponding derivatives
grow on their own when a new field is added to `Measurement`. Had they been written by
hand, this link would have broken.

Two diagnostics show that the constraints are really enforced:

```typescript
interface Measurement {
  id: string;
  value: number;
  time: number;
}

type MeasurementInput = Omit<Measurement, "id">;
type ArchivedRecord = Readonly<Measurement>;

const input: MeasurementInput = { id: "s-01", value: 21.4, time: 1 };

declare const archived: ArchivedRecord;
archived.value = 22.1;
```

```text
r2.ts(10,35): error TS2353: Object literal may only specify known properties, and 'id' does not exist in type 'MeasurementInput'.
r2.ts(13,10): error TS2540: Cannot assign to 'value' because it is a read-only property.
```

The definitions of these utility types are the same ones written by hand in the mapped
types lesson:

| Utility type | Equivalent |
|---|---|
| `Partial<T>` | `{ [A in keyof T]?: T[A] }` |
| `Required<T>` | `{ [A in keyof T]-?: T[A] }` |
| `Readonly<T>` | `{ readonly [A in keyof T]: T[A] }` |
| `Pick<T, A>` | `{ [B in A]: T[B] }` |
| `Record<A, D>` | `{ [B in A]: D }` |

`Omit` is not in this list, because it is built not directly by mapping but by a
composition: `Pick<T, Exclude<keyof T, A>>`. The names to exclude are first removed
from the `keyof T` union, and the remaining names are picked.

#### The Checking Difference Between `Omit` and `Pick`

This composition has a result that is easy to miss:

```typescript
interface Measurement {
  id: string;
  value: number;
}

type A = Omit<Measurement, "missingField">;
type B = Pick<Measurement, "missingField">;
```

```text
r3.ts(7,28): error TS2344: Type '"missingField"' does not satisfy the constraint 'keyof Measurement'.
```

The diagnostic comes only for `Pick`. `Omit`'s second parameter is constrained not by
`keyof T` but by any property key; a nonexistent field name is silently ignored, and
the result is the same as `Measurement`.

The maintenance cost of this is: when a field of `Measurement` is renamed, the `Omit`
notations that exclude it silently become ineffective, and the field that should have
been excluded comes back. In `Pick` notations, the compiler gives a diagnostic.

Practical consequence: when the field set is small, `Pick` is preferred. If `Omit` is
needed, a wrapper that tightens the constraint can be written:

```typescript
interface Measurement {
  id: string;
  value: number;
}

type Without<T, A extends keyof T> = Omit<T, A>;

type Safe = Without<Measurement, "id">;
const value: Safe = { value: 21.4 };
console.log(value.value);
```

The output is `21.4`. If a line that tries to exclude a nonexistent field with the same
wrapper is tried in a separate file (`r7.ts`):

```typescript
// r7.ts — excluding a nonexistent field
interface Measurement {
  id: string;
  value: number;
}

type Without<T, A extends keyof T> = Omit<T, A>;

type Wrong = Without<Measurement, "missingField">;
```

```text
r7.ts(9,35): error TS2344: Type '"missingField"' does not satisfy the constraint 'keyof Measurement'.
```

The only difference is putting the `extends keyof T` constraint on the type parameter;
the body still uses `Omit`.

#### Union Filters

Three utility types are built on the distribution behavior of conditional types:

```typescript
type Unit = "C" | "Pa" | "%";
type Interval = [min: number, max: number];

type Intervals = Record<Unit, Interval>;
type NumericUnit = Exclude<Unit, "%">;
type PercentUnit = Extract<Unit, "%" | "ppm">;
type DefinedValue = NonNullable<number | null | undefined>;

const intervals: Intervals = {
  C: [-40, 85],
  Pa: [0, 200000],
  "%": [0, 100],
};
const numeric: NumericUnit = "Pa";
const percent: PercentUnit = "%";
const value: DefinedValue = 21.4;

console.log(intervals.C[1], numeric, percent, value);
```

The output is `85 Pa % 21.4`.

Their definitions are one line with conditional types:

- `Exclude<T, U>` — `T extends U ? never : T`
- `Extract<T, U>` — `T extends U ? T : never`
- `NonNullable<T>` — `T & {}`

In the `Extract` example, the value `"ppm"` is not in the `Unit` union and does not
enter the result; it can be thought of as an intersection. `NonNullable` intersects
with the empty object type to filter out the `null` and `undefined` members.

#### Deriving Types from Signatures

The last group is built on `infer` and derives a type from an existing function's
signature:

```typescript
interface Measurement {
  id: string;
  value: number;
}

function createMeasurement(id: string, value: number) {
  return { id, value, createdAt: 0 };
}

async function fetchMeasurement(id: string): Promise<Measurement> {
  return { id, value: 21.4 };
}

type CreatedRecord = ReturnType<typeof createMeasurement>;
type CreationArguments = Parameters<typeof createMeasurement>;
type FetchedRecord = Awaited<ReturnType<typeof fetchMeasurement>>;

const record: CreatedRecord = { id: "s-01", value: 21.4, createdAt: 0 };
const args: CreationArguments = ["s-02", 22.1];
const fetched: FetchedRecord = { id: "s-03", value: 23.0 };

console.log(record.createdAt, createMeasurement(...args).id, fetched.value);
```

The output is `0 s-02 23`. If an assignment with a missing argument is tried with the
same `createMeasurement` definition in a separate file (`r5.ts`):

```typescript
// r5.ts — tuple with a missing argument
function createMeasurement(id: string, value: number) {
  return { id, value, createdAt: 0 };
}

type CreationArguments = Parameters<typeof createMeasurement>;

const wrong: CreationArguments = ["s-04"];
```

```text
r5.ts(8,7): error TS2322: Type '[string]' is not assignable to type '[id: string, value: number]'.
  Source has 1 element(s) but target requires 2.
```

The diagnostic shows that `Parameters`'s result is a named tuple. `ReturnType` gives
the inferred return type of the `createMeasurement` function; `Awaited` takes the type
of the value a promise produces when resolved, introduced in the Asynchronous
JavaScript course, and also unwraps nested promises.

This group's typical use is using a function's return type elsewhere without writing
it by hand. One warning applies: this link is one-directional, and when the function's
body changes, the derived type changes silently along with it. The rule established in
the Type Inference lesson applies here — the type of a boundary exposed outward should
be written, not left to inference.

#### Choosing a Utility Type

| Need | Utility type |
|---|---|
| Make all fields optional | `Partial<T>` |
| Make all fields required | `Required<T>` |
| Make all fields read-only | `Readonly<T>` |
| Keep specific fields | `Pick<T, A>` |
| Remove specific fields | `Omit<T, A>` |
| Build a table from a key set | `Record<A, D>` |
| Remove a member from a union | `Exclude<T, U>` |
| Select a member from a union | `Extract<T, U>` |
| Filter out empty values | `NonNullable<T>` |
| Derive a type from a signature | `ReturnType`, `Parameters`, `Awaited` |

Every row in the table rests on a mechanism built in this topic. There is no need to
memorize the utility types; once what they do and how they are built is known, they are
remembered on their own, and a new one can be written when a transform not on the list
is needed.

#### Summary

- Utility types are the standard library's ready-made forms of the mapped and
  conditional type mechanisms built in earlier lessons.
- `Pick`, `Omit`, `Partial`, and `Readonly` compute a data model's derivatives from the
  original type; when the model grows, the derivatives grow with it.
- `Omit` does not constrain its second parameter with `keyof T`; a nonexistent field
  name is silently ignored, while `Pick` gives the `TS2344` diagnostic.
- `Exclude`, `Extract`, and `NonNullable` are union filters built on the distribution
  behavior of conditional types.
- `ReturnType`, `Parameters`, and `Awaited` derive types from existing signatures using
  `infer`; the derived type changes silently when the body changes.

#### Next Step

Measurement records do not always carry a flat structure: a record can have
sub-records underneath it, a configuration object can have nested sections underneath
it. Transforms applied to the depth of such structures — making every level read-only,
counting nested key paths — require a type to refer to itself. The next lesson covers
recursive types and the limits the compiler places on them.

### Recursive Types

Source: https://academia.sh/en/courses/typescript/recursive-types

Self-referential type definitions, deep transforms, producing key paths with type-level recursion, and the compiler's expansion depth limit.

Measurement records do not always carry a flat structure: sub-records sit underneath a
record, nested sections sit underneath a configuration object. The type of such
structures has to refer to itself.

The Type Aliases lesson set the limit of this capability: a reference inside an object
field is valid, a direct reference is not. This lesson covers how far that capability
can be pushed — deep transforms and recursion at the type level — and where the
compiler stops.

#### Self-Referential Data Types

The typical example of data whose structure is not known ahead of time and can nest is
JSON itself:

```typescript
type JsonValue =
  | string
  | number
  | boolean
  | null
  | JsonValue[]
  | { [key: string]: JsonValue };

function depth(value: JsonValue): number {
  if (Array.isArray(value)) {
    return 1 + Math.max(0, ...value.map(depth));
  }
  if (typeof value === "object" && value !== null) {
    return 1 + Math.max(0, ...Object.values(value).map(depth));
  }
  return 0;
}

const record: JsonValue = {
  id: "s-01",
  value: 21.4,
  tags: ["boiler", "shift-3"],
  location: { building: "A", floor: 2 },
};

console.log(depth(record));
console.log(depth(21.4));
```

Output:

```text
2
0
```

The type is a six-member union, and its last two members refer to itself. Because the
references sit inside a field type, the compiler resolves them only when needed; no
infinite expansion results.

The type also carries a guarantee. If the line
`const invalid: JsonValue = { time: new Date() };` is added at the end of the file,
after a blank line:

```text
s1.ts(29,7): error TS2322: Type '{ time: Date; }' is not assignable to type 'JsonValue'.
  Types of property 'time' are incompatible.
    Type 'Date' is not assignable to type 'JsonValue | undefined'.
      Type 'Date' is not assignable to type '{ [key: string]: JsonValue; }'.
        Index signature for type 'string' is missing in type 'Date'.
```

This is a test that separates serializable data from non-serializable data. A
`JSON.stringify` call silently turns a date object into a string, and the type changes
after parsing; the `JsonValue` type moves this silent conversion to build time.

The `depth` function is recursive too and tracks the type's structure: the base case
and the reduction step built in the Programming Fundamentals course's recursion lesson
correspond here to the type's members.

#### Deep Transforms

`Readonly` from the utility types lesson only applies at the first level:

```typescript
interface Configuration {
  name: string;
  thresholds: { min: number; max: number };
}

const shallow: Readonly<Configuration> = {
  name: "boiler-2",
  thresholds: { min: -40, max: 85 },
};

shallow.thresholds.max = 90;
console.log(shallow.thresholds.max);
```

This file **passes** the type check and prints `90`. The `shallow.thresholds` field is
read-only, but the fields of the object it points to are not.

A recursive mapped type closes this gap:

```typescript
type DeepReadonly<T> = {
  readonly [A in keyof T]: T[A] extends object ? DeepReadonly<T[A]> : T[A];
};

interface Configuration {
  name: string;
  thresholds: { min: number; max: number };
  channels: { sensor: string; unit: string }[];
}

const configuration: DeepReadonly<Configuration> = {
  name: "boiler-2",
  thresholds: { min: -40, max: 85 },
  channels: [{ sensor: "temperature", unit: "C" }],
};

console.log(configuration.thresholds.max, configuration.channels[0].sensor);
```

The output is `85 temperature`. If a line that tries to change a nested field is tried
with the same definitions in a separate file (`s3.ts`):

```typescript
// s3.ts — assigning to a deeply read-only field
type DeepReadonly<T> = {
  readonly [A in keyof T]: T[A] extends object ? DeepReadonly<T[A]> : T[A];
};

interface Configuration {
  name: string;
  thresholds: { min: number; max: number };
  channels: { sensor: string; unit: string }[];
}

const configuration: DeepReadonly<Configuration> = {
  name: "boiler-2",
  thresholds: { min: -40, max: 85 },
  channels: [{ sensor: "temperature", unit: "C" }],
};

configuration.thresholds.max = 90;
```

```text
s3.ts(18,26): error TS2540: Cannot assign to 'max' because it is a read-only property.
```

The mapping tests the `T[A] extends object` condition on every field; it reapplies
itself on fields that are objects and stops on primitive fields. Because arrays are
objects too, the `channels` field turns into a `readonly` array, and its elements'
fields become read-only as well.

The same warning from earlier lessons still applies: this guarantee belongs to build
time. The configuration object can still be changed at run time; freezing it deeply is
a separate operation.

#### Recursion at the Type Level

Recursion is not limited to defining a data shape; it is also used to compute at the
type level. A type that produces every key path of a nested object is the typical
example of this:

```typescript
type Path<T> = T extends object
  ? {
      [A in keyof T & string]: T[A] extends object ? A | `${A}.${Path<T[A]>}` : A;
    }[keyof T & string]
  : never;

interface Configuration {
  name: string;
  thresholds: { min: number; max: number };
  network: { address: string; identity: { user: string } };
}

const valid: Path<Configuration>[] = [
  "name",
  "thresholds",
  "thresholds.min",
  "network.identity.user",
];
console.log(valid.length);
```

The output is `4`. If a path outside the set is tried with the same definitions in a
separate file (`s6.ts`):

```typescript
// s6.ts — path outside the set
type Path<T> = T extends object
  ? {
      [A in keyof T & string]: T[A] extends object ? A | `${A}.${Path<T[A]>}` : A;
    }[keyof T & string]
  : never;

interface Configuration {
  name: string;
  thresholds: { min: number; max: number };
  network: { address: string; identity: { user: string } };
}

const wrongPath: Path<Configuration> = "network.zone";
```

```text
s6.ts(14,7): error TS2322: Type '"network.zone"' is not assignable to type '"name" | "network" | "network.address" | "network.identity" | "network.identity.user" | "thresholds" | "thresholds.max" | "thresholds.min"'.
```

The diagnostic writes out the computation's full result: eight valid paths. The
structure is a composition of three lessons. The mapped type produces a value for
every field; the `[keyof T & string]` indexed access takes the union of those values;
the template literal type joins sub-paths with the prepended name; the conditional
type stops the recursion on fields that are not objects.

This type's use is an interface that takes a configuration key as a string. The path
string is no longer a free string but a closed set; a misspelled key is caught at
build time.

#### The Depth Limit

The compiler applies an upper limit to prevent infinite expansion. The limit can be
observed by building a tuple that counts its own length:

```typescript
type Repeat<N extends number, B extends unknown[] = []> = B["length"] extends N
  ? B
  : Repeat<N, [...B, unknown]>;

type Result = Repeat<1001>;
declare const s: Result;
console.log(s.length);
```

```text
s10.ts(5,15): error TS2589: Type instantiation is excessively deep and possibly infinite.
```

When `Repeat<999>` is written in the same file, no diagnostic comes. The exact value
of the limit depends on the compiler version, and code relying on it is fragile; what
matters is that **a limit exists**.

Recursion that stays under the limit has a cost too: every expansion increases the
compiler's running time. Types like path generation can noticeably slow down type
checking on large configuration objects. The time-and-space trade-off from the
Algorithms course shows up here as a trade-off between build time and type precision.

For cases that need unbounded depth, a type-level solution is not suitable; the right
tool is run-time validation and a branded type.

#### Writing Recursion

Three rules keep recursive types healthy.

**The base case is written explicitly.** Every example above has a condition where the
recursion stops: the primitive members for `JsonValue`, the `extends object` test for
`DeepReadonly`, the same test for `Path`. A type with no base case expands to the
compiler's limit and gives `TS2589`.

**Depth must not come from data.** Expansion depth should depend on the type's
structure, not on a number given by the caller. The `Repeat<N>` example deliberately
breaks this rule to show the limit.

**The result is tested.** A type computed at the type level can be tested by declaring
an array holding the expected values — the `valid` declaration above does exactly
this. An incorrectly computed type produces surprising diagnostics everywhere it is
used.

#### Summary

- A self-reference inside an object field is valid; the compiler resolves these
  references only when needed.
- Utility types like `Readonly` only apply at the first level; a recursive mapped type
  is written for a deep transform.
- A mapped type, indexed access, a template literal type, and a conditional type
  together can produce nested key paths as a closed union.
- The compiler gives the `TS2589` diagnostic past a certain expansion depth; the
  limit's value depends on the version, and code relying on it is fragile.
- In a recursive type, the base case is written explicitly, depth comes from
  structure rather than data, and the result is tested with a separate declaration.

#### Next Step

Every example up to this point ran as a single file with compiler options given by
hand. In a real project, options are collected in a configuration file and the
strictness level is chosen deliberately. The next topic covers project configuration:
which flag catches which error class, the role of declaration files, working together
with JavaScript, and separating type checking from publish output.

## Project Configuration

Compiler configuration and strictness flags, declaration files, working with JavaScript, module resolution, and the build and run flow.

### Compiler Configuration

Source: https://academia.sh/en/courses/typescript/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 |
| `strictBuiltinIteratorReturn` | The return type of built-in iterators being treated as `any` |
| `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 earlier in the
course. The example below is compiled with the following configuration, which carries only the
relevant options:

```json
// tsconfig.json
{
  "compilerOptions": {
    "target": "es2022",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noUnusedLocals": true,
    "noEmit": true
  },
  "include": ["src"]
}
```

The example lives in the `src` directory covered by `include`; its file name is `src/measurement.ts`:

```typescript
// src/measurement.ts
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 this configuration:

```text
src/measurement.ts(11,13): error TS18048: 'first' is possibly 'undefined'.
src/measurement.ts(13,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/measurement.ts(13,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.

### Declaration Files

Source: https://academia.sh/en/courses/typescript/declaration-files

Carrying type definitions separately from implementation; generating declarations from source, ambient module declarations, global scope declarations, and extending outside types with merging.

The previous lesson made it possible to check the project's own files. Code coming
from outside — a library with no type information, the global objects the runtime
provides — is still out of scope.

A **declaration file** carries only type information and produces no code. Its
extension is `.d.ts`. It is used in two directions: exposing your own code's types,
and bringing in outside code's types.

#### Generating a Declaration From Source

A library package publishes its implementation as JavaScript. For TypeScript-using
consumers to have access to type information, declaration files have to be published
too. The compiler generates these from source:

```json
// tsconfig.json
{
  "compilerOptions": {
    "target": "es2022",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "strict": true,
    "declaration": true,
    "rootDir": "src",
    "outDir": "dist"
  },
  "include": ["src"]
}
```

Let there be a `package.json` at the project root with this content:

```json
// package.json
{
  "name": "measurement-library",
  "type": "module",
  "version": "1.0.0"
}
```

And let `src/measurement.ts` be this:

```typescript
// src/measurement.ts
export type Unit = "C" | "Pa" | "%";

export interface Measurement {
  readonly id: string;
  value: number;
  unit: Unit;
}

export function average(records: readonly Measurement[]): number {
  if (records.length === 0) {
    return 0;
  }
  return records.reduce((t, r) => t + r.value, 0) / records.length;
}

export const defaultUnit = "C";
```

Running `tsc` in the project directory produces two files. The first-line `// …` comment in
these blocks only shows the file name and is not part of the real file. `dist/measurement.js`:

```javascript
// dist/measurement.js
export function average(records) {
    if (records.length === 0) {
        return 0;
    }
    return records.reduce((t, r) => t + r.value, 0) / records.length;
}
export const defaultUnit = "C";
```

And `dist/measurement.d.ts`:

```typescript
// dist/measurement.d.ts
export type Unit = "C" | "Pa" | "%";
export interface Measurement {
    readonly id: string;
    value: number;
    unit: Unit;
}
export declare function average(records: readonly Measurement[]): number;
export declare const defaultUnit = "C";
```

The two complete each other. The JavaScript file has no types, it has the
implementation; the declaration file has no implementation, it has the types. The word
`declare` says exactly this: "this name exists, this is its type, its definition is
not here."

This split is the package-level counterpart of the type erasure principle from the
first lesson. The type layer is separated from the published code; to a consumer not
reading it, the package is an ordinary JavaScript package.

The `rootDir` setting in this lesson has to be written explicitly together with
`outDir` in this compiler version; when it is not, a `TS5011` diagnostic appears. The
setting's role is determining the output directory layout: `src/measurement.ts`
becomes `dist/measurement.js`, not `dist/src/measurement.js`.

#### Ambient Module Declaration

When a library carrying no type information is used, its types can be written in the
project. This is called an **ambient declaration** — declaring the type of something
whose implementation is elsewhere.

Let a `types` directory be added to the configuration; this configuration replaces the previous one:

```json
// tsconfig.json
{
  "compilerOptions": {
    "target": "es2022",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "strict": true,
    "noEmit": true
  },
  "include": ["src", "types"]
}
```

The `types/measurement-driver.d.ts` file declares the names the library offers:

```typescript
// types/measurement-driver.d.ts
declare module "measurement-driver" {
  export interface DriverSettings {
    address: string;
    timeout?: number;
  }

  export function connect(settings: DriverSettings): Promise<void>;
  export function read(channel: string): Promise<number>;
}
```

After this declaration, the module can be used in a typed way inside
`src/app.ts`:

```typescript
// src/app.ts
import { connect, read } from "measurement-driver";

export async function firstReading(): Promise<number> {
  await connect({ address: "10.0.0.7:9000" });
  return read("temperature.C");
}

export async function invalidCall(): Promise<number> {
  await connect({ address: "10.0.0.7:9000", retryCoun: 3 });
  return read(42);
}
```

```text
src/app.ts(10,45): error TS2353: Object literal may only specify known properties, and 'retryCoun' does not exist in type 'DriverSettings'.
src/app.ts(11,15): error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.
```

A hand-written declaration is a real contract for the compiler, and all checks apply.

A warning is needed: this declaration's correctness is not verified. If the library
actually behaves differently, the compiler cannot know. An ambient declaration is a
**debt** like the `as` notation and a type predicate; it has to be updated together
when the library's version changes. This is why a hand-written declaration is not
preferred in cases where the library publishes its own declarations.

#### Global Scope Declaration

Some names do not come from a module; they are placed into global scope by the
runtime. A `declare global` block declares these, here in `types/environment.d.ts`:

```typescript
// types/environment.d.ts
declare global {
  interface MeasurementEnvironment {
    readonly nodeName: string;
    readonly samplingRate: number;
  }

  const measurementEnvironment: MeasurementEnvironment;
}

export {};
```

The trailing `export {};` line is required: it makes the file count as a module, and a
`declare global` block can only be written in module files.

After the declaration, the name can be used without an `import`:

```typescript
// src/environment.ts
export function environmentSummary(): string {
  return `${measurementEnvironment.nodeName} @ ${measurementEnvironment.samplingRate}Hz`;
}

export function invalidAccess(): string {
  return measurementEnvironment.unknownField;
}
```

```text
src/environment.ts(7,33): error TS2339: Property 'unknownField' does not exist on type 'MeasurementEnvironment'.
```

A global scope declaration is a powerful but carefully used tool: the declared name
becomes visible in every file of the project, and where it comes from cannot be
understood just by reading the code. If a name does not genuinely need to be global,
exporting it from a module is preferred.

#### Extending Outside Types

Declaration merging, introduced in the Interfaces lesson, finds its real use here. A
library's type can be extended without touching the library.

Without an extension, the `DriverSettings` interface has no `retryCount` field. When the
field name is misspelled as `retryCoun`, the `src/app.ts` file receives this diagnostic:

```typescript
// src/app.ts
import { connect, read } from "measurement-driver";

export async function firstReading(): Promise<number> {
  await connect({ address: "10.0.0.7:9000", retryCoun: 3 });
  return read("temperature.C");
}
```

```text
src/app.ts(5,45): error TS2353: Object literal may only specify known properties, and 'retryCoun' does not exist in type 'DriverSettings'.
```

Now let the `types/extension.d.ts` file open a second block with the same module name:

```typescript
// types/extension.d.ts
declare module "measurement-driver" {
  interface DriverSettings {
    retryCount?: number;
  }
}
```

Ambient declarations carrying the same module name merge; since `DriverSettings` is
also an interface, it is open to merging too. As a result, the field becomes usable
without the original declaration changing at all:

```typescript
// src/app.ts
import { connect, read } from "measurement-driver";

export async function firstReading(): Promise<number> {
  await connect({ address: "10.0.0.7:9000", retryCount: 3 });
  return read("temperature.C");
}

export function environmentSummary(): string {
  return `${measurementEnvironment.nodeName} @ ${measurementEnvironment.samplingRate}Hz`;
}
```

This file checks without error alongside the previous three declaration files.

That merging is really being applied is seen when the field name is misspelled. While the
extension is in the project, writing `retryCoun` instead of `retryCount` in the same file:

```typescript
// src/app.ts
import { connect, read } from "measurement-driver";

export async function firstReading(): Promise<number> {
  await connect({ address: "10.0.0.7:9000", retryCoun: 3 });
  return read("temperature.C");
}
```

```text
src/app.ts(5,45): error TS2561: Object literal may only specify known properties, but 'retryCoun' does not exist in type 'DriverSettings'. Did you mean to write 'retryCount'?
```

The difference between the two diagnostics — without the augmentation there is no
suggestion, with it there is one — directly shows whether merging was applied.

The limit of extension is that it only works on **interfaces**. If a library defines
its type with `type`, it cannot be extended; the `TS2300` diagnostic from the Type
Aliases lesson appears. The practical consequence for library authors: if the shapes
of exposed objects are declared with an interface, consumers can extend them.

#### Summary

- A declaration file carries only types; the word `declare` says a name exists but its
  definition is elsewhere.
- The `declaration` option generates declaration files from source; in this compiler
  version, `rootDir` has to be written together with `outDir`.
- An ambient module declaration types a library with no type information; its
  correctness is not verified and it is a debt that has to be updated with the
  library.
- A `declare global` block declares names in global scope; the file needs an
  `export {};` line to count as a module.
- A `declare module` block carrying an `import` line extends an existing module's
  types; extension only works on interfaces.

#### Next Step

An ambient declaration was the way to type an untyped library from outside. What
about the project's **own** JavaScript files? An existing codebase cannot be
converted to TypeScript overnight; this migration has to be done gradually. The next
lesson covers checking JavaScript files, typing them with comments, and the migration
strategy.

### Working With JavaScript

Source: https://academia.sh/en/courses/typescript/working-with-javascript

Bringing JavaScript files into the project and checking them, typing with comments, file-level check directives, and the gradual migration strategy.

The previous lesson built the way to type an untyped library from outside. The
project's **own** JavaScript files are a different problem: an existing codebase
cannot be converted overnight.

TypeScript is designed to support this migration. Every JavaScript file is at the
same time a valid TypeScript input; the level of checking can be set per project and
per file. This lesson covers the tools and order of that migration.

#### Bringing JavaScript Files Into the Project

There are two separate options, and they are frequently confused.

**`allowJs`** lets JavaScript files into the project: the compiler reads them, knows
the names they export, and allows them to be imported from TypeScript files.
**`checkJs`** makes those files go through type checking.

```json
// tsconfig.json
{
  "compilerOptions": {
    "target": "es2022",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "strict": true,
    "allowJs": true,
    "checkJs": false,
    "noEmit": true
  },
  "include": ["src"]
}
```

Let there be a `package.json` at the project root with this content:

```json
// package.json
{
  "name": "measurement-migration",
  "type": "module",
  "version": "1.0.0"
}
```

`src/legacy.js` is pre-migration code:

```javascript
// src/legacy.js
export function average(records) {
  let total = 0;
  for (const r of records) {
    total += r.value;
  }
  return total / records.length;
}
```

`src/new.ts` uses it:

```typescript
// src/new.ts
import { average } from "./legacy.js";

const records = [{ value: 21.4 }, { value: 22.1 }];
const result: number = average(records);
console.log(result.toFixed(1));
```

With this configuration, compilation passes without error. But there is no guarantee:
the `average` function's parameter and return value count as `any`, so the `result`
declaration checks nothing.

With `checkJs` turned on, the configuration becomes:

```json
// tsconfig.json
{
  "compilerOptions": {
    "target": "es2022",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "strict": true,
    "allowJs": true,
    "checkJs": true,
    "noEmit": true
  },
  "include": ["src"]
}
```

When the same `src/new.ts` is checked again with this configuration, the diagnostic is reported
for the JavaScript file:

```typescript
// src/new.ts
import { average } from "./legacy.js";

const records = [{ value: 21.4 }, { value: 22.1 }];
const result: number = average(records);
console.log(result.toFixed(1));
```

```text
src/legacy.js(2,25): error TS7006: Parameter 'records' implicitly has an 'any' type.
```

Strict configuration now applies to the JavaScript file too. In a large codebase this
step produces hundreds of diagnostics; migration is therefore usually done file by
file.

#### Typing With Comments

JavaScript files can be typed without changing their syntax. The compiler reads
comment blocks in a specific form as type declarations:

```javascript
// src/legacy.js
/**
 * @typedef {object} Measurement
 * @property {string} id
 * @property {number} value
 */

/**
 * @param {readonly Measurement[]} records
 * @returns {number}
 */
export function average(records) {
  let total = 0;
  for (const r of records) {
    total += r.value;
  }
  return total / records.length;
}
```

This file checks without error under `"checkJs": true`, and the function it exports is
now typed. The TypeScript file using it:

```typescript
// src/new.ts
import { average } from "./legacy.js";

const records = [
  { id: "s-01", value: 21.4 },
  { id: "s-02", value: 22.1 },
];
const result: number = average(records);
console.log(result.toFixed(1));

average([{ id: "s-03", value: "23.0" }]);
```

```text
src/new.ts(11,24): error TS2322: Type 'string' is not assignable to type 'number'.
```

The diagnostic shows that the contract coming from the comment in the JavaScript file
is genuinely enforced. The `@typedef`, `@param`, and `@returns` directives are the
comment-form counterpart of the type notations built in the course; union, array, and
generic notations are supported too.

The cost of this method is readability: the same information is written longer and
sits apart from the body. Its gain is that the file is still an ordinary JavaScript
file — it runs without needing a build step. It is a suitable middle ground in
projects with no build step, or in migration's early stage.

#### File-Level Check Directives

Checking can also be turned on and off with a comment placed in the first lines of a file.
While `"checkJs": false`, writing `// @ts-check` in a file brings only that file under
checking. For this section the configuration is set back to `checkJs` off:

```json
// tsconfig.json
{
  "compilerOptions": {
    "target": "es2022",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "strict": true,
    "allowJs": true,
    "checkJs": false,
    "noEmit": true
  },
  "include": ["src"]
}
```

And let the `src/check.js` file be this:

```javascript
// src/check.js
// @ts-check

/**
 * @typedef {object} Measurement
 * @property {string} id
 * @property {number} value
 */

/**
 * @param {readonly Measurement[]} records
 * @returns {number}
 */
export function average(records) {
  let total = 0;
  for (const r of records) {
    total += r.id;
  }
  return total / records.length;
}
```

When a TypeScript file that imports this file is compiled, the diagnostic is reported
for the JavaScript file itself:

```typescript
// src/usage.ts
import { average } from "./check.js";

console.log(average([{ id: "s-01", value: 21.4 }]).toFixed(1));
```

```text
src/check.js(17,5): error TS2322: Type 'string' is not assignable to type 'number'.
```

The reverse directive is `// @ts-nocheck`, and it excludes a file from checking while
`"checkJs": true`.

The two directives are used at different points. `// @ts-check` is for bringing a
small number of files under checking at the start of a migration — the project
setting is off, files are turned on one by one. `// @ts-nocheck` is for temporarily
leaving out the few files remaining at the end of a migration — the project setting
is on, files are exempted one by one. The second arrangement is preferred: the
default is safe, exemptions are visible.

#### Single-Line Exemption

Sometimes not a whole file but a single line is the problem. There are two directives
and the difference between them matters.

`// @ts-ignore` suppresses all diagnostics on the next line and stays silent even if
there is nothing to suppress. `// @ts-expect-error`, instead, says it **expects** a
diagnostic on the next line; if the expected diagnostic does not appear, it produces a
diagnostic of its own:

```typescript
// src/new.ts
import { average } from "./legacy.js";

const records = [
  { id: "s-01", value: 21.4 },
  { id: "s-02", value: 22.1 },
];
const result: number = average(records);
console.log(result.toFixed(1));

// @ts-expect-error string value is not accepted
average([{ id: "s-03", value: "23.0" }]);

// @ts-expect-error this line is actually valid
average([{ id: "s-04", value: 24.0 }]);
```

```text
src/new.ts(14,1): error TS2578: Unused '@ts-expect-error' directive.
```

The directive on the eleventh line is correct and produces no diagnostic; the directive
on the fourteenth line is unnecessary and gets reported.

This behavior makes `// @ts-expect-error` superior to `// @ts-ignore`. When a
suppressed error is later fixed — the library gets updated, the type definition
improves — `@ts-ignore` stays silent and no one notices. `@ts-expect-error` instead
says "I am no longer needed." Exemptions clean themselves up this way.

A reason is written next to both directives; as in the example above, the rest of the
directive's text is treated as a comment.

#### Gradual Migration Order

The tools gathered can be put into a sequence. Each step preserves the guarantee the
previous step provided:

1. **`allowJs` is turned on, `checkJs` stays off.** JavaScript files enter the
   project; no new diagnostic appears. Newly written code is TypeScript.
2. **Contracts for frequently used modules are written.** Either with a `.d.ts` file
   or with comments. The most-imported modules are handled first; each one spreads a
   guarantee to every file that uses it.
3. **Files are brought under checking one by one with `// @ts-check`.** Diagnostics
   that appear are fixed. Ones that cannot be fixed are justified with
   `// @ts-expect-error`.
4. **`checkJs` is turned on, remaining files are exempted with `// @ts-nocheck`.**
   The default is now on the safe side.
5. **Files are moved to the `.ts` extension** and comment types are converted to
   syntax.
6. **Strictness flags outside the umbrella are turned on one by one.** The
   `noUncheckedIndexedAccess` and `exactOptionalPropertyTypes` options from the
   Compiler Configuration lesson come at this stage.

The principle behind the order is **keeping red at zero** at every step: the build
should always pass clean, new diagnostics should not be allowed to accumulate.
Otherwise the diagnostic list turns into noise and real errors get lost in it.

At some point `any` becomes unavoidable. The rule from the escape hatches lesson
applies here: `any` is a transition marker. Where it remains is documented and
narrowed in later rounds of the migration.

#### Summary

- `allowJs` brings JavaScript files into the project, `checkJs` puts them through type
  checking; the two are separate decisions.
- Comment notation (`@typedef`, `@param`, `@returns`) types JavaScript files without
  changing their syntax; the contract is enforced on the TypeScript side too.
- `// @ts-check` and `// @ts-nocheck` turn checking on and off at the file level; the
  arrangement where exemption is visible is preferred.
- `// @ts-expect-error` produces `TS2578` when the expected diagnostic does not
  appear, cleaning up unneeded exemptions; `// @ts-ignore` does not do this.
- Gradual migration proceeds by keeping the build clean at every step: bringing files
  in, writing contracts, turning on checking, moving the extension, raising
  strictness.

#### Next Step

The import line in this lesson wrote `./legacy.js`, and the extension was required.
The same notation does not change even when the source is a TypeScript file — this is
one of module resolution's rules and is surprising at first. The next lesson covers
how the compiler turns a module name into a file, path mapping, and the effect of
resolution settings on output.

### Module Resolution

Source: https://academia.sh/en/courses/typescript/module-resolution

The compiler's rules for turning a module name into a file, the extension requirement, resolution strategies, the limit of path mapping on output, and the fix through package subpath imports.

The import line in the previous lesson wrote `./legacy.js`, and the extension was
required. There, the imported file really was a JavaScript file; the same notation
does not change even when the source is a TypeScript file.

This is one of the rules of **module resolution**: the compiler's process of turning a
module name into a file. The rules depend on the `moduleResolution` setting, the
file's module kind, and the package's manifest. This lesson covers the process and
its traps.

#### The Extension Requirement

Consider a project. Let the `package.json` file be this:

```json
// package.json
{
  "name": "measurement-modules",
  "type": "module",
  "version": "1.0.0"
}
```

And let the configuration be this:

```json
// tsconfig.json
{
  "compilerOptions": {
    "target": "es2022",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "strict": true,
    "rootDir": "src",
    "outDir": "dist"
  },
  "include": ["src"]
}
```

Let `src/data/measurement.ts` export the course's model:

```typescript
// src/data/measurement.ts
export interface Measurement {
  id: string;
  value: number;
}

export function average(records: readonly Measurement[]): number {
  return records.length === 0
    ? 0
    : records.reduce((t, r) => t + r.value, 0) / records.length;
}
```

Three different notations can be tried inside `src/main.ts`. Without an extension:

```typescript
// src/main.ts
import { average } from "./data/measurement";

console.log(average([{ id: "s-01", value: 21.4 }]));
```

```text
src/main.ts(2,25): error TS2835: Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './data/measurement.js'?
```

With the source file's real name, that is, the `.ts` extension:

```typescript
// src/main.ts
import { average } from "./data/measurement.ts";

console.log(average([{ id: "s-01", value: 21.4 }]));
```

```text
src/main.ts(2,25): error TS5097: An import path can only end with a '.ts' extension when 'allowImportingTsExtensions' is enabled.
```

And in the form the compiler suggested:

```typescript
// src/main.ts
import { average } from "./data/measurement.js";

console.log(average([{ id: "s-01", value: 21.4 }]));
```

This third form compiles. The first-line `// …` comment in these blocks only shows the file name
and is not part of the real file. The produced `dist/main.js`:

```javascript
// dist/main.js
import { average } from "./data/measurement.js";
console.log(average([{ id: "s-01", value: 21.4 }]));
```

The output of `node dist/main.js` is `21.4`.

The rule is surprising, and its reason lies in the type erasure principle: **the
compiler does not change import paths.** Whatever is written in the source stays in
the output. Since the JavaScript file that will run is `measurement.js`, that has to
be what is written in the source too. Writing the `.ts` extension means referring to a
file that does not exist in the output.

There are options that change this behavior: flags that allow writing a `.ts`
extension and rewrite the extension in the output when needed. Which ones exist
depends on the compiler version; knowing the principle makes it possible to
understand what any given flag does.

#### Resolution Strategies

The `moduleResolution` setting determines which rules a name is searched with. There
are three classes.

**Strategies matching the runtime rule** (`node16`, `nodenext`) mimic the server
runtime's module resolution algorithm: the extension is required, the package manifest
is read, whether a file is a module or a script is determined by the manifest's type
field. The diagnostics above come from this class.

**The strategy matching a bundler** (`bundler`) assumes a bundler is present at the
build step. No extension is required, because it is the bundler, not the compiler,
that will resolve the path:

```json
// tsconfig.json
{
  "compilerOptions": {
    "target": "es2022",
    "module": "preserve",
    "moduleResolution": "bundler",
    "strict": true,
    "noEmit": true
  },
  "include": ["src"]
}
```

With this configuration, the extensionless notation checks without error:

```typescript
// src/main.ts
import { average } from "./data/measurement";

console.log(average([{ id: "s-01", value: 21.4 }]));
```

**The old strategy** (`node10`) is the first-generation algorithm that does not read
details in the package manifest, and it is not chosen in new projects.

The selection criterion is this: **the rule of whoever will run the code is
chosen.** If output goes directly to a runtime, the runtime strategy; if it passes
through a bundler, the bundler strategy. The symptom of a wrong choice is type
checking passing while running fails.

#### Package Names and Finding Type Declarations

A non-relative name — a package name — is resolved a different way: the compiler
walks up the file tree, searches for package directories, and reads the manifest of
the package it finds.

In this section the previous configuration stays in effect, and the package is installed under
`node_modules/measurement-driver`. A package manifest can point to both a runtime file and a type
file for the same name:

```json
// node_modules/measurement-driver/package.json
{
  "name": "measurement-driver",
  "version": "1.0.0",
  "type": "module",
  "exports": {
    ".": {
      "types": "./dist/driver.d.ts",
      "default": "./dist/driver.js"
    }
  }
}
```

The `types` condition answers to the compiler, the `default` condition to the
runtime. Let the package publish this declaration
in `dist/driver.d.ts`:

```typescript
// node_modules/measurement-driver/dist/driver.d.ts
export declare function read(channel: string): Promise<number>;
```

The contract is then enforced on the consumer's side:

```typescript
// src/main.ts
import { read } from "measurement-driver";

export async function firstReading(): Promise<number> {
  return read("temperature.C");
}

export async function invalidReading(): Promise<number> {
  return read(42);
}
```

```text
src/main.ts(9,15): error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.
```

Which steps resolution goes through can be asked. The `--traceResolution` option
writes this; in the output below the project root's absolute path is shortened to the
form `.../app`:

```text
======== Resolving module 'measurement-driver' from '.../app/src/main.ts'. ========
Loading module 'measurement-driver' from 'node_modules' folder, target file types: TypeScript, JavaScript, Declaration, JSON.
Found 'package.json' at '.../app/node_modules/measurement-driver/package.json'.
File '.../app/node_modules/measurement-driver/dist/driver.d.ts' exists - use it as a name resolution result.
======== Module name 'measurement-driver' was successfully resolved to '.../app/node_modules/measurement-driver/dist/driver.d.ts' with Package ID 'measurement-driver/dist/driver.d.ts@1.0.0'. ========
```

The output is the most direct way to find where resolution gets stuck: the question
"why aren't this package's types showing up" is answered by the lines showing which
file was searched for and not found.

If a package does not publish its own declarations, two options remain. The first is
installing declarations published separately by the community; these packages are
gathered under a name prefix and the compiler finds them on its own. The second is
writing the ambient declaration from the Declaration Files lesson. Both carry the same
debt: the declaration has to be kept in sync by hand with the package's real behavior.

#### Path Mapping and Its Limit

In deep directory structures, relative paths become hard to read. The `paths` setting
establishes a mapping between a name and a file:

```json
// tsconfig.json
{
  "compilerOptions": {
    "target": "es2022",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "strict": true,
    "rootDir": "src",
    "outDir": "dist",
    "paths": {
      "@data/*": ["./src/data/*"]
    }
  },
  "include": ["src"]
}
```

```typescript
// src/main.ts
import { average } from "@data/measurement.js";

console.log(average([{ id: "s-01", value: 21.4 }]));
```

This file **passes type checking without error**. The produced `dist/main.js`:

```javascript
// dist/main.js
import { average } from "@data/measurement.js";
console.log(average([{ id: "s-01", value: 21.4 }]));
```

When `node dist/main.js` runs, the runtime stops with the code `ERR_MODULE_NOT_FOUND`:
it cannot find a package named `@data/measurement.js`.

The reason is the same as before: the compiler does not change import paths. The
`paths` setting only makes the **compiler** resolve the name; the runtime knows
nothing of the same mapping.

This is the last and most expensive example of the distinction established in the
course's first lesson. The type layer belongs to compile time; it does not change
runtime behavior. `paths` is a type-layer setting, not a module-system setting.

Also, the `baseUrl` setting often used alongside `paths` is deprecated in this
compiler version and produces `TS5101` when written. Path patterns are now
written directly relative to the directory the configuration file sits in.

#### The Correct Fix: Package Subpath Imports

The same readability can be obtained a way the runtime also knows about. An internal
path mapping is written into the package manifest:

```json
// package.json
{
  "name": "measurement-modules",
  "type": "module",
  "version": "1.0.0",
  "imports": {
    "#data/*": "./dist/data/*"
  }
}
```

`paths` is removed from the configuration:

```json
// tsconfig.json
{
  "compilerOptions": {
    "target": "es2022",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "strict": true,
    "rootDir": "src",
    "outDir": "dist"
  },
  "include": ["src"]
}
```

And the import takes this form:

```typescript
// src/main.ts
import { average } from "#data/measurement.js";

console.log(average([{ id: "s-01", value: 21.4 }]));
```

Compilation passes without error, the `dist/main.js` output keeps the import as is,
and running `node dist/main.js` gives `21.4`.

The difference is this: the `#data/*` pattern is a property of the **package
manifest**, not the compiler. Because the `nodenext` strategy reads the package
manifest, the compiler uses the same mapping too. A single source, two consumers.

General principle: **the resolution rule is defined where the runtime will
understand it.** A rule known only to the compiler opens a gap between output and
source.

The situation is different in projects using a bundler: there, it is the bundler that
resolves paths, and the `paths` setting is safe as long as it is kept **identical**
to the bundler's own mapping. Two pieces of information kept in two places drifting
apart is a known maintenance problem.

#### Summary

- The compiler does not change import paths; the name written in the source stays
  exactly the same in the output.
- Runtime strategies require an extension on relative paths, and the extension is the
  output's name, not the source's; writing `.ts` gives `TS5097`, writing no extension
  gives `TS2835`.
- The bundler strategy requires no extension, because the bundler resolves the path;
  the strategy is chosen according to whoever will run the code.
- The `paths` setting only affects the compiler's resolution; because the runtime
  does not know the same mapping, the output can fail to run.
- A package manifest's internal path mapping is read by both the compiler and the
  runtime, giving a single-source solution.

#### Next Step

Throughout the course, the compiler both did type checking and produced output.
Splitting these two jobs is a decision that determines build time and deployment
layout in large projects. The final lesson covers this split, publish behavior on
error, and all the layers the course has built, together.

### Build and Run Flow

Source: https://academia.sh/en/courses/typescript/build-and-run-flow

Separating type checking from publish output, publish behavior on error, the options enabling per-file transpilation, type-only imports, and type-stripping runtimes.

Throughout the course, the compiler both did type checking and produced output.
Separating these two jobs is a decision that determines a project's build time and
deployment layout.

The reason for the separation was established in the first lesson: types are erased
at compile time. Erasure is mechanical and fast; type checking, on the other hand,
requires resolving the entire program and is slow. Splitting the two jobs shortens the
development loop.

#### Publishing on Error

The behavior seen in the first lesson is re-examined here, this time in a project
context. `src/main.ts`:

```typescript
const value: number = "21.4";
console.log(value);
```

If the configuration is set to produce output with `outDir`, compilation gives this
diagnostic:

```text
src/main.ts(1,7): error TS2322: Type 'string' is not assignable to type 'number'.
```

and still writes the `dist/main.js` file:

```javascript
const value = "21.4";
console.log(value);
export {};
```

The `noEmitOnError` option changes this behavior; the same compilation gives the
diagnostic and the `dist` directory is never created.

Which is wanted depends on context. During development, running code despite a type
error is useful — the error is in one module, the behavior being tried is in another.
In the publish pipeline, the opposite holds: code that has not passed checking should
not be packaged. Using two separate configurations is common for this reason.

The trailing `export {};` line is a separate detail: the compiler adds it to mark the
file as a module. The `moduleDetection` setting from the Compiler Configuration lesson
determines this behavior.

#### Splitting Into Two Steps

The split is set up with two commands:

- **Type checking:** `tsc --noEmit`. Produces no output, only gives diagnostics.
- **Output production:** A transpiler that runs per file. Erases types, adapts syntax
  to the target, does not do type checking.

The reason the second step is fast is that it can run **per file**: transpiling one
file requires no looking at other files. But this is impossible for some TypeScript
notations — the transpilation needs type information to be correct.

The `isolatedModules` option catches these notations. Let the project root hold this `package.json`:

```json
// package.json
{
  "name": "measurement-flow",
  "type": "module",
  "version": "1.0.0"
}
```

And let the configuration be this:

```json
// tsconfig.json
{
  "compilerOptions": {
    "target": "es2022",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "strict": true,
    "isolatedModules": true,
    "noEmit": true
  },
  "include": ["src"]
}
```

The files are these. `src/measurement.ts`:

```typescript
// src/measurement.ts
export interface Measurement {
  id: string;
  value: number;
}

export function average(records: readonly Measurement[]): number {
  return records.length === 0
    ? 0
    : records.reduce((t, r) => t + r.value, 0) / records.length;
}
```

and `src/main.ts`:

```typescript
// src/main.ts
import { Measurement, average } from "./measurement.js";

export { Measurement };

const records: Measurement[] = [{ id: "s-01", value: 21.4 }];
console.log(average(records));
```

With `isolatedModules` on:

```text
src/main.ts(4,10): error TS1205: Re-exporting a type when 'isolatedModules' is enabled requires using 'export type'.
```

The problem is this: a transpiler running per file cannot know whether the name
`Measurement` is a type or a value. If it is a type, the export line has to be erased
entirely; if it is a value, it has to be kept. Deciding requires looking at the
`measurement.ts` file, and that breaks per-file transpilation.

The `verbatimModuleSyntax` option applies the same rule to imports too. The configuration becomes:

```json
// tsconfig.json
{
  "compilerOptions": {
    "target": "es2022",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "strict": true,
    "verbatimModuleSyntax": true,
    "noEmit": true
  },
  "include": ["src"]
}
```

The same `src/main.ts` is checked again with this configuration:

```typescript
// src/main.ts
import { Measurement, average } from "./measurement.js";

export { Measurement };

const records: Measurement[] = [{ id: "s-01", value: 21.4 }];
console.log(average(records));
```

```text
src/main.ts(2,10): error TS1484: 'Measurement' is a type and must be imported using a type-only import when 'verbatimModuleSyntax' is enabled.
src/main.ts(4,10): error TS1205: Re-exporting a type when 'verbatimModuleSyntax' is enabled requires using 'export type'.
```

The fix is using type-only notations:

```typescript
// src/main.ts
import type { Measurement } from "./measurement.js";
import { average } from "./measurement.js";

export type { Measurement };

const records: Measurement[] = [{ id: "s-01", value: 21.4 }];
console.log(average(records));
```

`tsc` checks this file without error. The first-line `// …` comment in these blocks only shows
the file name and is not part of the real file. When `noEmit` is removed from the configuration
and `outDir: "dist"` and `rootDir: "src"` are added, the produced `dist/main.js`:

```javascript
// dist/main.js
import { average } from "./measurement.js";
const records = [{ id: "s-01", value: 21.4 }];
console.log(average(records));
```

The output of `node dist/main.js` is `21.4`.

The `import type` notation says **in the declaration** that the line will be erased
entirely. The gain is not only speed: taking only a type from a module whose import
has a side effect does not cause that module to load in the output. Same principle as
tree shaking in the Modules, Tooling and the Ecosystem course — knowing an erasable
dependency is erasable.

#### Incremental Builds

The `incremental` option makes the compiler write information left over from its
previous run to a file. On the next build, only changed files and the ones that
depend on them are re-resolved.

With the option on, a file named `tsconfig.tsbuildinfo` is created at the project
root. This file is a build artifact; it is not put under version control and is
removed in cleanup steps. Its location can be changed with `tsBuildInfoFile`.

Watch mode (`tsc --watch`) carries the same idea into a continuously running
process: file changes are watched, and only the affected part is re-checked. During
development, this mode is preferred over a one-shot build.

#### Type-Stripping Runtimes

Some runtimes can run TypeScript files directly: they strip type annotations and
execute the remaining JavaScript. This is the first lesson's type erasure principle
carried over to runtime.

The distinction has to be seen. The following `src/main.ts` file contains a type error:

```typescript
// src/main.ts
const value: number = "21.4";
console.log(value);
```

```text
src/main.ts(2,7): error TS2322: Type 'string' is not assignable to type 'number'.
```

The compiler reports this diagnostic. In such a runtime, however, the file runs and prints
`21.4`. **No type checking happens at all.** Stripping is mechanical; it has nothing to do with
the guarantee a compiler gives.

The second limit shows up in TypeScript notations that produce code. The `src/unit.ts` file:

```typescript
// src/unit.ts
enum Unit {
  Celsius = "C",
}
console.log(Unit.Celsius);
```

This file does not run: the runtime reports, with the code `ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX`, that
`enum` declarations are not supported in strip-only mode.

The Basic Types lesson showed that an enum is the exception to the type erasure
principle; its consequence is concrete here. A runtime that only strips cannot run
syntax that cannot be erased. The same restriction covers parameter properties too.

For this reason, the `erasableSyntaxOnly` option is kept on in projects targeting a
type-stripping runtime: notations that cannot be erased are reported at compile time,
instead of turning into a runtime surprise.

Comparison of the three paths:

| Path | Type checking | Speed | Constraint |
|---|---|---|---|
| Compiling with the compiler | Yes | Low | — |
| Separate checking + per-file transpilation | Separate step | High | Requires `isolatedModules` |
| Type-stripping runtime | No | High | Non-erasable syntax does not run |

What the three have in common is that type checking must not **disappear**. In the
second and third paths, checking has to happen somewhere — in the publish pipeline,
in the editor, in a pre-processing hook. When it does not happen, what remains is
JavaScript with type annotations written on it.

#### Summary

- A type error does not by itself stop publishing; `noEmitOnError` changes this, and
  separate configurations for development and publishing are common.
- Type checking (`tsc --noEmit`) and output production can be split into separate
  steps; the second step's speed comes from being able to run per file.
- `isolatedModules` and `verbatimModuleSyntax` report notations that break per-file
  transpilation; the fix is `import type` and `export type` notation.
- The `incremental` option writes build information to a file; this file is a build
  artifact and is not put under version control.
- Type-stripping runtimes do no type checking at all and cannot run non-erasable
  syntax; `erasableSyntaxOnly` catches these notations at compile time.

#### Course Wrap-Up

The course began with a single question: what can be said about a program without
running it? TypeScript's answer was a layer of proof built on source code that leaves
nothing behind once translation is done.

This layer's limits were drawn from the start and re-tested in every topic. Types are
erased at compile time; they do not change runtime behavior; they do not prove the
shape of the outside world. These constraints are not a shortcoming, they are the
definition of what the layer is — and the non-working output of the `paths` setting
and the lack of checking in a type-stripping runtime are two different views of the
same constraint.

A single model was carried through the whole course. The measurement record started
as a JavaScript object carrying no guarantees; its fields were typed, its unit was
bound to a closed set, its valid and invalid states were split apart with a
discriminated union, its source was abstracted with an interface, its log was
generalized with a type parameter, its derivatives were computed with mapped and
conditional types, its boundary was guarded with a type predicate, and finally it was
published outward with a package manifest. What was gained at every step was the
same thing: moving a problem that would surface at runtime to compile time.

Three principles to take from the course:

**Proof is produced at the boundary.** Data from a system boundary is received as
`unknown`, checked at runtime, and the result is recorded with a type predicate. The
`as` notation is not proof, it is a debt.

**The model stays in one place.** Derived types are not written by hand, they are
computed from the real type; fixed lists come from a single source, both as a value
and as a type. The model's growth forces, through the compiler, a review of the code
that uses it.

**Strictness is not a preference, it is a scope decision.** Every flag corresponds to
an error class; leaving a flag off means leaving that class to runtime.

Next comes where this code will run. The **Node.js Runtime** course covers the
server-side runtime: the phases of the event loop, filesystem and network interfaces,
streams and backpressure, process management, and logging and monitoring under
production conditions. The type layer built in this course holds there too — the
interfaces the runtime offers are typed with declaration files, and the module
resolution rules are that runtime's rules.

