TypeScript is frequently misunderstood as a cosmetic syntactical wrapper over JavaScript. In reality, modern TypeScript implements one of the most advanced, expressive, and mathematically rigorous type systems in industrial computer science. Operating as a compile-time static analysis engine, TypeScript unifies Set-Theoretic Type Semantics, Structural Subtyping, F-Bounded Polymorphism, and Control Flow Graph (CFG) Narrowing.
Unlike nominal type systems (such as Java, C++, or C#) where type equivalence is declared explicitly through nominal inheritance trees, TypeScript operates under structural compatibility: types are evaluated based on their shape, properties, and set-membership invariants.
This guide delivers an authoritative, university-grade architectural treatise on the mathematical foundations of the TypeScript type system, formal variance lattices (covariance, contravariance, invariance), branded nominal typing, and high-order generic abstractions.
1. Set-Theoretic Foundations: Types as Sets of Values#
The foundational theoretical model governing TypeScript is Set Theory. A type T in TypeScript does not represent an isolated class or memory allocation template; it represents a set of permissible runtime values:
+---------------------------------------------------------------------------------------+
| TypeScript Set-Theoretic Lattice |
| |
| Universal Top Types (All Possible Values): |
| [ unknown ] |
| / \ |
| Object Shapes Primitive Values |
| { id: string } string, number, boolean |
| \ / |
| Literal Singletons |
| "production", 42, true |
| \ / |
| Empty Set / Bottom Type: |
| [ never ] |
+---------------------------------------------------------------------------------------+The Type Hierarchy & Extreme Types#
- The Universal Set (`unknown`):
- unknown represents the set of all possible values across the entire JavaScript universe:
unknown = π° = { v | v is any valid ECMAScript value }
- Every single type T in TypeScript is a subset of unknown:
β T, T β unknown (T <: unknown)
- Type Safety Invariant: Because unknown contains arbitrary shapes, the compiler permits zero operations on a variable of type unknown without explicit narrowing (type predicates, typeof, or instanceof).
- The Escape Hatch (`any`):
- any acts as both a universal top type AND a universal bottom type simultaneously.
- It turns off the type checker for that variable, breaking the mathematical lattice. any is an escape hatch for legacy JavaScript interop, strictly prohibited in mission-critical codebases (noImplicitAny).
- The Empty Set (`never`):
- never represents the Bottom Type (β₯), containing zero elements:
never = β
- No value can ever be assigned to never (except never itself).
- never is the subtype of every other type:
β T, never β T (never <: T)
- Functionally represents unreachability: functions that unconditionally throw exceptions, infinite loops, or impossible branches in exhaustive union matching.
Union Types as Set Union (βͺ)#
A union type A | B represents the mathematical union of values belonging to set A or set B:
A βͺ B = { v | v β A β¨ v β B }
- A value is assignable to
A | Bif it satisfies either shape. - When consuming
A | B, the compiler only permits access to the intersection of their properties (properties guaranteed to exist regardless of which variant is present at runtime).
Intersection Types as Set Intersection (β©)#
An intersection type A & B represents the mathematical intersection of sets:
A β© B = { v | v β A β§ v β B }
- A value satisfies
A & Bif and only if it possesses all properties mandated by A AND all properties mandated by B. - Mathematical Invariant: If two incompatible primitive types are intersected:
string β© number = β = never
2. Structural Subtyping vs. Nominal Typing#
In nominal type systems (e.g., Java), two classes with identical fields are completely incompatible unless they share an explicit extends or implements declaration:
// Java (Nominal): Incompatible types!
class Point2D { public double x, y; }
class Vector2D { public double x, y; }
Point2D p = new Vector2D(); // Compile error!In TypeScript, subtyping is Structural (informally known as compile-time duck typing):
interface Point2D { x: number; y: number; }
interface Vector2D { x: number; y: number; }
const p: Point2D = { x: 10, y: 20 };
const v: Vector2D = p; // Valid! Perfectly compatible shapes.The Subtyping Relation (<:)#
A type S is a Subtype of T (denoted S <: T) if and only if every value that satisfies S also satisfies T:
S <: T βΊ β s β S, \; s β T
For object types:
- An object type S is a subtype of T if S contains all properties declared in T, and for every shared property, the property type in S is a subtype of the property type in T (Width Subtyping and Depth Subtyping).
- S may contain additional properties not present in T:
{ x: number, y: number, z: number } <: { x: number, y: number }
+---------------------------------------------------------------------------------------+
| Object Subtyping Hierarchy |
| |
| Supertype: interface Shape { id: string } |
| ^ |
| | Subtype has MORE properties (Smaller set of matching objects!) |
| Subtype: interface Circle { id: string; radius: number } |
| ^ |
| | Subtype has EVEN MORE properties |
| Subtype: interface ColoredCircle { id: string; radius: number; color: string } |
+---------------------------------------------------------------------------------------+Object Literal Freshness & Excess Property Checks#
A common source of confusion in TypeScript is Excess Property Checking:
interface Options { timeout?: number; retries?: number; }
function configure(opts: Options) { /* ... */ }
// Case 1: Direct Object Literal (Triggers Excess Property Check!)
configure({ timeout: 5000, retrise: 3 }); // Compile Error: Unknown property 'retrise'
// Case 2: Assigned to intermediate variable (Disables Freshness Check)
const configObj = { timeout: 5000, retrise: 3 };
configure(configObj); // Valid! Structural subtyping allows excess properties.Why This Behavior Exists:
- Under pure structural subtyping,
{ timeout: 5000, retrise: 3 }is a mathematically valid subtype ofOptions(width subtyping). - However, when developers pass an in-place object literal, writing an unknown property almost certainly represents a human typo (
retriseinstead ofretries). - To catch typos, TypeScript introduces Object Literal Freshness: when an object literal is assigned directly to a typed target, the compiler enforces an extra-linguistic sanity check prohibiting unrecognized keys. Assigning to an intermediate variable clears freshness, restoring pure structural subtyping rules.
3. Variance in Generics: Covariance, Contravariance & Invariance#
Variance describes how subtyping between complex generic types relates to subtyping between their underlying component types.
Given two types A and B such that A is a subtype of B (A <: B), how does a generic type constructor F<T> relate F<A> to F<B>?
+---------------------------------------------------------------------------------------+
| Variance Taxonomies |
| |
| [ Covariance ] : Preserves subtyping direction. |
| If A <: B, then F<A> <: F<B>. |
| Found in: Read-only data collections, function return types, Promise<T>. |
| |
| [ Contravariance ] : Reverses subtyping direction. |
| If A <: B, then F<B> <: F<A>. |
| Found in: Function parameter types (under strictFunctionTypes: true). |
| |
| [ Invariance ] : No subtyping relation is preserved. |
| F<A> <: F<B> ONLY IF A = B. |
| Found in: Read-write mutable data structures (e.g. Map<T, T>). |
| |
| [ Bivariance ] : Both covariant and contravariant simultaneously. |
| F<A> <: F<B> AND F<B> <: F<A>. |
| Found in: Method parameter declarations (legacy historical TypeScript compatibility)|
+---------------------------------------------------------------------------------------+The Function Subtyping Rule: Contravariant Parameters & Covariant Returns#
Consider two function types:
f_1: A β B
f_2: C β D
Under what conditions can f_1 be safely used wherever f_2 is expected (f_1 <: f_2)?
The Golden Function Subtyping Invariant:
(A -> B) <: (C -> D) <===> (C <: A) AND (B <: D)The Mathematical Proof:
- Suppose a consumer expects a function f_2: C β D.
- The consumer will invoke the function passing an argument c β C.
- If we substitute f_1: A β B, the argument c will be fed into f_1. For this to be safe, every value of C must be acceptable to A:
C β A βΉ C <: A (Contravariance in Parameters!)
- Next, f_1 produces a return value b β B.
- The consumer expects a return value of type D. For this to be safe, every value of B must be acceptable as D:
B β D βΉ B <: D (Covariance in Returns!)
Practical TypeScript Example
Let Dog <: Animal (Every Dog is an Animal).
type ProcessAnimal = (a: Animal) => voidtype ProcessDog = (d: Dog) => void
Which function can be assigned to which?
- A function that can process ANY Animal (
(a: Animal) => void) can safely handle a Dog, because a Dog is an Animal!
Therefore:
ProcessAnimal <: ProcessDog
- Notice the subtyping arrow flipped! Animal was the supertype, but
(a: Animal) => voidis the subtype! Function parameters are strictly contravariant. - In TypeScript, enabling
"strictFunctionTypes": trueintsconfig.jsonenforces contravariant function parameter checking, preventing runtime undefined property access errors.
4. Advanced Generic Constraints & F-Bounded Polymorphism#
Generics allow types to be parameterized, enabling high-performance reusability while preserving precision type information.
Generic Constraints (`extends`)#
A type parameter T can be bounded using the extends keyword:
interface Entity {
id: string;
}
// T is constrained to any type that satisfies Entity
function persist<T extends Entity>(record: T): T {
console.log(`Persisting record: {record.id}`);
return record;
}Multiple constraints can be composed using intersection:
function synchronize<T extends Entity & Serializable>(item: T): void { /* ... */ }Key Constraints (`keyof` and Indexed Access)#
TypeScript allows bounding generic parameters to the property keys of another generic:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Alice", age: 30, active: true };
const nameVal = getProperty(user, "name"); // Type inferred as: string
const ageVal = getProperty(user, "age"); // Type inferred as: number
// getProperty(user, "invalid"); // Compile Error: Argument of type '"invalid"' is not assignable to "name" | "age" | "active"F-Bounded Polymorphism#
In object-oriented generic architecture, a common requirement is defining a method that returns the exact type of the derived subclass (fluent interface / builder pattern).
F-Bounded Polymorphism is a mathematical formulation where a type parameter is bounded by an expression containing the type parameter itself:
T β€ F(T)
// F-Bounded Generic Interface
interface Comparable<T extends Comparable<T>> {
compareTo(other: T): number;
}
// Fluent Builder using Self-Referential Generics
abstract class QueryBuilder<TSubclass extends QueryBuilder<TSubclass>> {
protected queryParts: string[] = [];
// Returns exact derived subclass type rather than base class
public where(condition: string): TSubclass {
this.queryParts.push(`WHERE {condition}`);
return this as unknown as TSubclass;
}
public orderBy(column: string): TSubclass {
this.queryParts.push(`ORDER BY {column}`);
return this as unknown as TSubclass;
}
public abstract build(): string;
}
class PostgresQueryBuilder extends QueryBuilder<PostgresQueryBuilder> {
public limit(count: number): this {
this.queryParts.push(`LIMIT {count}`);
return this;
}
public build(): string {
return this.queryParts.join(" ");
}
}
// Type safety preserved through fluent chaining:
const sql = new PostgresQueryBuilder()
.where("active = true") // Returns PostgresQueryBuilder!
.limit(10) // Method exists on PostgresQueryBuilder!
.build();5. Branded Nominal Typing & Compile-Time Invariants#
Because TypeScript uses structural typing, two types with identical primitive structures can be assigned interchangeably, introducing insidious business-logic bugs:
type UserId = string;
type OrderId = string;
function cancelOrder(userId: UserId, orderId: OrderId) { /* ... */ }
const u: UserId = "usr_12345";
const o: OrderId = "ord_99999";
// Bug: Accidental transposition of identical string types passes type checking!
cancelOrder(o, u); // Compiles with ZERO warnings under pure structural typing!The Branded (Nominal) Pattern#
To force nominal type safety onto structural primitives without incurring runtime object wrapping overhead, architects employ Branded Types (also known as Tagged or Flavor types):
// Define unique compile-time brand symbols
declare const BrandSymbol: unique symbol;
type Brand<T, TBrand extends string> = T & { readonly [BrandSymbol]: TBrand };
// Nominal primitive aliases
export type UserId = Brand<string, "UserId">;
export type OrderId = Brand<string, "OrderId">;
export type EUR = Brand<number, "EUR">;
export type USD = Brand<number, "USD">;
// Constructor assertions (Smart Constructors)
export function createUserId(raw: string): UserId {
if (!raw.startsWith("usr_")) throw new Error("Invalid UserId format");
return raw as UserId;
}
export function createOrderId(raw: string): OrderId {
if (!raw.startsWith("ord_")) throw new Error("Invalid OrderId format");
return raw as OrderId;
}
// Invariant Enforcement:
const validUser = createUserId("usr_123");
const validOrder = createOrderId("ord_456");
function processPayment(user: UserId, order: OrderId) { /* ... */ }
// processPayment(validOrder, validUser);
// COMPILE ERROR: Type '"OrderId"' is not assignable to type '"UserId"'.Zero-Cost Invariant: At runtime, validUser is an ordinary string (typeof validUser === 'string'). The brand property exists purely in the compiler's symbol table, yielding zero runtime memory allocation and zero performance overhead while guaranteeing absolute compile-time nominal safety.
6. Programmatic Type Validator & Set Lattice in TypeScript#
Below is a production-grade TypeScript implementation demonstrating runtime schema validation that mirrors compile-time type invariants, implementing tagged discriminated unions and exhaustive type narrowing:
// Production Discriminated Union & Exhaustive Narrowing Engine
export type NetworkResult<TData> =
| { readonly status: "SUCCESS"; readonly data: TData; readonly latencyMs: number }
| { readonly status: "RETRYABLE_ERROR"; readonly error: Error; readonly retryCount: number }
| { readonly status: "FATAL_ERROR"; readonly error: Error; readonly httpCode: number }
| { readonly status: "PENDING"; readonly startedAt: number };
// The Exhaustiveness Checker Function (Bottom-Type Guard)
export function assertNever(x: never, message: string = "Unexpected unreachable value"): never {
throw new Error(`{message}: {JSON.stringify(x)}`);
}
// Consumer Function Demonstrating Complete Exhaustive CFA
export function handleNetworkResponse<T>(response: NetworkResult<T>): string {
switch (response.status) {
case "SUCCESS":
return `Operation completed in {response.latencyMs}ms.`;
case "RETRYABLE_ERROR":
return `Retry attempt #{response.retryCount} failed: {response.error.message}`;
case "FATAL_ERROR":
return `Fatal HTTP {response.httpCode}: {response.error.message}`;
case "PENDING":
return `Request in flight since timestamp {response.startedAt}`;
default:
// If a new variant is added to NetworkResult (e.g., "CANCELLED")
// and not handled above, the TypeScript compiler will raise:
// "Argument of type '{ status: "CANCELLED" }' is not assignable to parameter of type 'never'."
return assertNever(response);
}
}7. Checkpoint Practice Problems & Applied Walkthroughs#
Consolidate your mastery of TypeScript type systems and variance through four comprehensive multi-step mathematical problems.
Problem 1: Formal Variance Proof of Higher-Order Function Subtyping#
Consider the subtyping hierarchy where Dog <: Animal and Cat <: Animal, with Dog and Cat being disjoint non-empty sets.
We define four function types:
- F_1:
(a: Animal) => Dog - F_2:
(d: Dog) => Animal - F_3:
(a: Animal) => Animal - F_4:
(d: Dog) => Dog
- Tasks Required:
1. For each pair (F_i, F_j), apply the formal function subtyping rule:
(P_1 β R_1) <: (P_2 β R_2) βΊ P_2 <: P_1 β§ R_1 <: R_2
to determine all valid subtyping relationships.
2. Draw the complete partial-order subtyping lattice (directed acyclic graph) among the four function types F_1, F_2, F_3, F_4.
3. Identify the maximal type (top of this 4-type lattice) and the minimal type (bottom of this lattice).
Step-by-Step Solution:
Step 1: Evaluate subtyping conditions for all pairs.
Let P_i denote the parameter type of F_i, and R_i denote the return type of F_i:
- F_1: P_1 = Animal, R_1 = Dog
- F_2: P_2 = Dog, R_2 = Animal
- F_3: P_3 = Animal, R_3 = Animal
- F_4: P_4 = Dog, R_4 = Dog
Recall: Dog <: Animal.
Let us test F_1 <: F_3:
- Condition 1 (Parameters): P_3 <: P_1 βΉ Animal <: Animal (True).
- Condition 2 (Returns): R_1 <: R_3 βΉ Dog <: Animal (True).
- Result: F_1 <: F_3.
Let us test F_1 <: F_4:
- Condition 1 (Parameters): P_4 <: P_1 βΉ Dog <: Animal (True).
- Condition 2 (Returns): R_1 <: R_4 βΉ Dog <: Dog (True).
- Result: F_1 <: F_4.
Let us test F_3 <: F_2:
- Condition 1 (Parameters): P_2 <: P_3 βΉ Dog <: Animal (True).
- Condition 2 (Returns): R_3 <: R_2 βΉ Animal <: Animal (True).
- Result: F_3 <: F_2.
Let us test F_4 <: F_2:
- Condition 1 (Parameters): P_2 <: P_4 βΉ Dog <: Dog (True).
- Condition 2 (Returns): R_4 <: R_2 βΉ Dog <: Animal (True).
- Result: F_4 <: F_2.
Now check if F_3 <: F_4:
- Condition 1: P_4 <: P_3 βΉ Dog <: Animal (True).
- Condition 2: R_3 <: R_4 βΉ Animal <: Dog (FALSE!).
- Result: F_3 β F_4.
Now check if F_4 <: F_3:
- Condition 1: P_3 <: P_4 βΉ Animal <: Dog (FALSE!).
- Result: F_4 β F_3.
Step 2: Construct the Subtyping Partial-Order Lattice:
F_2: (Dog => Animal) [Supertype / Top of Lattice]
/ \
/ \
F_3: (Animal => Animal) F_4: (Dog => Dog)
\ /
\ /
F_1: (Animal => Dog) [Subtype / Bottom of Lattice]Step 3: Identification of Extreme Types:
- Minimal Type (Subtype of all four): F_1 (`(Animal) => Dog`).
Intuitive Reason: A function that can take any Animal and guarantees to return a Dog is the most capable, restrictive, and safe function. It can be plugged into any context expecting F_1, F_2, F_3, or F_4.
- Maximal Type (Supertype of all four): F_2 (`(Dog) => Animal`).
Intuitive Reason: A function that only accepts a Dog and only promises to return an Animal is the least capable. It can accept assignments from any of the other three functions.
Problem 2: Branded Type Currency Exchange Arithmetic#
A high-frequency trading platform processes foreign exchange orders across USD and EUR currencies.
- Type Definitions:
```typescript
declare const Brand: unique symbol;
type USD = number & { readonly [Brand]: "USD" };
type EUR = number & { readonly [Brand]: "EUR" };
type Rate_EUR_USD = number & { readonly [Brand]: "EUR_USD_Rate" };
```
- Arithmetic Rules:
Adding two incompatible currencies (e.g., USD + EUR) must trigger a compile-time type error.
Multiplying EUR by Rate_EUR_USD must yield a USD type.
- Calculations & Implementation:
1. Define a generic type-safe arithmetic operator addUSD(a: USD, b: USD): USD and addEUR(a: EUR, b: EUR): EUR.
2. Implement a generic conversion function convertEurToUsd(amount: EUR, rate: Rate_EUR_USD): USD.
3. Show the exact TypeScript compiler error generated when a developer attempts to pass a USD value as the first argument to convertEurToUsd.
Step-by-Step Solution:
Step 1: Implementation of Type-Safe Arithmetic Operators:
export function addUSD(a: USD, b: USD): USD {
return (a + b) as USD;
}
export function addEUR(a: EUR, b: EUR): EUR {
return (a + b) as EUR;
}Step 2: Implementation of Conversion Function:
export function convertEurToUsd(amount: EUR, rate: Rate_EUR_USD): USD {
return (amount * rate) as USD;
}Step 3: Verification of Invariant Failure:
Consider the errant code:
const balanceUSD = 1000 as USD;
const currentRate = 1.08 as Rate_EUR_USD;
// Developer mistakenly attempts to convert USD using EUR rate:
const result = convertEurToUsd(balanceUSD, currentRate);Compiler Error Output:
Argument of type 'USD' is not assignable to parameter of type 'EUR'.
Type 'number & { readonly [Brand]: "USD"; }' is not assignable to type 'number & { readonly [Brand]: "EUR"; }'.
Type '"USD"' is not assignable to type '"EUR"'.Mathematical Takeaway: Despite both USD and EUR being primitive IEEE-754 64-bit floating point numbers at runtime, the TypeScript compiler detects the nominal brand mismatch at zero runtime cost, permanently preventing catastrophic multi-million-dollar currency transposition bugs.
Problem 3: F-Bounded Generic Matrix Transformation Engine#
A graphics engine library models 2D geometric transformations using F-Bounded polymorphism.
- Mathematical Specification:
Every transformation matrix class must implement a fluent multiply method that accepts and returns the exact concrete matrix type to allow chainable matrix multiplications without loss of subclass properties.
- Implementation Requirements:
1. Define the F-bounded abstract class Matrix<TMatrix extends Matrix<TMatrix>>.
2. Implement concrete subclass AffineMatrix2D containing an additional property translation: [number, number].
3. Implement concrete subclass ProjectionMatrix3D containing an additional property fovDegrees: number.
4. Demonstrate that invoking multiply() on an instance of AffineMatrix2D retains access to translation without requiring manual type assertions (as).
Step-by-Step Solution:
Step 1: Define F-Bounded Base Class:
export abstract class Matrix<TMatrix extends Matrix<TMatrix>> {
public abstract readonly dimensions: [number, number];
// F-Bounded method signature
public abstract multiply(other: TMatrix): TMatrix;
}Step 2: Implement Concrete Subclasses:
export class AffineMatrix2D extends Matrix<AffineMatrix2D> {
public readonly dimensions: [number, number] = [3, 3];
constructor(public translation: [number, number]) {
super();
}
public multiply(other: AffineMatrix2D): AffineMatrix2D {
// Computes composite affine translation
return new AffineMatrix2D([
this.translation[0] + other.translation[0],
this.translation[1] + other.translation[1]
]);
}
}
export class ProjectionMatrix3D extends Matrix<ProjectionMatrix3D> {
public readonly dimensions: [number, number] = [4, 4];
constructor(public fovDegrees: number) {
super();
}
public multiply(other: ProjectionMatrix3D): ProjectionMatrix3D {
return new ProjectionMatrix3D(Math.min(this.fovDegrees, other.fovDegrees));
}
}Step 3: Prove Type Retention across Fluent Operations:
const m1 = new AffineMatrix2D([10, 20]);
const m2 = new AffineMatrix2D([5, 15]);
// Fluent chaining:
const result = m1.multiply(m2);
// TypeScript statically knows 'result' is strictly AffineMatrix2D:
console.log(result.translation); // [15, 35] - Valid! No typecast required!
// Cross-multiplication of disparate matrices is rejected:
const proj = new ProjectionMatrix3D(90);
// m1.multiply(proj);
// COMPILE ERROR: Argument of type 'ProjectionMatrix3D' is not assignable to 'AffineMatrix2D'.Problem 4: Compile-Time Union Exhaustiveness & Cardinality Analysis#
A compiler backend represents Abstract Syntax Tree (AST) expression nodes as a discriminated union:
type ASTNode =
| { kind: "Literal"; value: number }
| { kind: "Identifier"; name: string }
| { kind: "BinaryOp"; operator: "+" | "-" | "*" | "/"; left: ASTNode; right: ASTNode }
| { kind: "UnaryOp"; operator: "!" | "-"; operand: ASTNode };- Tasks Required:
1. Write an evaluator function evaluateConstant(node: ASTNode): number | null that recursively computes constant expressions.
2. Implement an exhaustive default branch using the bottom-type never guard pattern.
3. If a language designer extends ASTNode by introducing a fifth variant:
| { kind: "TernaryOp"; condition: ASTNode; trueExpr: ASTNode; falseExpr: ASTNode }
describe the exact compiler error message generated by TypeScript and explain why the build fails.
Step-by-Step Solution:
Step 1 & 2: Implementation of Recursive Constant Evaluator with `never` Guard:
function assertUnreachable(x: never): never {
throw new Error(`Unhandled AST Node: {JSON.stringify(x)}`);
}
export function evaluateConstant(node: ASTNode): number | null {
switch (node.kind) {
case "Literal":
return node.value;
case "Identifier":
// Identifiers cannot be resolved at compile time without environment
return null;
case "BinaryOp": {
const leftVal = evaluateConstant(node.left);
const rightVal = evaluateConstant(node.right);
if (leftVal === null || rightVal === null) return null;
switch (node.operator) {
case "+": return leftVal + rightVal;
case "-": return leftVal - rightVal;
case "*": return leftVal * rightVal;
case "/": return rightVal !== 0 ? leftVal / rightVal : null;
}
}
case "UnaryOp": {
const operandVal = evaluateConstant(node.operand);
if (operandVal === null) return null;
if (node.operator === "-") return -operandVal;
return null; // Logical NOT on number
}
default:
// Exhaustiveness check: node is narrowed to 'never' here
return assertUnreachable(node);
}
}Step 3: Impact of Union Expansion:
When ASTNode is expanded with { kind: "TernaryOp"; ... }:
- The
switch (node.kind)statement checks cases"Literal","Identifier","BinaryOp", and"UnaryOp". - When execution drops into
default:, the Control Flow Analysis engine determines thatnodeis NOTnever. Instead,nodeis narrowed to:
node: { kind: "TernaryOp"; condition: ASTNode; trueExpr: ASTNode; falseExpr: ASTNode }
- The line
assertUnreachable(node)is evaluated:
- assertUnreachable takes a parameter of type never.
- The compiler attempts to assign { kind: "TernaryOp", ... } to never.
- Because never is the empty set (β
), no non-empty type can be assigned to never.
- Compile Error Output:
```
Argument of type '{ kind: "TernaryOp"; condition: ASTNode; trueExpr: ASTNode; falseExpr: ASTNode; }'
is not assignable to parameter of type 'never'.
```
Architectural Guarantee: The build fails immediately across the continuous integration pipeline, preventing silent runtime unhandled case errors and forcing the developer to implement the new AST variant before code can be deployed.