thecodex.expert · The Codex Family of Knowledge
TypeScript

Types and Interfaces

Annotate your JavaScript with types — the compiler catches mistakes before they reach production.

TypeScript 5.x Structural typing Last verified:
Canonical Definition

TypeScript types describe the shape of values — basic types (string, number, boolean), object shapes (interface and type alias), union types (A | B), and intersection types (A & B) — all erased at compile time, leaving plain JavaScript.

One sentence

TypeScript adds a type system on top of JavaScript — you annotate variables, parameters, and return values with types, and the TypeScript compiler catches type errors before your code ever runs.

Basic type annotations

TypeScriptbasic_types.ts
// Type annotations — added with a colon after the variable name
let name:    string  = "Priya";
let age:     number  = 28;
let active:  boolean = true;
let nothing: null    = null;
let unknown: undefined = undefined;

// TypeScript infers types when you initialise
let inferred = "Hello";   // type: string — no annotation needed
inferred = 42;            // Error: Type 'number' is not assignable to type 'string'

// Arrays
let names:   string[]      = ["Priya", "Rahul"];
let scores:  Array<number> = [95, 88, 72];   // generic syntax

// Tuple — fixed length, fixed types at each position
let pair: [string, number] = ["Priya", 95];
let rgb:  [number, number, number] = [255, 128, 0];

// Union — one of several types
let id: string | number = "user-42";
id = 42;   // also valid

// Literal types — only specific values
let direction: "north" | "south" | "east" | "west" = "north";
let statusCode: 200 | 404 | 500 = 200;

// any — opts out of type checking (avoid in new code)
let flexible: any = "hello";
flexible = 42;      // no error — any disables checking
flexible.xyz();     // no error — dangerous

// unknown — safer alternative to any
let value: unknown = fetchFromAPI();
value.toUpperCase();              // Error — must narrow first
if (typeof value === "string") {
    value.toUpperCase();          // OK — narrowed to string
}

Interfaces

TypeScriptinterfaces.ts
// Interface — describes the shape of an object
interface User {
    readonly id:   number;    // readonly — cannot reassign after creation
    name:          string;
    email:         string;
    age?:          number;    // optional — may or may not be present
    role:          "admin" | "user" | "guest";
}

// Any object matching this shape is a valid User (structural typing)
const priya: User = {
    id:    1,
    name:  "Priya",
    email: "priya@example.com",
    role:  "admin",
};

// priya.id = 99;   // Error: Cannot assign to 'id' because it is a read-only property

// Interface for a function
interface Formatter {
    (input: string, options?: { uppercase?: boolean }): string;
}

const format: Formatter = (input, opts) =>
    opts?.uppercase ? input.toUpperCase() : input;

// Extending interfaces
interface AdminUser extends User {
    permissions: string[];
    lastLogin:   Date;
}

// Merging — interfaces with the same name are merged (declaration merging)
interface Window {
    myCustomProp: string;   // adds to the existing Window interface
}

Type aliases vs interfaces

TypeScripttype_vs_interface.ts
// type alias — can name any type, including unions and primitives
type ID      = string | number;
type Point   = { x: number; y: number };
type Status  = "pending" | "active" | "inactive";

// Intersection — combine types with &
type AdminUser = User & { permissions: string[] };

// Interface and type aliases are often interchangeable for objects
// Key differences:
// 1. type can express unions and intersections directly
// 2. interface supports declaration merging
// 3. interface extends uses extends keyword; type uses &
// 4. Error messages often mention interface names, not type alias names

// Prefer interface for object shapes (especially public APIs)
// Prefer type for unions, intersections, and complex type expressions

// Discriminated union — a union where a common field acts as a tag
type Shape =
    | { kind: "circle";    radius: number }
    | { kind: "rectangle"; width: number; height: number }
    | { kind: "triangle";  base: number;  height: number };

function area(s: Shape): number {
    switch (s.kind) {
        case "circle":    return Math.PI * s.radius ** 2;
        case "rectangle": return s.width * s.height;
        case "triangle":  return 0.5 * s.base * s.height;
    }
    // TypeScript knows this switch is exhaustive — no default needed
}

Type narrowing

TypeScript narrows the type of a variable inside conditional blocks — if you check typeof x === "string", TypeScript knows inside that block that x is a string. Multiple narrowing mechanisms exist.

TypeScriptnarrowing.ts
function process(value: string | number | null) {
    // typeof narrowing
    if (typeof value === "string") {
        console.log(value.toUpperCase());   // value: string
    } else if (typeof value === "number") {
        console.log(value.toFixed(2));      // value: number
    } else {
        console.log("null");                // value: null
    }
}

// instanceof narrowing
function formatDate(d: Date | string): string {
    if (d instanceof Date) {
        return d.toISOString();    // d: Date
    }
    return d;                      // d: string
}

// in narrowing — check for a property
type Cat = { meow(): void };
type Dog = { bark(): void };

function makeSound(animal: Cat | Dog) {
    if ("meow" in animal) {
        animal.meow();   // animal: Cat
    } else {
        animal.bark();   // animal: Dog
    }
}

// User-defined type guard — return type is a "type predicate"
function isString(value: unknown): value is string {
    return typeof value === "string";
}

// Assertion function — throws if condition is false
function assert(condition: unknown, msg: string): asserts condition {
    if (!condition) throw new Error(msg);
}

Classes with TypeScript

TypeScriptclasses.ts
class BankAccount {
    readonly owner:    string;
    private  balance:  number;
    protected  bank:   string;

    constructor(owner: string, initial: number, bank: string) {
        this.owner   = owner;
        this.balance = initial;
        this.bank    = bank;
    }

    deposit(amount: number): void {
        if (amount <= 0) throw new Error("Amount must be positive");
        this.balance += amount;
    }

    getBalance(): number { return this.balance; }
}

// Shorthand constructor — declare and assign in one
class Point {
    constructor(
        public  readonly x: number,
        public  readonly y: number,
    ) {}   // fields declared and assigned automatically

    distanceTo(other: Point): number {
        return Math.sqrt((this.x - other.x) ** 2 + (this.y - other.y) ** 2);
    }
}

const p1 = new Point(0, 0);
const p2 = new Point(3, 4);
p1.distanceTo(p2);   // 5

// Abstract class
abstract class Animal {
    abstract speak(): string;
    describe(): string { return \`I am a \${this.constructor.name}\`; }
}

class Dog extends Animal {
    speak(): string { return "Woof!"; }
}
Commonly confused
TypeScript types are erased at compile time — there is no runtime type information. if (x instanceof MyInterface) is invalid — interfaces do not exist at runtime. Only classes, constructors, and JavaScript primitives work with instanceof. For runtime checks, use type guards (value is Type) or discriminated unions.
TypeScript uses structural typing, not nominal typing. Two types are compatible if they have the same shape — regardless of their names. class Cat { meow() {} } and class Dog { meow() {} } are interchangeable in TypeScript's type system even though they are different classes. Java and C# use nominal typing (names must match).
any disables all type checking silently — unknown is almost always better. A variable of type any can be used in any way without errors — even calling methods that don't exist. unknown requires a type check (narrowing) before use. Always prefer unknown for values whose type you genuinely don't know.

Structural typing and the Liskov Substitution Principle

TypeScript's structural type system is formally described in the TypeScript spec as "assignability." Type A is assignable to type B if A has all the required properties of B (with compatible types). This is the TypeScript formalisation of duck typing: if it has all the right properties, it is the right type. This means TypeScript interfaces are not sealed — a value can have more properties than required and still satisfy the interface. This differs from Go's structural typing only in that TypeScript types exist only at compile time.

Control flow analysis

TypeScript's type narrowing is powered by control flow analysis — the compiler traces all possible execution paths through branches, loops, and exception handlers. The type of a variable at any point is the union of types it could have on all paths that reach that point. This is why TypeScript can narrow inside if, after return, after throw, and in switch exhaustive patterns. The "never" type represents an unreachable code path — if a variable narrows to never, that branch is provably unreachable.

Sources

1
TypeScript Handbook — Everyday Types. typescriptlang.org/docs/handbook/2/everyday-types.html.
2
TypeScript Handbook — Narrowing. typescriptlang.org/docs/handbook/2/narrowing.html.
3
TypeScript Handbook — Object Types. typescriptlang.org/docs/handbook/2/objects.html.
4
TypeScript Spec — Type Relationships. github.com/microsoft/TypeScript/blob/main/doc/spec-ARCHIVED.md.
Source confidence: High Last verified: Primary source: TypeScript Handbook · typescriptlang.org/docs/

Sources

1
TypeScript Handbook — Everyday Types. typescriptlang.org/docs/handbook/2/everyday-types.html.
2
TypeScript Handbook — Narrowing. typescriptlang.org/docs/handbook/2/narrowing.html.
3
TypeScript Handbook — Object Types. typescriptlang.org/docs/handbook/2/objects.html.
4
TypeScript Spec (archived). github.com/microsoft/TypeScript/blob/main/doc/spec-ARCHIVED.md.