Knowledge/TypeScript/Introduction
20 Chapters
5.x Baseline
Living Document
Free License
Engineering Publication · Syed Omar Ibn Feroj← Back to portfolio
TS
Engineering Notes · Open Knowledge Repository

TypeScript — Engineering
Notes for Typed Code

A working reference for the TypeScript type system as it shows up in real codebases — inference first, then the generics, narrowing, conditional and mapped types that make a large codebase safe to change.

20
Chapters
5.x
Baseline
Living
Document
Free
License
Ch. 01
Getting Started
What TypeScript is, how the compiler fits in, and the smallest useful setup.
1.1 — A type layer over JavaScript

TypeScript is JavaScript plus a static type system that is fully erased at build time. Nothing about types exists at runtime — tsc checks your code and emits plain JavaScript. Every valid JS file is a valid TS file; you adopt types incrementally.

Shell
$ npm i -D typescript
$ npx tsc --init      # creates tsconfig.json
$ npx tsc             # type-check + emit
$ npx tsc --noEmit    # check only, emit nothing
1.2 — The mental model

Think of the type system as a separate program that runs at compile time and proves your value-level program never does anything contradictory. When the two disagree, the type program wins the argument before your code ever ships.

Run tsc --noEmit in CI even if your bundler (esbuild, swc, Vite) strips types itself — those tools transpile without type-checking, so without a separate tsc gate the types are decorative.
Ch. 02
Basic Types
The primitive and structural types you annotate with daily.
2.1 — Primitives and the special types
TypeScript
let s: string = "hi";
let n: number = 42;        // no int/float distinction
let b: boolean = true;
let big: bigint = 9007199254740993n;
let sym: symbol = Symbol();
let u: undefined = undefined;
let nl: null = null;

// any  — opt out of checking (avoid)
// unknown — safe any: must narrow before use
// never — the type with no values (unreachable)
// void — a function returns nothing meaningful
2.2 — Arrays, tuples, objects
TypeScript
let xs: number[] = [1, 2, 3];
let pair: [string, number] = ["a", 1];   // tuple
let user: { id: number; name?: string } = { id: 1 };
// name? — optional. readonly id — immutable property.
any disables type-checking for everything it touches and spreads silently. Prefer unknown when a value's type is genuinely not known — it forces you to narrow before use.
Ch. 03
Inference & Annotations
Let the compiler infer; annotate only at boundaries.
3.1 — Inference does most of the work

TypeScript infers the type of any initialised binding. Over-annotating is noise. The rule that scales: annotate function parameters and public return types and module boundaries; let everything internal be inferred.

TypeScript
const count = 0;            // inferred: number
const names = ["a", "b"];   // inferred: string[]

// annotate the boundary, infer the body
function total(xs: number[]): number {
  return xs.reduce((a, x) => a + x, 0);
}
3.2 — const assertions and satisfies
TypeScript
const route = { path: "/x", method: "GET" } as const;
// route.method is the literal "GET", not string

const cfg = { port: 3000 } satisfies Record<string, number>;
// satisfies checks the shape WITHOUT widening cfg's type
Ch. 04
Interfaces
Named object contracts — extendable, mergeable, the default for public shapes.
4.1 — Declaring and extending
TypeScript
interface Animal { name: string; legs: number; }
interface Dog extends Animal { breed: string; }

interface Repo<T> {
  get(id: string): Promise<T | null>;
  save(x: T): Promise<void>;
}
4.2 — Interface vs type alias

Both describe object shapes. Interfaces support declaration merging and read better in errors; type aliases can express unions, tuples and mapped/conditional types that interfaces cannot. Practical rule: interfaces for public object contracts, type aliases for everything else.

Ch. 05
Aliases & Unions
Union and intersection types — the algebra the type system is built on.
5.1 — Unions and intersections
TypeScript
type Id = string | number;            // either
type Staff = Person & { role: string };  // both

type Result<T> =
  | { ok: true; value: T }
  | { ok: false; error: string };
A union of object shapes with a shared literal field is a discriminated union (Ch. 14) — the single most useful modelling tool in TypeScript. Reach for it before classes or enums.
Ch. 06
Functions
Parameter types, optional/rest params, overloads, and call signatures.
6.1 — Signatures, defaults, rest
TypeScript
function greet(name: string, title = "friend", ...tags: string[]): string {
  return `${title} ${name} ${tags.join(" ")}`;
}

// Function type alias
type BinOp = (a: number, b: number) => number;
const add: BinOp = (a, b) => a + b;  // params inferred
6.2 — Overloads
TypeScript
function len(x: string): number;
function len(x: unknown[]): number;
function len(x: string | unknown[]) { return x.length; }
// callers see only the two specific signatures
Ch. 07
Generics
Parametric types — write the relationship once, get it everywhere.
7.1 — Generic functions and constraints
TypeScript
function first<T>(xs: T[]): T | undefined { return xs[0]; }

// constrain T to things with a length
function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}
7.2 — Inference and defaults

Generic parameters are usually inferred from arguments — you rarely write them at the call site. Provide defaults (<T = string>) for ergonomic APIs, and use keyof + indexed access to relate parameters to each other:

TypeScript
function prop<T, K extends keyof T>(o: T, k: K): T[K] {
  return o[k];   // return type tracks the exact key
}
Ch. 08
Classes
Class syntax, access modifiers, abstract members, parameter properties.
8.1 — Members and visibility
TypeScript
class Account {
  private #balance = 0;          // true private (runtime)
  constructor(public readonly owner: string) {}  // param property
  deposit(n: number) { this.#balance += n; }
  get balance() { return this.#balance; }
}
TypeScript's private is compile-time only. For runtime privacy use the JS #field syntax — it can't be accessed from outside the class even via bracket notation.
Ch. 09
Enums & Literals
Why literal unions usually beat enums.
9.1 — Enums
TypeScript
enum Dir { Up, Down }        // numeric, reverse-mapped
const enum Log { Info = "info" }  // inlined, no runtime object
9.2 — Prefer literal unions
TypeScript
type Dir = "up" | "down";  // no runtime cost, narrows cleanly
const DIRS = ["up", "down"] as const;
type Dir2 = (typeof DIRS)[number];   // derive the union from data
Literal-union + as const array gives you the type AND an iterable list with one source of truth — enums force you to maintain two.
Ch. 10
Narrowing
How the compiler refines a union to a specific type inside a branch.
10.1 — The narrowing toolbox
TypeScript
function f(x: string | number | null) {
  if (x == null) return;                 // null & undefined
  if (typeof x === "string") x.trim(); // typeof
  else x.toFixed(2);                    // x is number here
}

// user-defined type guard
function isCat(a: Animal): a is Cat { return "purr" in a; }
Ch. 11
Utility Types
The built-in type transformers you'll reach for constantly.
11.1 — The everyday set
UtilityDoes
Partial<T>all properties optional
Required<T>all properties required
Readonly<T>all properties readonly
Pick<T, K>keep only keys K
Omit<T, K>drop keys K
Record<K, V>object with keys K, values V
Returntype<F>a function's return type
Awaited<P>unwrap a Promise
Ch. 12
Mapped Types
Transform every key of a type programmatically.
12.1 — Building your own utilities
TypeScript
type Nullable<T> = { [K in keyof T]: T[K] | null };

// key remapping + modifiers
type Getters<T> = {
  [K in keyof T as `get${string & K}`]: () => T[K]
};
// -readonly / -? remove the modifiers
Ch. 13
Conditional Types
Types that branch — the basis of every advanced library type.
13.1 — extends ? : and infer
TypeScript
type ElementOf<T> = T extends (infer U)[] ? U : never;
type A = ElementOf<string[]>;   // string

// distributive over unions
type NonNull<T> = T extends null | undefined ? never : T;
A conditional type over a naked type parameter distributes across a union — NonNull<string | null> becomes NonNull<string> | NonNull<null>string. Wrap in a tuple [T] to switch distribution off.
Ch. 14
Discriminated Unions
The single highest-leverage modelling pattern in TypeScript.
14.1 — Tag, switch, exhaustiveness
TypeScript
type Shape =
  | { kind: "circle"; r: number }
  | { kind: "rect"; w: number; h: number };

function area(s: Shape): number {
  switch (s.kind) {
    case "circle": return Math.PI * s.r ** 2;
    case "rect":   return s.w * s.h;
  }
  const _exhaustive: never = s;  // compile error if a case is missed
  return _exhaustive;
}
The never exhaustiveness check turns "I added a new variant and forgot to handle it" from a production bug into a compile error.
Ch. 15
Template Literal Types
String types computed from other types.
15.1 — Composing string shapes at the type level
TypeScript
type Event = "click" | "hover";
type Handler = `on${Capitalize<Event>}`;
// "onClick" | "onHover"

type Route = `/${string}`;   // any path starting with /
Ch. 16
Modules
ES module syntax, type-only imports, and resolution.
16.1 — Type-only imports
TypeScript
import type { User } from "./types";   // erased entirely
import { type User, save } from "./api"; // mixed
export type { User };
Use import type for anything used only in type position. It guarantees the import is erased — preventing accidental runtime cycles and keeping bundles lean.
Ch. 17
Declaration Files
Typing untyped JavaScript with .d.ts.
17.1 — Ambient declarations
TypeScript
// globals.d.ts
declare global {
  interface Window { __APP_VERSION__: string; }
}

// module-shim.d.ts — type an untyped package
declare module "legacy-lib" {
  export function doThing(x: number): string;
}
Ch. 18
tsconfig
The compiler options that actually matter.
18.1 — A sane baseline
json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noEmit": true,
    "skipLibCheck": true
  }
}
noUncheckedIndexedAccess is off in the default strict set but catches a huge class of "undefined is not a function" bugs from array/record access. Turn it on for new projects.
Ch. 19
Async Types
Promises, Awaited, and typing async boundaries.
19.1 — Promise and Awaited
TypeScript
async function load(id: string): Promise<User> {
  const r = await fetch(`/u/${id}`);
  if (!r.ok) throw new Error(`HTTP ${r.status}`);
  return r.json() as Promise<User>;
}

type U = Awaited<ReturnType<typeof load>>;  // User
Ch. 20
Strictness & Migration
Adopting TypeScript in an existing JS codebase without a big-bang rewrite.
20.1 — Incremental path
  1. Add tsconfig.json with allowJs + checkJs: false — TS compiles JS but doesn't check it yet
  2. Rename leaf files .js → .ts one at a time; fix errors locally
  3. Turn on strict per-directory via separate tsconfigs, or file-by-file with // @ts-check
  4. Treat any as tech debt — grep for it in review; ratchet the count down, never up
Don't chase 100% type purity on day one. A codebase that's 80% well-typed with honest unknown at the edges is far safer than one papered over with any to make the build green.
REF
Type Cheatsheet
The operators and utilities worth memorising.
Type operators
OperatorMeaning
keyof Tunion of T's keys
T[K]indexed access (value type at K)
typeof vthe type of a value
T extends U ? X : Yconditional type
infer Rcapture a type inside a conditional
{ [K in keyof T]: ... }mapped type
as constliteral, deeply readonly
x satisfies Tcheck shape without widening
x as Tassertion (escape hatch — last resort)
x!non-null assertion