1. Overview & Questions (SQ3R · Survey & Question)

SQ3R Step 1: Skim the big picture, formulate key questions.

What Is TypeScript?

TypeScript is an open-source programming language developed and maintained by Microsoft. It is a strict superset of JavaScript, meaning any valid JavaScript code is also valid TypeScript code. TypeScript adds optional static typing and support for the latest ECMAScript features on top of JavaScript, and ultimately compiles back to plain JavaScript.

First released in 2012 and led by Anders Hejlsberg, the chief architect of C#, TypeScript was born from a clear need: as JavaScript applications grew in scale and complexity, the shortcomings of a dynamic type system became increasingly apparent. Large codebases lacked compile-time error checking, IDE support was limited, and refactoring was error-prone. TypeScript addresses these problems by introducing type annotations and static analysis during development, while preserving JavaScript's flexibility and ecosystem compatibility.

Key Questions

  • Where does TypeScript shine? Mid-to-large scale projects, team collaboration, long-lived codebases, and any scenario where code reliability matters.
  • What advantages does TypeScript have over alternatives? Compared to Flow or JSDoc type annotations, TypeScript offers a more powerful type system, a more active community, superior tooling support, and first-class integration with major frameworks (React, Vue, Angular).
  • What prerequisites are needed? Solid JavaScript knowledge (ES6+). Basic familiarity with object-oriented or functional programming helps but is not required.

Technology Landscape

TypeScript's architecture can be understood in three layers:

Foundation: The Type System — Includes primitive types (string, number, boolean, null, undefined, symbol, bigint), object types (interfaces, type aliases), union types, intersection types, literal types, and more. This is the foundation for all type expressions.

Middle: Type Manipulation — Generics make types parameterizable. Conditional Types enable if-else logic at the type level. Mapped Types batch-transform type properties. Template Literal Types manipulate strings at the type level. Combined, these tools enable complex compile-time type computations.

Top: Engineering Practices — Includes tsconfig.json configuration, declaration files (.d.ts), the module system, decorators, integration with build tools (Webpack, Vite, esbuild), and daily development best practices.

2. Explained Simply (Feynman Technique)

Feynman Technique core idea: If you can't explain something in simple language, you don't truly understand it.

Core Concepts Explained

Type Annotations

Type annotations tell TypeScript: "this variable is this kind of thing." Think of it like labeling books in a library — slap a "Science Fiction" label on a book, and everyone knows which shelf it belongs on.

let name: string = "TypeScript";
let version: number = 6.0;
let isOpenSource: boolean = true;

Interfaces

An interface is a "contract" or "blueprint" that defines what properties and methods an object should have. Any object that wants to fulfill this contract must provide everything the interface requires.

interface User {
  name: string;
  age: number;
  greet(): string;
}
 
const user: User = {
  name: "Alice",
  age: 30,
  greet() {
    return `Hello, I'm ${this.name}`;
  },
};

Union Types

A union type means a value can be any one of several types. Connected with the | symbol, it reads as "or."

let id: string | number;
 
id = "abc-123"; // OK
id = 42; // OK

Generics

Generics add "placeholders" to types. Think of them like blank fields in a form template — you fill in the specifics when you actually use it. This lets functions and classes handle multiple types while staying type-safe.

function identity<T>(value: T): T {
  return value;
}
 
const result1 = identity<string>("hello"); // type is string
const result2 = identity<number>(42); // type is number

Type Guards

A type guard is a runtime check that helps TypeScript "narrow" a variable's type within a specific code block. Like a security guard checking your ID — once verified, the system knows exactly who you are.

function processValue(value: string | number) {
  if (typeof value === "string") {
    // In this branch, TypeScript knows value is string
    console.log(value.toUpperCase());
  } else {
    // In this branch, TypeScript knows value is number
    console.log(value.toFixed(2));
  }
}

Type Inference

TypeScript is smart enough to figure out types automatically in many cases — you don't always have to write them out. Like walking into your usual coffee shop and saying "the usual" — the barista knows exactly what you mean.

// TypeScript infers x as number
let x = 10;
 
// Infers return type as string
function greet(name: string) {
  return `Hello, ${name}`;
}

Analogies & Metaphors

Think of TypeScript's type system as a package sorting center:

  • Primitive types (string, number, boolean) are like standardized package sizes — small, medium, large, each with fixed dimensions.
  • Interfaces and type aliases are like custom box templates — you define how many compartments and what goes in each.
  • Union types are like flexible slots that accept "small or medium" — either works, but it must be one of the two.
  • Generics are like adjustable shelving — the shelf size adapts to the type of goods being stored, ensuring everything fits.
  • Type guards are like barcode scanners — once scanned, the exact package type is confirmed and routed accordingly.
  • Conditional types are like automated sorting rules — "if fragile, apply a 'handle with care' label; otherwise, mark as 'standard'."

Common Misconceptions Clarified

  1. "TypeScript is a different language — I need to learn everything from scratch." Not true. TypeScript is a superset of JavaScript. Your JS knowledge transfers completely. You can adopt types incrementally — no need to rewrite everything at once.

  2. "TypeScript makes code bloated." Type annotations add very little code while delivering compile-time error catching and better IDE support. In large projects, the return on investment is substantial.

  3. "Using any is the same as not using TypeScript." While any bypasses type checking, you should generally prefer unknown or generics. any is an escape hatch, not a daily tool.

  4. "interface and type are exactly the same." They're interchangeable in most cases, but interface supports declaration merging while type supports inline union and intersection definitions. Choose based on your specific needs.

  5. "TypeScript is only for large projects." Even small projects benefit from catching typos, missing properties, and other mistakes. Many developers find that once they get used to TS, going back to plain JS feels unsafe.

3. Cone of Deepening Knowledge (Simon Learning Method)

Simon Learning Method: Focused effort, goal-oriented, cone-shaped deepening — start from the core and expand outward.

Layer 1: Core Fundamentals

3.1 Primitive Types

TypeScript's primitive types correspond directly to JavaScript's, plus special types like void, never, and unknown.

// Primitives
let str: string = "hello";
let num: number = 42;
let bool: boolean = true;
let n: null = null;
let u: undefined = undefined;
let sym: symbol = Symbol("id");
let big: bigint = 100n;
 
// Special types
let notSure: unknown = "maybe a string";
notSure = 42; // OK, unknown accepts any value
 
let nothing: void = undefined; // Common for functions with no return
let impossible: never; // Type that never has a value
 
// Arrays
let numbers: number[] = [1, 2, 3];
let strings: Array<string> = ["a", "b", "c"];
 
// Tuples — fixed-length, fixed-type arrays
let tuple: [string, number] = ["age", 25];

3.2 Interfaces

Interfaces describe the "shape" of an object — what properties it has and their types.

interface Person {
  name: string;
  age: number;
  email?: string; // Optional property
  readonly id: number; // Read-only property
}
 
const person: Person = {
  name: "Bob",
  age: 25,
  id: 1,
};
 
// person.id = 2; // Error: id is read-only
 
// Interfaces can be extended
interface Employee extends Person {
  department: string;
  salary: number;
}

3.3 Type Aliases

The type keyword gives a name to a type. It is more flexible than interface in certain ways.

// Basic usage
type ID = string | number;
type Point = { x: number; y: number };
 
// Unlike interface: type can represent unions inline
type Status = "active" | "inactive" | "suspended";
 
// Can also represent function types
type Callback = (data: string) => void;
 
// Intersection types
type Named = { name: string };
type Aged = { age: number };
type NamedAndAged = Named & Aged;
 
const user: NamedAndAged = { name: "Charlie", age: 28 };

3.4 Union & Intersection Types

// Union type: A or B
type Result = Success | Error;
 
interface Success {
  status: "success";
  data: string;
}
 
interface Error {
  status: "error";
  message: string;
}
 
function handleResult(result: Result) {
  if (result.status === "success") {
    console.log(result.data); // TypeScript knows this is Success
  } else {
    console.log(result.message); // TypeScript knows this is Error
  }
}
 
// Intersection type: A and B
type Admin = User & { permissions: string[] };
 
interface User {
  name: string;
  email: string;
}
 
const admin: Admin = {
  name: "Diana",
  email: "diana@example.com",
  permissions: ["read", "write", "delete"],
};

3.5 Function Types

// Parameter types and return type
function add(a: number, b: number): number {
  return a + b;
}
 
// Optional parameters and defaults
function greet(name: string, greeting?: string): string {
  return `${greeting || "Hello"}, ${name}`;
}
 
// Rest parameters
function sum(...numbers: number[]): number {
  return numbers.reduce((acc, n) => acc + n, 0);
}
 
// Function overloads
function formatDate(value: string): string;
function formatDate(value: number): string;
function formatDate(value: string | number): string {
  if (typeof value === "number") {
    return new Date(value).toLocaleDateString();
  }
  return new Date(value).toLocaleDateString();
}

3.6 Enums

// Numeric enum
enum Direction {
  Up = 0,
  Down = 1,
  Left = 2,
  Right = 3,
}
 
// String enum
enum HttpStatusCode {
  OK = "200",
  NotFound = "404",
  InternalError = "500",
}
 
let status: HttpStatusCode = HttpStatusCode.OK;

3.7 Literal Types

// String literal
type Theme = "light" | "dark" | "system";
let theme: Theme = "dark";
 
// Numeric literal
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
let roll: DiceRoll = 3;
 
// Boolean literal
type Truthy = true;

3.8 Type Assertions

// as syntax
const canvas = document.getElementById("main") as HTMLCanvasElement;
canvas.getContext("2d");
 
// Angle-bracket syntax (not usable in JSX files)
// const input = <HTMLInputElement>document.getElementById("input");
 
// Non-null assertion
const element = document.querySelector(".box")!;
element.classList.add("active");

Layer 2: Advanced Usage

3.9 Generics In Depth

Generics are one of the most powerful features in TypeScript's type system. They let you write reusable, type-safe code without being locked to a single concrete type.

// Generic function
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}
 
first([1, 2, 3]); // returns number | undefined
first(["a", "b"]); // returns string | undefined
 
// Generic constraints (extends)
interface HasLength {
  length: number;
}
 
function logLength<T extends HasLength>(value: T): T {
  console.log(value.length);
  return value;
}
 
logLength("hello"); // OK, string has length
logLength([1, 2, 3]); // OK, arrays have length
// logLength(123);      // Error, number has no length
 
// Multiple generic parameters
function merge<K extends string, V>(key: K, value: V): Record<K, V> {
  return { [key]: value } as Record<K, V>;
}
 
// Generic defaults
interface PaginatedResponse<T = unknown> {
  data: T[];
  total: number;
  page: number;
}
 
// Using the default type
const res: PaginatedResponse = { data: [], total: 0, page: 1 };

3.10 Mapped Types

Mapped types generate new types from existing ones by iterating over their properties.

// Basic mapped type
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};
 
type Optional<T> = {
  [P in keyof T]?: T[P];
};
 
interface Todo {
  title: string;
  description: string;
  completed: boolean;
}
 
type ReadonlyTodo = Readonly<Todo>;
// Equivalent to { readonly title: string; readonly description: string; readonly completed: boolean }
 
// Mapped type + key remapping (TypeScript 4.1+)
type Getters<T> = {
  [P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};
 
type TodoGetters = Getters<Todo>;
// { getTitle: () => string; getDescription: () => string; getCompleted: () => boolean }

3.11 Conditional Types

Conditional types let you perform if-else logic at the type level.

// Basic form: SomeType extends OtherType ? TrueType : FalseType
type IsString<T> = T extends string ? "yes" : "no";
 
type A = IsString<string>; // "yes"
type B = IsString<number>; // "no"
 
// The infer keyword: extracting types within conditional types
type Flatten<T> = T extends Array<infer Item> ? Item : T;
 
type F1 = Flatten<string[]>; // string
type F2 = Flatten<number>; // number
 
// Extract function return type
type GetReturnType<T> = T extends (...args: never[]) => infer R ? R : never;
 
type R1 = GetReturnType<() => string>; // string
type R2 = GetReturnType<(x: number) => boolean>; // boolean
 
// Distributive conditional types
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>; // string[] | number[]
 
// Avoiding distributive behavior: wrap with [T]
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Result2 = ToArrayNonDist<string | number>; // (string | number)[]

3.12 Type Inference In Depth

// typeof: get the type of a variable
const config = {
  port: 3000,
  host: "localhost",
  debug: true,
};
 
type Config = typeof config;
// { port: number; host: string; debug: boolean }
 
// keyof: get a union of all property names of a type
type ConfigKeys = keyof Config; // "port" | "host" | "debug"
 
// Indexed access types: Type['key']
type PortType = Config["port"]; // number
 
// ReturnType: extract the return type of a function
function createUser(name: string) {
  return { name, createdAt: new Date() };
}
 
type User = ReturnType<typeof createUser>;
// { name: string; createdAt: Date }
 
// Parameters: get parameter types as a tuple
type CreateUserParams = Parameters<typeof createUser>;
// [name: string]

3.13 Type Guards & Narrowing

// typeof guard
function double(value: string | number): string | number {
  if (typeof value === "string") {
    return value.repeat(2); // value: string
  }
  return value * 2; // value: number
}
 
// instanceof guard
function formatDate(value: string | Date): string {
  if (value instanceof Date) {
    return value.toISOString(); // value: Date
  }
  return new Date(value).toISOString(); // value: string
}
 
// in operator guard
interface Fish {
  swim(): void;
}
interface Bird {
  fly(): void;
}
 
function move(animal: Fish | Bird) {
  if ("swim" in animal) {
    animal.swim(); // animal: Fish
  } else {
    animal.fly(); // animal: Bird
  }
}
 
// Custom type guards (type predicates)
function isString(value: unknown): value is string {
  return typeof value === "string";
}
 
function process(input: unknown) {
  if (isString(input)) {
    console.log(input.toUpperCase()); // input: string
  }
}
 
// Discriminated unions
type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; x: number }
  | { kind: "triangle"; x: number; y: number };
 
function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.x ** 2;
    case "triangle":
      return (shape.x * shape.y) / 2;
  }
}

3.14 Declaration Files

Declaration files (.d.ts) provide type information for existing JavaScript libraries, allowing TypeScript to understand them.

// Writing a declaration file: example.d.ts
declare module "my-lib" {
  interface MyLibOptions {
    timeout?: number;
    retries?: number;
  }
 
  function init(options: MyLibOptions): void;
  function getData(id: string): Promise<string>;
 
  export { init, getData, MyLibOptions };
}
 
// Usage
import { init, getData } from "my-lib";
 
init({ timeout: 5000 });
const data = await getData("abc");

3.15 Modules & Namespaces

// ES Modules (recommended)
// math.ts
export function add(a: number, b: number): number {
  return a + b;
}
export const PI = 3.14159;
 
// app.ts
import { add, PI } from "./math";
 
// Namespaces (legacy approach, not recommended for new projects)
namespace Validation {
  export interface StringValidator {
    isValid(s: string): boolean;
  }
 
  export class EmailValidator implements StringValidator {
    isValid(s: string): boolean {
      return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s);
    }
  }
}
 
const validator = new Validation.EmailValidator();

3.16 Decorators

TypeScript 5.0 introduced decorators compliant with the ECMAScript standard.

// Method decorator
function logged(originalMethod: any, context: ClassMethodDecoratorContext) {
  const methodName = String(context.name);
  function replacementMethod(this: any, ...args: any[]) {
    console.log(`LOG: Entering method '${methodName}'.`);
    const result = originalMethod.call(this, ...args);
    console.log(`LOG: Exiting method '${methodName}'.`);
    return result;
  }
  return replacementMethod;
}
 
class Calculator {
  @logged
  add(a: number, b: number): number {
    return a + b;
  }
}

Layer 3: Deep Dives

3.17 Advanced Type Gymnastics

Combining all the tools you've learned enables extremely powerful type-level programming.

// Deep Partial
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
 
// Deep Readonly
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};
 
// Unwrap nested Promises
type UnwrapPromise<T> = T extends Promise<infer U> ? UnwrapPromise<U> : T;
 
type Result = UnwrapPromise<Promise<Promise<string>>>; // string
 
// Convert snake_case keys to camelCase
type SnakeToCamel<S extends string> = S extends `${infer Head}_${infer Tail}`
  ? `${Head}${Capitalize<SnakeToCamel<Tail>>}`
  : S;
 
type CamelCase = SnakeToCamel<"user_name_id">; // "userNameId"
 
// Recursive type: path-based value access
type PathValue<T, P extends string> = P extends `${infer Key}.${infer Rest}`
  ? Key extends keyof T
    ? PathValue<T[Key], Rest>
    : never
  : P extends keyof T
    ? T[P]
    : never;
 
interface Data {
  user: {
    profile: {
      name: string;
    };
  };
}
 
type Name = PathValue<Data, "user.profile.name">; // string

3.18 tsconfig.json Best Practices

{
  "compilerOptions": {
    // Language & Environment
    "target": "ES2022", // Compilation target
    "lib": ["ES2022", "DOM"], // Type definition libraries
    "module": "ESNext", // Module system
    "moduleResolution": "bundler", // Module resolution strategy
 
    // Strict mode (recommended: enable all)
    "strict": true, // Enable all strict checks
    "noUncheckedIndexedAccess": true, // Array/object index returns T | undefined
    "noImplicitOverride": true, // Override methods must use override keyword
    "exactOptionalPropertyTypes": true, // Exact optional property types
 
    // Output control
    "declaration": true, // Generate .d.ts files
    "declarationMap": true, // Generate declaration maps
    "sourceMap": true, // Generate source maps
 
    // Interoperability
    "esModuleInterop": true, // Allow default imports from CommonJS
    "allowSyntheticDefaultImports": true,
    "resolveJsonModule": true, // Allow importing JSON
    "isolatedModules": true, // Ensure each file can be compiled independently
 
    // Other
    "skipLibCheck": true, // Skip type checking of .d.ts files
    "forceConsistentCasingInFileNames": true,
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"],
}

3.19 Type System Design Philosophy

TypeScript's type system follows several core design principles:

Structural Typing: TypeScript uses "duck typing" — if two types have compatible structures, they are compatible, regardless of whether an explicit inheritance relationship exists.

interface Point1 {
  x: number;
  y: number;
}
interface Point2 {
  x: number;
  y: number;
}
 
const p1: Point1 = { x: 1, y: 2 };
const p2: Point2 = p1; // OK, structurally compatible

Type Narrowing: TypeScript's control flow analysis automatically narrows variable types based on typeof, instanceof, in operators, truthiness checks, assignment statements, and more.

Gradual Adoption: You can incrementally migrate from .js to .ts files using allowJs and checkJs options. While any is not recommended for general use, it provides an escape hatch when needed.

3.20 Performance Optimization

// Avoid excessively deep type recursion (TypeScript has recursion depth limits)
// Bad practice
type BadDeepPartial<T> = {
  [K in keyof T]?: T[K] extends object ? BadDeepPartial<T[K]> : T[K];
};
 
// For very deep nesting, consider concrete depth levels
type Partial3<T> = Partial<{
  [K in keyof T]: T[K] extends object
    ? Partial<{
        [K2 in keyof T[K]]: T[K][K2] extends object ? Partial<T[K][K2]> : T[K][K2];
      }>
    : T[K];
}>;
 
// Use interface over intersection types for better performance
// Intersection types create intermediate types; interfaces are declarative
// Bad practice
type A = { a: string } & { b: number } & { c: boolean };
// Good practice
interface B {
  a: string;
  b: number;
  c: boolean;
}
 
// Project References speed up compilation in large projects
// tsconfig.json
// {
//   "references": [
//     { "path": "./packages/core" },
//     { "path": "./packages/utils" }
//   ]
// }

4. Key Notes (Cornell Note-taking Method)

Cornell Method: Divide notes into cue column, notes column, and summary column for efficient review and retrieval.

Key Concept Quick Reference

Cue / KeywordDetailed Notes
Primitive Typesstring, number, boolean, null, undefined, symbol, bigint; special types void, never, unknown
InterfaceDefines an object's shape. Supports optional properties (?), readonly (readonly), extension (extends), declaration merging
Type AliasNames a type. Supports inline union, intersection, and literal types. Cannot be merged via declaration
Union TypeA | B — value can be either A or B
Intersection TypeA & B — value must satisfy both A and B
Generics<T> type parameterization. Supports constraints (extends), defaults, multiple parameters
Type Guardstypeof, instanceof, in, custom guards (value is Type). Used for narrowing types
Conditional TypesT extends U ? X : Y. Use with infer to extract types. Supports distributive behavior
Mapped Types[P in keyof T]: T[P]. Iterate over properties to generate new types. Supports key remapping
Template Literal Types`prefix${Type}`. String manipulation at the type level, combined with Uppercase/Lowercase/Capitalize/Uncapitalize
NarrowingControl flow analysis auto-narrows types based on typeof, instanceof, in, switch, truthiness, etc.
Declaration Files.d.ts files provide type info for JS libraries. Support declare module, declare global, etc.

Core API / Utility Types Quick Reference

Type / UtilityPurposeExample
Partial<T>Make all properties optionalPartial<{ a: string; b: number }> -> { a?: string; b?: number }
Required<T>Make all properties requiredRequired<{ a?: string }> -> { a: string }
Readonly<T>Make all properties read-onlyReadonly<{ a: string }> -> { readonly a: string }
Record<K, V>Construct an object type with keys K and values VRecord<"a" | "b", number> -> { a: number; b: number }
Pick<T, K>Select a subset of properties from TPick<{ a: string; b: number }, "a"> -> { a: string }
Omit<T, K>Exclude a subset of properties from TOmit<{ a: string; b: number }, "b"> -> { a: string }
Exclude<U, M>Exclude members from a union typeExclude<"a" | "b" | "c", "a"> -> "b" | "c"
Extract<U, M>Extract members from a union typeExtract<"a" | "b" | "c", "a" | "f"> -> "a"
NonNullable<T>Exclude null and undefinedNonNullable<string | null | undefined> -> string
ReturnType<F>Get the return type of a functionReturnType<() => string> -> string
Parameters<F>Get function parameter types as a tupleParameters<(a: string, b: number) => void> -> [a: string, b: number]
Awaited<T>Recursively unwrap PromisesAwaited<Promise<Promise<string>>> -> string
NoInfer<T>Block type inferenceNoInfer<C> prevents C from being inferred to other values
keyof TGet a union of all property names of a typekeyof { a: string; b: number } -> "a" | "b"
typeof xGet the type of a variabletypeof config -> { port: number; host: string }
T[K]Indexed access type{ a: string; b: number }["a"] -> string

Section Summary

TypeScript is a superset of JavaScript that catches errors at compile time through a static type system, significantly improving code reliability and developer experience. Its foundation is a structural type system — type compatibility is based on shape, not nominal declaration. Generics are the most powerful tool in the type system, enabling code reuse while maintaining type safety. Conditional types and mapped types provide type-level programming capabilities, and combined with operators like infer, keyof, and typeof, they enable highly flexible type transformations. TypeScript's design philosophy embraces gradual adoption — you can introduce types incrementally from a pure JavaScript project without needing to rewrite everything at once.

5. Review & Practice (SQ3R · Recite & Review)

SQ3R final steps: Recite key points and solidify understanding through practice.

Core Points Review

  1. TypeScript is a superset of JavaScript that adds optional static typing and compile-time type checking.
  2. Primitive types include string, number, boolean, null, undefined, symbol, bigint, plus special types void, never, and unknown.
  3. Both interface and type alias describe object shapes. Interface supports declaration merging; type is more flexible for inline compositions.
  4. Union types (|) express an "or" relationship; intersection types (&) express an "and" relationship.
  5. Generics (<T>) parameterize functions and types, with extends adding constraints.
  6. Type guards (typeof, instanceof, in, custom type predicates) help TypeScript narrow types at runtime.
  7. Conditional types (T extends U ? X : Y) perform if-else logic at the type level, with infer for type extraction.
  8. Mapped types iterate over existing type properties to generate new types, with key remapping support (as).
  9. Utility types (Partial, Required, Readonly, Pick, Omit, Record, etc.) are high-frequency daily development tools.
  10. TypeScript uses structural typing (duck typing) — type compatibility is based on shape, not nominal declaration.

Hands-On Exercises

  1. Beginner: Define a complete type system for a TODO application. Include a Todo interface (id, title, description, completed, createdAt) and type signatures for addTodo, toggleTodo, and filterTodos functions.

  2. Intermediate: Implement a type-safe EventEmitter. Use generics and mapped types to ensure on, emit, and off methods have full type inference. Event names and callback parameter types must be correlated.

  3. Advanced: Implement a type-safe API route definition. Given a route configuration object, use template literal types and conditional types to automatically derive request parameter types and response types for each route.

  4. Real-World Project: Migrate an existing JavaScript project to TypeScript. Start with allowJs: true, gradually add types to critical modules, write .d.ts declaration files, and eventually enable strict: true.

Common Pitfalls

  • Overusing any: any disables type checking. Prefer unknown or use generics to express more precise constraints.
  • Abusing type assertions: as tells the compiler "trust me" without any runtime check. Prefer type guards for safe narrowing.
  • Forgetting to handle undefined: With noUncheckedIndexedAccess enabled, array indexing and object key access may return undefined. Always handle this explicitly.
  • Circular type dependencies: Two types referencing each other can cause circular dependency errors. Use import type and lazy references to resolve this.
  • Type/runtime mismatch: TypeScript types are erased at compile time. Never rely on type information for runtime decisions — use type guards or schema validation libraries (like Zod) instead.

Further Reading