DEV Community

Cover image for TypeScript Expert Revision Handbook
Animesh Pandey
Animesh Pandey

Posted on

TypeScript Expert Revision Handbook

📑 Table of Contents


🟦 Fundamentals (Expert Level)


1. 🧩 Type System vs Runtime Behavior

Definition:
TypeScript is a compile-time type system — all types are erased at runtime.

✅ Key Points

  • Types are structural, not nominal (duck typing).
  • Type checking = compile-time only.
  • At runtime, it’s just JavaScript.

⚠️ Gotchas

  • Type safety doesn’t prevent runtime errors:
function greet(name: string) {
  return "Hello " + name.toUpperCase();
}
greet((123 as unknown) as string); // compiles, but runtime crash
Enter fullscreen mode Exit fullscreen mode
  • No runtime enforcement — you need runtime validators (zod, io-ts, yup).

🎯 Interview One-Liner

“TypeScript’s types disappear at runtime. They make dev-time guarantees, but runtime safety requires explicit validation.”


2. 🔢 Core Primitive Types

✅ Basic Types

  • string, number, boolean
  • null, undefined
  • symbol, bigint

✅ Special Types

  • any → Opt-out of type system (unsafe).
  • unknown → Safer alternative; must narrow before use.
  • never → Function that never returns (errors, infinite loops).
  • void → Function returns nothing.

⚠️ Gotchas

let x: any = 42;
x.toUpperCase(); // compiles, runtime crash

let y: unknown = 42;
y.toUpperCase(); // ❌ compile error
Enter fullscreen mode Exit fullscreen mode

🎯 Interview One-Liner

any disables safety. Prefer unknown when you don’t know the type, since it forces narrowing before use.”


3. 🏷️ Type Assertions vs Casting

Definition:
Tell the compiler “trust me, this is of type X.”

✅ Syntax

const el = document.getElementById("foo") as HTMLDivElement;
Enter fullscreen mode Exit fullscreen mode

✅ Non-null Assertion

const el = document.getElementById("foo")!; // never null
Enter fullscreen mode Exit fullscreen mode

⚠️ Gotchas

  • Overusing assertions bypasses safety.
  • Wrong assertions → runtime crashes.
  • Non-null ! can hide real null bugs.

🎯 Interview One-Liner

“Assertions tell TS to trust you. They don’t change runtime — overuse can hide real errors.”


4. 🔍 Type Narrowing (Control Flow Analysis)

Definition:
TS refines types based on runtime checks.

✅ Techniques

  • typeof
  • instanceof
  • Equality checks
  • Discriminated unions (tag property)

Example

function printLen(x: string | string[]) {
  if (typeof x === "string") console.log(x.length);
  else console.log(x.length); // string[] length
}
Enter fullscreen mode Exit fullscreen mode

⚠️ Gotchas

  • Narrowing only works if TS can see the check.
  • External function calls won’t narrow unless you define type predicates.
function isString(x: unknown): x is string {
  return typeof x === "string";
}
Enter fullscreen mode Exit fullscreen mode

🎯 Interview One-Liner

“TypeScript narrows unions using control flow. You can extend it with custom type predicates.”


5. 🚦 Strict Null Checking

Definition:
With strictNullChecks, null and undefined are not assignable to other types unless explicitly included.

✅ Example

let x: string = null; // ❌ error under strictNullChecks
let y: string | null = null; // ✅
Enter fullscreen mode Exit fullscreen mode

✅ Optional Chaining + Nullish Coalescing

const name = user?.profile?.name ?? "Guest";
Enter fullscreen mode Exit fullscreen mode

⚠️ Gotchas

  • Many JS libs don’t account for undefined.
  • Without strictNullChecks, you get unsound behavior (nullable everywhere).

🎯 Interview One-Liner

“Strict null checks make nullability explicit, avoiding the billion-dollar mistake. Combine with optional chaining and nullish coalescing.”


6. 🔎 Structural Typing (vs Nominal)

Definition:
TS uses structural typing — compatibility is based on shape, not declared type.

✅ Example

type Point = { x: number; y: number };
type Coord = { x: number; y: number };

let a: Point = { x: 1, y: 2 };
let b: Coord = a; // ✅ works (same shape)
Enter fullscreen mode Exit fullscreen mode

⚠️ Gotchas

  • Extra properties are rejected in object literals:
let p: Point = { x: 1, y: 2, z: 3 }; // ❌ error
Enter fullscreen mode Exit fullscreen mode
  • But extra props are allowed if passed as variable:
const tmp = { x: 1, y: 2, z: 3 };
let p: Point = tmp; // ✅ works
Enter fullscreen mode Exit fullscreen mode

🎯 Interview One-Liner

“TypeScript is structurally typed — if it quacks like a duck, it’s assignable. But object literals have stricter excess property checks.”


7. 📝 Type Aliases vs Interfaces

Definition:
Two ways to define object types.

✅ Differences

  • Interface: extendable via declaration merging.
  • Type Alias: supports unions, intersections, primitives.

Example

interface A { x: number }
interface A { y: number } // merged
type B = { x: number } & { y: number } // no merging, must intersect
Enter fullscreen mode Exit fullscreen mode

⚠️ Gotchas

  • Prefer interface for public APIs (extendable).
  • Prefer type for unions and complex compositions.

🎯 Interview One-Liner

“Interfaces are open (mergeable), types are closed but more flexible (unions, intersections). Use each where it fits.”


8. 📦 Enums vs Literal Types

✅ Enums

enum Direction { Up, Down }
Enter fullscreen mode Exit fullscreen mode

✅ Literal Unions

type Direction = "up" | "down";
Enter fullscreen mode Exit fullscreen mode

⚠️ Gotchas

  • String literal unions are usually better (type-safe, no runtime overhead).
  • Enums generate runtime objects → heavier.
  • const enum is inlined at compile time but can break tooling.

🎯 Interview One-Liner

“Prefer literal unions over enums — they’re lighter and more type-safe. Use enums only when runtime mapping is required.”


🟦 Advanced Types


1. ➕ Union & Intersection Types

✅ Union (|)

  • Represents either type.
type Input = string | number;
let val: Input = "hi";  // ✅
val = 42;               // ✅
Enter fullscreen mode Exit fullscreen mode

✅ Intersection (&)

  • Combines multiple types.
type Person = { name: string };
type Worker = { company: string };
type Employee = Person & Worker; 
// { name: string; company: string }
Enter fullscreen mode Exit fullscreen mode

⚠️ Gotchas

  • Union narrows to shared members:
function len(x: string | string[]) {
  return x.length; // works (length exists on both)
}
Enter fullscreen mode Exit fullscreen mode
  • Intersection of incompatible types → never:
type Impossible = string & number; // never
Enter fullscreen mode Exit fullscreen mode

🎯 One-Liner

“Unions = OR, intersections = AND. Incompatible intersections collapse to never.”


2. 🔢 Literal Types & Const Assertions

✅ Literal Types

let dir: "up" | "down";
dir = "up"; // ✅
dir = "left"; // ❌
Enter fullscreen mode Exit fullscreen mode

as const

  • Locks values into literal types.
const colors = ["red", "green"] as const;
type Color = typeof colors[number]; // "red" | "green"
Enter fullscreen mode Exit fullscreen mode

🎯 One-Liner

“Literal types restrict values to exact strings/numbers. as const freezes arrays/objects for inference.”


3. 📝 Template Literal Types

✅ Key Points

  • Build strings at type level.
type Event = `on${Capitalize<"click" | "hover">}`;
// "onClick" | "onHover"
Enter fullscreen mode Exit fullscreen mode

✅ Use Cases

  • Event handler names.
  • Typed CSS props.
  • API route patterns.

🎯 One-Liner

“Template literal types compose strings at type level — great for event names, routes, and API typings.”


4. 🧮 Conditional Types

Definition:
T extends U ? X : Y

✅ Example

type IsString<T> = T extends string ? true : false;
type A = IsString<"hi">;   // true
type B = IsString<number>; // false
Enter fullscreen mode Exit fullscreen mode

✅ Distributive Behavior

type ToArray<T> = T extends any ? T[] : never;
type C = ToArray<string | number>; // string[] | number[]
Enter fullscreen mode Exit fullscreen mode

⚠️ Gotchas

  • Distribution only happens on naked type parameters.
  • Use brackets to disable:
type NoDistrib<T> = [T] extends [any] ? T[] : never;
Enter fullscreen mode Exit fullscreen mode

🎯 One-Liner

“Conditional types let you branch at type level. By default they distribute over unions.”


5. 🗂️ Mapped Types

✅ Basics

type OptionsFlags<T> = {
  [K in keyof T]: boolean;
};
type Features = { darkMode: () => void };
type Flags = OptionsFlags<Features>; 
// { darkMode: boolean }
Enter fullscreen mode Exit fullscreen mode

✅ Modifiers

  • readonly
  • ? optional
  • -readonly / -? to remove
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
Enter fullscreen mode Exit fullscreen mode

🎯 One-Liner

“Mapped types iterate over keys to transform shape — add/remove optionality, readonly, etc.”


6. 🛠️ Utility Types

✅ Built-ins

  • Partial<T> → all optional
  • Required<T> → all required
  • Readonly<T> → all readonly
  • Pick<T, K> → subset
  • Omit<T, K> → all except
  • Record<K, V> → dict type
  • ReturnType<T> → infer fn return
  • Parameters<T> → tuple of args
  • InstanceType<T> → type of new T()

Example

type Todo = { id: number; title: string; done?: boolean };
type TodoDraft = Partial<Todo>; // all optional
Enter fullscreen mode Exit fullscreen mode

🎯 One-Liner

“Utility types like Partial, Pick, Omit, Record abstract common transformations of object types.”


7. 🧩 Key Remapping in Mapped Types (TS 4.1+)

type Prefix<T> = {
  [K in keyof T as `on${Capitalize<string & K>}`]: T[K]
};
type Events = Prefix<{ click: () => void }>;
// { onClick: () => void }
Enter fullscreen mode Exit fullscreen mode

🎯 One-Liner

“Mapped types can rename keys dynamically using as clauses and template literals.”


8. 🌀 Recursive & Deep Types

✅ Recursive Types

type JSONValue = string | number | boolean | JSONValue[] | { [k: string]: JSONValue };
Enter fullscreen mode Exit fullscreen mode

✅ Deep Utility

type DeepPartial<T> = { [K in keyof T]?: DeepPartial<T[K]> };
Enter fullscreen mode Exit fullscreen mode

⚠️ Gotchas

  • Deep recursion can slow compiler dramatically.
  • Use cautiously in very large codebases.

🎯 One-Liner

“Recursive types enable JSON-like structures and deep utilities, but may hit compiler perf limits.”


🟦 Generics in Depth


1. 🧩 Generic Functions

Definition:
Functions parameterized with type variables.

✅ Example

function identity<T>(arg: T): T {
  return arg;
}
const a = identity<string>("hello");
const b = identity(42); // inferred as number
Enter fullscreen mode Exit fullscreen mode

🎯 One-Liner

“Generics make functions reusable across types, with inference for convenience.”


2. 🔒 Constraints (extends)

Restrict generics to a subset of types.

✅ Example

function getLength<T extends { length: number }>(x: T) {
  return x.length;
}
getLength("hi");       // ✅
getLength([1,2,3]);    // ✅
getLength(42);         // ❌
Enter fullscreen mode Exit fullscreen mode

🎯 One-Liner

“Constraints limit generics with extends, allowing access to known members.”


3. 🏷️ Generic Interfaces & Classes

✅ Interfaces

interface Box<T> { value: T }
const stringBox: Box<string> = { value: "hi" };
Enter fullscreen mode Exit fullscreen mode

✅ Classes

class Container<T> {
  constructor(public value: T) {}
}
const c = new Container<number>(123);
Enter fullscreen mode Exit fullscreen mode

🎯 One-Liner

“Generics extend beyond functions — interfaces and classes can also be parameterized.”


4. ⚖️ Default Generic Parameters

Provide defaults for flexibility.

✅ Example

interface ApiResponse<T = any> {
  data: T;
  error?: string;
}
const res: ApiResponse = { data: "hi" }; // T defaults to any
Enter fullscreen mode Exit fullscreen mode

🎯 One-Liner

“Defaults reduce boilerplate when generic type can be inferred or safely assumed.”


5. 🔄 Keyof & Indexed Access with Generics

✅ Keyof

type Keys<T> = keyof T;
type User = { id: number; name: string };
type UKeys = Keys<User>; // "id" | "name"
Enter fullscreen mode Exit fullscreen mode

✅ Indexed Access

type Value<T, K extends keyof T> = T[K];
type NameType = Value<User, "name">; // string
Enter fullscreen mode Exit fullscreen mode

🎯 One-Liner

“Keyof + indexed access lets you make type-safe utilities (like Pick/Omit).”


6. 🎭 Conditional Generics

Generics inside conditional types = super powerful.

✅ Example

type Flatten<T> = T extends any[] ? T[number] : T;
type A = Flatten<string[]>; // string
type B = Flatten<number>;   // number
Enter fullscreen mode Exit fullscreen mode

🎯 One-Liner

“Conditional generics let you branch inside generics — e.g., flatten arrays, unwrap promises.”


7. ⚖️ Variance (Covariance vs Contravariance)

Definition:
How subtyping interacts with generics.

✅ Covariance

  • Safe to use subtype in place of supertype.
let str: string = "hi";
let val: string | number = str; // ✅
Enter fullscreen mode Exit fullscreen mode

✅ Contravariance

  • Function parameters are contravariant:
type Fn<T> = (x: T) => void;
let fn: Fn<string | number> = (x: string) => {}; // ✅
Enter fullscreen mode Exit fullscreen mode

⚠️ Gotchas

  • TypeScript uses bivariant function parameters by default for compatibility (unsafe but practical).
  • --strictFunctionTypes enforces contravariance.

🎯 One-Liner

“Function parameters are contravariant, return types are covariant. TS is bivariant by default unless strictFunctionTypes is on.”


8. 🌀 Higher-Kinded Types (HKTs, Workarounds)

TypeScript doesn’t support true HKTs (types parameterized over type constructors), but you can simulate.

✅ Example

interface Functor<F> {
  map<A, B>(fa: F & { value: A }, fn: (a: A) => B): F & { value: B }
}
Enter fullscreen mode Exit fullscreen mode

Or libraries like fp-ts emulate HKT with encoding tricks.

🎯 One-Liner

“TypeScript lacks HKTs, but libraries like fp-ts emulate them with encoding patterns.”


9. 🔗 Generics in React

✅ Typing useState

const [val, setVal] = useState<string | null>(null);
Enter fullscreen mode Exit fullscreen mode

✅ Typing useReducer

type Action = { type: "inc" } | { type: "dec" };
function reducer(s: number, a: Action): number {
  return a.type === "inc" ? s+1 : s-1;
}
const [count, dispatch] = useReducer(reducer, 0);
Enter fullscreen mode Exit fullscreen mode

✅ Typing Props

interface ButtonProps<T extends "button" | "a"> {
  as: T;
  props: T extends "a" ? { href: string } : { onClick: () => void };
}
Enter fullscreen mode Exit fullscreen mode

🎯 One-Liner

“Generics in React type hooks, props, reducers, and flexible components (e.g., polymorphic components).”


🟦 Type Inference & Compatibility


1. 🔍 Type Inference Basics

Definition:
TypeScript infers types when not explicitly annotated.

✅ Examples

let x = 42;      // inferred as number
let y = [1, 2];  // inferred as number[]
Enter fullscreen mode Exit fullscreen mode

✅ Contextual Typing

  • Function parameters infer type from usage:
window.onmousedown = (e) => { 
  console.log(e.button); // e inferred as MouseEvent
};
Enter fullscreen mode Exit fullscreen mode

🎯 One-Liner

“Type inference works both from initializer values and from context (like callbacks).”


2. ⚖️ Excess Property Checks

Definition:
TypeScript enforces stricter checks for object literals.

✅ Example

type User = { name: string };
const u1: User = { name: "Alice", age: 30 }; // ❌ excess property
const tmp = { name: "Alice", age: 30 };
const u2: User = tmp; // ✅ allowed (assignment, not literal)
Enter fullscreen mode Exit fullscreen mode

⚠️ Gotchas

  • Literal checks prevent typos in inline objects.
  • Assigning via variable bypasses check.

🎯 One-Liner

“Object literals get extra checks for unknown properties. Assign via variable to bypass.”


3. 🔄 Type Widening & Narrowing

✅ Widening

  • Without as const, literal types widen:
let a = "hi";     // type string (widened)
const b = "hi";   // type "hi" (literal)
Enter fullscreen mode Exit fullscreen mode

✅ Narrowing

  • Control-flow analysis narrows union types:
function f(x: string | null) {
  if (x !== null) return x.toUpperCase(); // narrowed to string
}
Enter fullscreen mode Exit fullscreen mode

⚠️ Gotchas

  • let x = null → type is any unless strictNullChecks.
  • Arrays widen unless frozen with as const.

🎯 One-Liner

“TS widens literals by default. Use as const to keep narrow literal types.”


4. 🏷️ Assignability & Compatibility

Definition:
TypeScript is structurally typed → assignability depends on shape.

✅ Example

type Point = { x: number; y: number };
type Coord = { x: number; y: number; z?: number };

let p: Point = { x: 1, y: 2 };
let c: Coord = p;  // ✅ works
p = c;             // ✅ works (z optional)
Enter fullscreen mode Exit fullscreen mode

⚠️ Gotchas

  • Function parameters are bivariant by default (unsafe).
  • Use strictFunctionTypes for true contravariance.

🎯 One-Liner

“TS uses structural typing: if shapes match, types are compatible. Functions default to bivariant params unless strict.”


5. 🧩 any vs unknown vs never

✅ any

  • Opt-out of type checking.
  • Can be assigned to/from anything.
let a: any = 42;
a.foo.bar(); // compiles, runtime error
Enter fullscreen mode Exit fullscreen mode

✅ unknown

  • Top type (safe any).
  • Must be narrowed before use.
let b: unknown = 42;
b.toUpperCase(); // ❌ error
if (typeof b === "string") b.toUpperCase(); // ✅
Enter fullscreen mode Exit fullscreen mode

✅ never

  • Bottom type (no value possible).
  • Used in exhaustiveness checks.
type Shape = { kind: "circle" } | { kind: "square" };
function area(s: Shape): number {
  switch (s.kind) {
    case "circle": return 1;
    case "square": return 2;
    default: const _exhaustive: never = s; // ✅ ensures all cases handled
  }
}
Enter fullscreen mode Exit fullscreen mode

🎯 One-Liner

any disables safety, unknown forces narrowing, never represents impossible cases.”


6. 🧠 Inference in Functions

✅ Return Inference

function add(a: number, b: number) {
  return a + b; // inferred as number
}
Enter fullscreen mode Exit fullscreen mode

✅ Generic Inference

function first<T>(arr: T[]): T {
  return arr[0];
}
const v = first(["a", "b"]); // v: string
Enter fullscreen mode Exit fullscreen mode

⚠️ Gotchas

  • Sometimes inference is too wide:
function f() { return Math.random() ? "a" : "b"; }
// inferred as string, not "a"|"b"
Enter fullscreen mode Exit fullscreen mode

→ Fix with as const or explicit typing.

🎯 One-Liner

“Function return types are inferred, but unions may widen. Use as const or annotations for precision.”


7. 🛠️ Type Compatibility in Enums

✅ Numeric Enums

  • Compatible with numbers.
enum Dir { Up, Down }
let d: Dir = 0; // ✅
Enter fullscreen mode Exit fullscreen mode

✅ String Enums

  • Not compatible with strings unless explicitly.
enum Color { Red = "red" }
let c: Color = "red"; // ❌
Enter fullscreen mode Exit fullscreen mode

🎯 One-Liner

“Numeric enums are assignable to numbers, but string enums require exact matches.”


8. ⚡ Type Compatibility in Tuples vs Arrays

✅ Tuples

type Pair = [string, number];
let p: Pair = ["hi", 42];
Enter fullscreen mode Exit fullscreen mode
  • Tuples have fixed length, arrays are flexible.

⚠️ Gotchas

let t: [number, number] = [1, 2];
t.push(3); // ✅ allowed! TS doesn’t enforce length at runtime
Enter fullscreen mode Exit fullscreen mode

🎯 One-Liner

“Tuples enforce order but not length at runtime — they’re arrays under the hood.”


🟦 Type Safety in JavaScript Interop


1. 📜 Ambient Declarations (declare)

Definition:
Tell TypeScript about variables/modules that exist at runtime (JS), but aren’t defined in TS code.

✅ Examples

declare const VERSION: string;
console.log(VERSION); // TS knows VERSION is string

declare module "legacy-lib" {
  export function legacyFn(): void;
}
Enter fullscreen mode Exit fullscreen mode

⚠️ Gotchas

  • Declarations don’t generate runtime code — only inform compiler.
  • If declaration doesn’t match actual runtime → runtime errors.

🎯 One-Liner

declare informs the type system about external JS values but doesn’t emit code. Accuracy is critical or you’ll get runtime errors.”


2. 📦 Type Declarations for JS Libraries

✅ Strategies

  • @types packages:
npm install --save-dev @types/lodash
Enter fullscreen mode Exit fullscreen mode
  • Manual d.ts files for missing types:
// lodash.d.ts
declare module "lodash" {
  export function chunk<T>(arr: T[], size: number): T[][];
}
Enter fullscreen mode Exit fullscreen mode

⚠️ Gotchas

  • If types are outdated/mismatched → TS lies about API.
  • Can “augment” instead of redefining (see below).

🎯 One-Liner

“Missing types for JS libs? Install @types or write .d.ts files — but keep them accurate with the runtime.”


3. 🧩 Module Augmentation & Declaration Merging

Definition:
Extend existing module or type definitions without rewriting.

✅ Example

// add a method to lodash
declare module "lodash" {
  export function customHello(): string;
}
Enter fullscreen mode Exit fullscreen mode

✅ Declaration Merging

  • Interfaces with the same name merge:
interface User { id: number }
interface User { name: string }
const u: User = { id: 1, name: "Alice" }; // ✅
Enter fullscreen mode Exit fullscreen mode

⚠️ Gotchas

  • Augmentation is global → can cause conflicts across packages.
  • Prefer module augmentation for libs, not any hacks.

🎯 One-Liner

“Declaration merging/augmentation extends types safely. Useful for adding custom fields or extending 3rd-party libraries.”


4. 🎯 as const for Safe Interop

Definition:
as const freezes literals into readonly narrow types.

✅ Example

const roles = ["admin", "user", "guest"] as const;
type Role = typeof roles[number]; 
// "admin" | "user" | "guest"
Enter fullscreen mode Exit fullscreen mode

✅ Use Case

  • Ensures config/constants match at runtime.
  • Great for enums/union types from JS arrays.

🎯 One-Liner

as const locks JS literals into readonly narrow types — perfect for role lists, config, or enum-like structures.”


5. ⚙️ tsconfig Strictness Flags

✅ Important Flags

  • strict → enables all strict checks.
  • noImplicitAny → no silent any.
  • strictNullChecks → enforce explicit nullability.
  • noUncheckedIndexedAccess → array lookups can return undefined.
  • exactOptionalPropertyTypes? means strictly optional.

⚠️ Gotchas

  • Teams often disable strictness → leaks any.
  • Migrating large JS → TS requires incremental adoption.

🎯 One-Liner

“Always enable strict. Key flags like noImplicitAny and strictNullChecks catch hidden bugs in JS interop.”


6. 🛠️ Working with Untyped JS Objects

✅ Options

  • Use unknown + type guards:
function isUser(u: any): u is { id: number } {
  return typeof u.id === "number";
}
Enter fullscreen mode Exit fullscreen mode
  • Or validate at runtime with Zod/io-ts:
import { z } from "zod";
const User = z.object({ id: z.number(), name: z.string() });
type User = z.infer<typeof User>;
Enter fullscreen mode Exit fullscreen mode

🎯 One-Liner

“Untyped JS objects should be validated at runtime with schemas (Zod/io-ts), not just trusted via any.”


7. 🔗 Migrating JS → TS (Gradual Typing)

✅ Strategies

  • Rename .js.ts or .tsx.
  • Use allowJs + checkJs in tsconfig to gradually type JS.
  • Add JSDoc annotations:
/**
 * @param {string} name
 * @returns {string}
 */
function greet(name) { return "Hello " + name; }
Enter fullscreen mode Exit fullscreen mode
  • TS infers types from JSDoc until converted.

⚠️ Gotchas

  • JSDoc typing is weaker than TS proper.
  • Incremental migration often mixes any → must clean up later.

🎯 One-Liner

“Migrate JS → TS gradually: enable checkJs, add JSDoc types, then refactor into full TypeScript.”


8. 🛡️ Runtime vs Compile-Time Safety

Core Rule:
Types vanish at runtime → if interoping with JS, you need runtime checks.

✅ Example

function safeParse(json: string): unknown {
  try { return JSON.parse(json) } catch { return null }
}
const data = safeParse("not-json"); // type unknown
// must validate before using
Enter fullscreen mode Exit fullscreen mode

🎯 One-Liner

“TypeScript only checks at compile time. For JS interop, add runtime validation to truly guarantee safety.”


🟦 Type Guards & Narrowing


1. 🔎 Built-in Type Guards: typeof

Definition:
typeof lets TypeScript narrow primitive types.

✅ Example

function log(val: string | number) {
  if (typeof val === "string") {
    console.log(val.toUpperCase()); // val: string
  } else {
    console.log(val.toFixed(2));    // val: number
  }
}
Enter fullscreen mode Exit fullscreen mode

⚠️ Gotchas

  • Only works on primitives: "string" | "number" | "boolean" | "bigint" | "symbol" | "undefined" | "object" | "function".
  • Doesn’t differentiate null vs object (typeof null === "object").

🎯 One-Liner

“Use typeof for primitives — but note typeof null === 'object'.”


2. 🏗️ Built-in Type Guards: instanceof

Definition:
Narrow objects by checking prototype chain.

✅ Example

function handleError(e: Error | string) {
  if (e instanceof Error) {
    console.error(e.message); // Error
  } else {
    console.error(e); // string
  }
}
Enter fullscreen mode Exit fullscreen mode

⚠️ Gotchas

  • Only works with classes/constructors (not plain objects).
  • Inheritance hierarchy is respected.

🎯 One-Liner

“Use instanceof for class-based narrowing — it checks prototype chain.”


3. 🧩 In-Operator Narrowing

Definition:
Check if a property exists in object → narrows union.

✅ Example

type Cat = { meow: () => void };
type Dog = { bark: () => void };

function speak(animal: Cat | Dog) {
  if ("meow" in animal) animal.meow();
  else animal.bark();
}
Enter fullscreen mode Exit fullscreen mode

🎯 One-Liner

“The in operator narrows unions by checking for property existence.”


4. 🏷️ Discriminated (Tagged) Unions

Definition:
Unions with a common literal field (“tag”) for safe narrowing.

✅ Example

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; side: number };

function area(s: Shape) {
  switch (s.kind) {
    case "circle": return Math.PI * s.radius ** 2;
    case "square": return s.side ** 2;
  }
}
Enter fullscreen mode Exit fullscreen mode

🎯 One-Liner

“Discriminated unions use a common literal field to guarantee safe narrowing in switches.”


5. 🛠️ Custom Type Predicates

Definition:
User-defined functions that tell TS a condition implies a type.

✅ Example

function isString(x: unknown): x is string {
  return typeof x === "string";
}

function log(x: unknown) {
  if (isString(x)) console.log(x.toUpperCase());
}
Enter fullscreen mode Exit fullscreen mode

🎯 One-Liner

“Custom type predicates (x is T) let you teach TS how to narrow beyond built-ins.”


6. 🚦 Exhaustiveness Checking with never

Definition:
Force handling all cases in a union.

✅ Example

type Shape =
  | { kind: "circle"; r: number }
  | { kind: "square"; s: number };

function perimeter(s: Shape) {
  switch (s.kind) {
    case "circle": return 2 * Math.PI * s.r;
    case "square": return 4 * s.s;
    default:
      const _exhaustive: never = s; // compile error if new case added
      return _exhaustive;
  }
}
Enter fullscreen mode Exit fullscreen mode

🎯 One-Liner

“Exhaustiveness checks with never ensure all union cases are handled — future-proofing code.”


7. 🧠 Control Flow Analysis

Definition:
TS tracks variables through branches to narrow automatically.

✅ Example

function f(x: string | null) {
  if (!x) return; 
  // x is now string, since null was filtered out
  return x.toUpperCase();
}
Enter fullscreen mode Exit fullscreen mode

⚠️ Gotchas

  • TS is flow-sensitive — order matters.
  • Reassignments can widen again.

🎯 One-Liner

“TS narrows types flow-sensitively — once a check passes, TS refines type until reassignment.”


8. 🧩 Assertion Functions (TS 3.7+)

Definition:
Custom functions that throw on invalid values while narrowing type.

✅ Example

function assertIsString(x: any): asserts x is string {
  if (typeof x !== "string") throw new Error("Not a string");
}

function shout(x: any) {
  assertIsString(x);
  console.log(x.toUpperCase()); // x: string
}
Enter fullscreen mode Exit fullscreen mode

🎯 One-Liner

“Assertion functions throw at runtime and tell TS the variable is narrowed if no error occurs.”


9. 📦 Combining Guards

✅ Example

function handle(input: string | number | null) {
  if (input == null) return;          // null/undefined filtered
  if (typeof input === "string") {    // narrowed to string
    return input.toUpperCase();
  }
  return input.toFixed(2);            // number
}
Enter fullscreen mode Exit fullscreen mode

🎯 One-Liner

“Combine guards (== null, typeof, instanceof) for precise narrowing of complex unions.”


🟦 Advanced Patterns


1. 🏷️ Branded Types (Nominal Typing in TS)

Problem:
TS is structuraltype UserId = string is indistinguishable from any string.

Solution:
Add a “brand” field to enforce nominal typing.

✅ Example

type UserId = string & { __brand: "UserId" };
function getUser(id: UserId) {}
getUser("123" as UserId);  // ✅
getUser("random");         // ❌ must be branded
Enter fullscreen mode Exit fullscreen mode

🎯 One-Liner

“Branded types simulate nominal typing in TS, preventing accidental mixing of structurally identical types.”


2. 🌀 Opaque Types

Definition:
Similar to branded types, but completely hide the underlying type from consumers.

✅ Example

type Opaque<K, T> = T & { __TYPE__: K };
type UserId = Opaque<"UserId", string>;

function createUserId(s: string): UserId {
  return s as UserId;
}
Enter fullscreen mode Exit fullscreen mode

🎯 One-Liner

“Opaque types hide implementation details and prevent misuse, forcing controlled constructors.”


3. 🛠️ Recursive Utility Types

✅ DeepPartial

type DeepPartial<T> = {
  [K in keyof T]?: DeepPartial<T[K]>;
};
Enter fullscreen mode Exit fullscreen mode

✅ DeepReadonly

type DeepReadonly<T> = {
  readonly [K in keyof T]: DeepReadonly<T[K]>;
};
Enter fullscreen mode Exit fullscreen mode

⚠️ Gotchas

  • Deep recursion can slow down compiler.
  • Use selectively in large projects.

🎯 One-Liner

“Recursive types enable deep utilities like DeepPartial, but heavy use impacts compiler performance.”


4. 🧩 Conditional & Mapped Utilities

✅ NonNullable

type NonNullable<T> = T extends null | undefined ? never : T;
Enter fullscreen mode Exit fullscreen mode

✅ Diff

type Diff<T, U> = T extends U ? never : T;
Enter fullscreen mode Exit fullscreen mode

✅ Overwrite

type Overwrite<T, U> = Omit<T, keyof U> & U;
Enter fullscreen mode Exit fullscreen mode

🎯 One-Liner

“Mapped + conditional types let you build powerful utilities like Diff, Overwrite, NonNullable.”


5. 🔄 Variadic Tuple Types

Definition:
Model tuples of variable length with generics.

✅ Example

type Push<T extends any[], V> = [...T, V];
type T1 = Push<[1,2], 3>; // [1,2,3]

type Concat<T extends any[], U extends any[]> = [...T, ...U];
type T2 = Concat<[1,2], [3,4]>; // [1,2,3,4]
Enter fullscreen mode Exit fullscreen mode

🎯 One-Liner

“Variadic tuple types let you append, prepend, or merge tuples while preserving type precision.”


6. 🏗️ Builder Pattern in TypeScript

✅ Example

class RequestBuilder {
  private url: string = "";
  private method: "GET" | "POST" = "GET";

  setUrl(url: string) { this.url = url; return this; }
  setMethod(m: "GET" | "POST") { this.method = m; return this; }
  build() { return { url: this.url, method: this.method }; }
}

const req = new RequestBuilder().setUrl("/api").setMethod("POST").build();
Enter fullscreen mode Exit fullscreen mode

🎯 One-Liner

“Builder patterns ensure chained configuration APIs with type safety and autocomplete.”


7. 🛡️ Exact Types (Prevent Excess Keys)

Problem:
TS normally allows extra props via assignment.

Solution:
Create an Exact<T, U> utility.

✅ Example

type Exact<T, U extends T> = T & { [K in Exclude<keyof U, keyof T>]?: never };

type Person = { name: string };
const p: Exact<Person, { name: string; age: number }> = { name: "A", age: 20 }; 
// ❌ error: extra 'age'
Enter fullscreen mode Exit fullscreen mode

🎯 One-Liner

“Exact types prevent excess keys, useful for APIs where only known fields are allowed.”


8. 📦 Extracting Types from Values

✅ typeof + keyof

const config = {
  roles: ["admin", "user", "guest"] as const,
};
type Role = typeof config["roles"][number]; 
// "admin" | "user" | "guest"
Enter fullscreen mode Exit fullscreen mode

🎯 One-Liner

“Use typeof and as const to derive union types from runtime values like config arrays.”


9. 🧠 Phantom Types (Static Guarantees)

Definition:
Types that exist only at compile-time, to encode invariants.

✅ Example

type Celsius = number & { __unit: "Celsius" };
type Fahrenheit = number & { __unit: "Fahrenheit" };

function toF(c: Celsius): Fahrenheit { return (c * 9/5 + 32) as Fahrenheit; }

let t: Celsius = 100 as Celsius;
toF(t); // ✅
toF(100 as Fahrenheit); // ❌
Enter fullscreen mode Exit fullscreen mode

🎯 One-Liner

“Phantom types enforce domain-specific rules (like units) without runtime cost.”


🟦 TypeScript in React


1. 🎯 Typing Component Props

✅ Functional Components

type ButtonProps = {
  label: string;
  onClick?: () => void;
};

const Button: React.FC<ButtonProps> = ({ label, onClick }) => (
  <button onClick={onClick}>{label}</button>
);
Enter fullscreen mode Exit fullscreen mode

⚠️ Gotchas

  • React.FC implicitly adds children prop — often unwanted.
  • Better to type children explicitly.
type Props = { children?: React.ReactNode };
Enter fullscreen mode Exit fullscreen mode

🎯 One-Liner

“Prefer explicit prop typing over React.FC to avoid hidden children.”


2. 🏷️ Children Typing

✅ Common Patterns

type Props = { children: React.ReactNode }; // anything renderable
type Props2 = { children: React.ReactElement }; // exactly one element
type Props3<T> = { children: (data: T) => React.ReactNode }; // render prop
Enter fullscreen mode Exit fullscreen mode

🎯 One-Liner

“Use ReactNode for generic children, ReactElement for single elements, and functions for render props.”


3. 🧩 Typing Hooks (useState, useReducer)

✅ useState

const [count, setCount] = useState<number>(0); // explicit
const [name, setName] = useState("Alice"); // inferred as string
Enter fullscreen mode Exit fullscreen mode
  • useState<T | null>(null) when initial value is null.

✅ useReducer

type Action = { type: "inc" } | { type: "dec" };
function reducer(state: number, action: Action): number {
  switch (action.type) {
    case "inc": return state + 1;
    case "dec": return state - 1;
  }
}
const [count, dispatch] = useReducer(reducer, 0);
Enter fullscreen mode Exit fullscreen mode

🎯 One-Liner

“Type state & actions explicitly in hooks. Use union types for reducers.”


4. 🌐 Typing Context Providers

✅ Example

type User = { id: string; name: string };
type UserContextType = { user: User | null; setUser: (u: User) => void };

const UserContext = React.createContext<UserContextType | undefined>(undefined);

function useUser() {
  const ctx = React.useContext(UserContext);
  if (!ctx) throw new Error("useUser must be inside UserProvider");
  return ctx;
}
Enter fullscreen mode Exit fullscreen mode

🎯 One-Liner

“Context values should include both data and setters, wrapped in a custom hook for safety.”


5. 🧵 Typing Refs & forwardRef

✅ DOM Refs

const inputRef = React.useRef<HTMLInputElement>(null);
inputRef.current?.focus();
Enter fullscreen mode Exit fullscreen mode

✅ forwardRef

type InputProps = { label: string };
const Input = React.forwardRef<HTMLInputElement, InputProps>(
  ({ label }, ref) => <input ref={ref} placeholder={label} />
);
Enter fullscreen mode Exit fullscreen mode

✅ useImperativeHandle

type Handle = { focus: () => void };
const CustomInput = React.forwardRef<Handle>((props, ref) => {
  const inputRef = React.useRef<HTMLInputElement>(null);
  React.useImperativeHandle(ref, () => ({
    focus: () => inputRef.current?.focus(),
  }));
  return <input ref={inputRef} />;
});
Enter fullscreen mode Exit fullscreen mode

🎯 One-Liner

“Use forwardRef with generic ref types. Expose imperative APIs with useImperativeHandle.”


6. 🧮 Typing Higher-Order Components (HOCs)

✅ Example

function withLoading<T>(Component: React.ComponentType<T>) {
  return (props: T & { loading: boolean }) =>
    props.loading ? <div>Loading...</div> : <Component {...props} />;
}
Enter fullscreen mode Exit fullscreen mode

⚠️ Gotchas

  • Must preserve props (T) and merge with new ones.
  • Watch out for lost generics when wrapping.

🎯 One-Liner

“HOCs should preserve original props via generics and merge additional ones.”


7. 🧑‍🔬 Typing Generic Components

✅ Example

type ListProps<T> = {
  items: T[];
  render: (item: T) => React.ReactNode;
};

function List<T>({ items, render }: ListProps<T>) {
  return <ul>{items.map(render)}</ul>;
}

<List items={[1, 2, 3]} render={(x) => <li>{x}</li>} />;
Enter fullscreen mode Exit fullscreen mode

🎯 One-Liner

“Generic components let props depend on type parameters — perfect for reusable lists and tables.”


8. 🔄 Typing Event Handlers

✅ Example

function Form() {
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.value);
  };
  return <input onChange={handleChange} />;
}
Enter fullscreen mode Exit fullscreen mode

✅ Common Event Types

  • React.MouseEvent<HTMLButtonElement>
  • React.ChangeEvent<HTMLInputElement>
  • React.FormEvent<HTMLFormElement>

🎯 One-Liner

“Use React’s synthetic event types (MouseEvent, ChangeEvent) for handlers, parameterized by element type.”


9. 🧩 Typing Polymorphic as Components

✅ Example

type PolymorphicProps<T extends React.ElementType> = {
  as?: T;
  children: React.ReactNode;
} & React.ComponentProps<T>;

function Box<T extends React.ElementType = "div">({ as, ...props }: PolymorphicProps<T>) {
  const Component = as || "div";
  return <Component {...props} />;
}

<Box as="a" href="https://ts">Link</Box>; // href type-safe
Enter fullscreen mode Exit fullscreen mode

🎯 One-Liner

“Polymorphic components use as + generics + ComponentProps<T> to forward correct props.”


🟦 Performance & Scaling TypeScript


1. ⚡ Type-Checking Performance in Large Projects

✅ Common Bottlenecks

  • Deeply nested conditional types.
  • Overuse of recursive mapped types (e.g., DeepPartial, DeepReadonly).
  • Giant union types (e.g., "A" | "B" | ... | "Z" with hundreds of members).
  • Heavy infer usage inside generics.

✅ Tools

  • tsc --diagnostics → measure type-check performance.
  • tsc --extendedDiagnostics → detailed breakdown (parse time, check time, emit time).

🎯 One-Liner

“The biggest type-check killers are deep recursion, massive unions, and heavy conditional types — profile with --diagnostics.”


2. 🛠️ Avoiding Over-Complex Types

✅ Problem

Some teams abuse TS to encode too much at type level.

type Crazy<T> = T extends string 
  ? { str: T } 
  : T extends number 
    ? { num: T } 
    : never;
Enter fullscreen mode Exit fullscreen mode
  • Hard to maintain, slows compiler.

✅ Guidelines

  • Keep types simple for DX (developer experience).
  • Don’t encode logic that belongs in runtime code.
  • Prefer branded/opaque types for safety, instead of extreme conditional gymnastics.

🎯 One-Liner

“Don’t over-engineer types — TS is for safety, not replacing runtime logic.”


3. 📦 Build Pipelines: tsc vs Babel vs SWC

✅ tsc

  • Full type-checker + emit.
  • Slowest, but canonical.

✅ Babel with @babel/preset-typescript

  • Strips types → no type-checking.
  • Fast, but must run tsc --noEmit separately.

✅ SWC (used in Next.js, Vite, Turborepo)

  • Rust-based transpiler, very fast.
  • Strips types only.

⚠️ Gotchas

  • Babel/SWC do not catch type errors — must run type-check separately in CI.

🎯 One-Liner

“Use SWC/Babel for fast builds, but keep tsc --noEmit in CI for type safety.”


4. 🔄 Incremental Compilation

✅ Options

  • "incremental": true in tsconfig.json → saves .tsbuildinfo cache.
  • "composite": true for project references (multi-package repos).

✅ Benefits

  • Only re-check changed files.
  • Required for monorepos with shared libraries.

🎯 One-Liner

“Enable incremental + composite in tsconfig to avoid full re-checks in large repos.”


5. 🧩 Project References (Scaling Monorepos)

Definition:
Break project into multiple sub-projects with clear build boundaries.

✅ Example

// tsconfig.json
{
  "files": [],
  "references": [
    { "path": "./packages/ui" },
    { "path": "./packages/server" }
  ]
}
Enter fullscreen mode Exit fullscreen mode

✅ Benefits

  • Faster builds (independent packages).
  • Enforces dependency contracts.
  • Works great with Nx/Turborepo.

🎯 One-Liner

“Project references let you split large codebases into smaller typed units with enforced contracts.”


6. 🧠 Type-Only Imports & Exports (TS 3.8+)

✅ Example

import type { User } from "./types"; // stripped at runtime
export type { Config } from "./config";
Enter fullscreen mode Exit fullscreen mode

✅ Benefits

  • Avoids accidentally bundling type-only modules.
  • Reduces unnecessary runtime imports.

🎯 One-Liner

“Use import type and export type to ensure pure type imports don’t affect runtime bundles.”


7. ⚖️ Managing any at Scale

✅ Problems

  • any spreads like a virus in codebases.
  • One any can propagate through dozens of types.

✅ Solutions

  • Use unknown instead of any when possible.
  • Use eslint rules (@typescript-eslint/no-explicit-any).
  • Introduce “escape hatches” (TODO: fix any) but track debt.

🎯 One-Liner

“Manage any aggressively — prefer unknown, enforce lint rules, and track escape hatches.”


8. 📊 Large Codebase Best Practices

  • ✅ Strict mode always (strict: true).
  • ✅ Type-only imports (import type).
  • ✅ Use Zod/io-ts for runtime validation of API responses.
  • ✅ Add tsc --noEmit in CI to enforce type safety.
  • ✅ Use paths in tsconfig.json for clean imports.
  • ✅ Monitor tsc --diagnostics to spot type-check slowdowns.

🎯 One-Liner

“Large TS projects succeed when strict mode, type-only imports, runtime validation, and CI type-checks are enforced.”


🟦 Ecosystem & Future


1. 🧩 Decorators (Stage 3 Proposal)

Definition:
Annotations for classes, methods, and properties.

✅ Example

function readonly(target: any, key: string) {
  Object.defineProperty(target, key, { writable: false });
}

class User {
  @readonly
  name = "Alice";
}
Enter fullscreen mode Exit fullscreen mode

✅ Use Cases

  • Dependency injection (NestJS).
  • ORMs (TypeORM, Prisma).
  • Metadata reflection.

⚠️ Gotchas

  • Still experimental — syntax differs across versions.
  • Requires "experimentalDecorators": true in tsconfig.json.

🎯 One-Liner

“Decorators add metadata to classes/members — useful in frameworks like NestJS, but still experimental in TS.”


2. 📜 Type Annotations in JavaScript (TC39 Proposal)

Definition:
JavaScript itself may gain type syntax (stripped at runtime).

✅ Example (future JS)

function add(a: number, b: number): number {
  return a + b;
}
Enter fullscreen mode Exit fullscreen mode
  • Types would be ignored at runtime, like TS today.
  • TS would align with native JS type syntax.

🎯 One-Liner

“JS is moving toward built-in type annotations. TS will align, making gradual adoption easier.”


3. ⚖️ TypeScript vs Flow vs Others

✅ TypeScript

  • Mainstream, broad ecosystem.
  • Stronger tooling, VSCode integration.

✅ Flow (Meta)

  • Better type inference in theory.
  • Lost adoption due to ecosystem fragmentation.

✅ Elm / ReasonML / PureScript

  • Stronger type systems, but niche.

🎯 One-Liner

“TS won the ecosystem war — Flow and others have niche uses, but TS dominates frontend and Node.”


4. 🆕 New & Recent TS Features

satisfies Operator (TS 4.9)

const theme = {
  primary: "blue",
  secondary: "red"
} satisfies Record<string, string>;
Enter fullscreen mode Exit fullscreen mode
  • Ensures structure without widening values.

const Type Parameters (coming soon)

function tuple<const T extends string[]>(...args: T): T {
  return args;
}
const t = tuple("a", "b"); // type ["a", "b"]
Enter fullscreen mode Exit fullscreen mode

✅ Variance Annotations (future)

  • Explicitly mark generics as in (contravariant) or out (covariant).

🎯 One-Liner

“Features like satisfies and const generics improve precision without hacks — future TS is about better inference + clarity.”


5. 🏗️ Migration Strategies

✅ JS → TS

  • Enable allowJs + checkJs.
  • Rename .js.ts gradually.
  • Add strict config (noImplicitAny, strictNullChecks).
  • Replace JSDoc with real types.

✅ Flow → TS

  • Use codemods (flow-to-ts).
  • Incrementally replace types.

✅ Legacy TS → Modern

  • Remove namespace in favor of ES modules.
  • Switch to strict mode.
  • Replace /// <reference> with proper imports.

🎯 One-Liner

“Migrate incrementally: JS → TS with checkJs, Flow → TS with codemods, legacy TS → strict modules.”


6. 🏢 TypeScript at Scale

✅ Observations

  • At very large scale (10M+ LOC), TS type-checking can bottleneck.
  • Some companies (Google, Meta) experiment with faster type-checkers (SWC, Rome, incremental builds).
  • Types become API contracts between teams — not just safety.

🎯 One-Liner

“At scale, TypeScript types are contracts between teams. Performance requires project references + incremental builds.”


7. 🔮 Future of TypeScript

✅ Trends

  • Closer alignment with JavaScript (native type annotations).
  • Better inference (const generics, variance).
  • Compiler performance improvements (Rust-based checkers like tsc-swc).
  • More runtime type-checking integration (Zod + TS).

🎯 One-Liner

“The future of TS is tighter JS integration, better inference, and faster compilers — runtime validation will bridge static gaps.”


✅ Summary (Full Handbook)

This TypeScript handbook covers staff/architect-level depth:

  • Fundamentals (type system vs runtime, primitives, assertions, narrowing, null checks, structural typing, interfaces vs types, enums vs literal types)
  • Advanced Types (unions, intersections, literals, const assertions, template literals, conditional, mapped, utility types, recursive types)
  • Generics (functions, constraints, defaults, keyof/indexed access, conditional generics, variance, HKTs, React generics)
  • Type Inference & Compatibility (inference, widening/narrowing, assignability, any vs unknown vs never, enums, tuples vs arrays)
  • JavaScript Interop (ambient declarations, @types, module augmentation, declaration merging, as const, tsconfig strictness, runtime validation, gradual migration)
  • Type Guards & Narrowing (typeof, instanceof, in-operator, discriminated unions, custom predicates, assertion functions, exhaustiveness checks)
  • Advanced Patterns (branded types, opaque types, recursive utilities, mapped utilities, variadic tuples, builder pattern, exact types, phantom types)
  • TypeScript in React (props, children, hooks, context, refs, HOCs, generic components, event handlers, polymorphic components)
  • Performance & Scaling (diagnostics, avoiding complex types, Babel/SWC vs tsc, incremental compilation, project references, import type, managing any, best practices)
  • Ecosystem & Future (decorators, JS type annotations proposal, TS vs Flow, new features like satisfies/const generics, migration strategies, TS at scale, future trends)

Top comments (1)

Collapse
 
richmirks profile image
Richard Mirks

Fantastic handbook—concise, well-structured, and loaded with the exact “expert refresher” one-liners I'll tape to my monitor for interview panic moments. Loved the emphasis on compile-time vs runtime and the consistent “use unknown, not any” drumbeat; my duck-typed ducks salute you. The React notes (especially avoiding React.FC's stealth children) and the polymorphic as patterns are clear and practical, and the scaling advice around diagnostics, project references, and import type hits real-world pain points. Also appreciate the runtime validation reminders—Zod saves us from trusting lying JSON. Minor quibbles: the enums vs literal unions bit could nod more to const enum pitfalls under isolatedModules, the HKT workaround is a little hand-wavy (maybe point to fp-ts patterns directly), and variance deserves a tiny, concrete strictFunctionTypes example to scare people straight. A small decision-tree for “type vs interface” and a quick real API client example using satisfies + zod would elevate this from excellent to taped-above-every-desk. Oh, and a warning that exactOptionalPropertyTypes changes mental models would save a few head scratches. Overall, tight, accurate, and immediately useful—this will help me stop as any from multiplying like gremlins after midnight.