Skip to content

TypeScript Lesson Plan

A progressive curriculum to master TypeScript through hands-on practice.

Goal: Set up TypeScript and understand basic types.

TypeScript adds static types to JavaScript. Types catch errors at compile time, not runtime.

  1. Setup and run

    Terminal window
    npm init -y
    npm install -D typescript
    npx tsc --init
    hello.ts
    const greeting: string = "Hello, TypeScript!";
    console.log(greeting);
    Terminal window
    npx tsc hello.ts
    node hello.js
  2. Basic types

    // Primitives
    let name: string = "Alice";
    let age: number = 30;
    let active: boolean = true;
    // Arrays
    let nums: number[] = [1, 2, 3];
    let names: Array<string> = ["a", "b"];
    // Tuple
    let point: [number, number] = [10, 20];
    // Any (escape hatch)
    let anything: any = "could be anything";
  3. Type inference

    // TypeScript infers types
    let x = 10; // number
    let s = "hello"; // string
    // Function return type inferred
    function add(a: number, b: number) {
    return a + b; // Returns number
    }
  4. Type annotations

    // Variables
    let count: number;
    count = 42;
    // Functions
    function greet(name: string): string {
    return `Hello, ${name}!`;
    }
    // Arrow functions
    const double = (n: number): number => n * 2;

Write a function that takes a name and age and returns a formatted greeting.


Goal: Define object shapes with interfaces.

  1. Object types

    // Inline object type
    let user: { name: string; age: number } = {
    name: "Alice",
    age: 30,
    };
    // Optional properties
    let config: { host: string; port?: number } = {
    host: "localhost",
    };
  2. Interfaces

    interface User {
    name: string;
    age: number;
    email?: string; // Optional
    readonly id: number; // Cannot modify
    }
    const user: User = {
    id: 1,
    name: "Alice",
    age: 30,
    };
  3. Extending interfaces

    interface Animal {
    name: string;
    }
    interface Dog extends Animal {
    breed: string;
    }
    const dog: Dog = {
    name: "Rex",
    breed: "Labrador",
    };
  4. Index signatures

    interface Dictionary {
    [key: string]: string;
    }
    const dict: Dictionary = {
    hello: "world",
    foo: "bar",
    };

Define an interface for a blog post with title, content, author, and optional tags.


Goal: Create flexible yet precise types.

  1. Union types

    let id: string | number;
    id = "abc";
    id = 123;
    function printId(id: string | number) {
    if (typeof id === "string") {
    console.log(id.toUpperCase());
    } else {
    console.log(id);
    }
    }
  2. Literal types

    type Direction = "north" | "south" | "east" | "west";
    function move(dir: Direction) {
    console.log(`Moving ${dir}`);
    }
    move("north"); // OK
    // move("up"); // Error!
  3. Discriminated unions

    interface Circle {
    kind: "circle";
    radius: number;
    }
    interface Square {
    kind: "square";
    size: number;
    }
    type Shape = Circle | Square;
    function area(shape: Shape): number {
    switch (shape.kind) {
    case "circle":
    return Math.PI * shape.radius ** 2;
    case "square":
    return shape.size ** 2;
    }
    }
  4. Narrowing

    function process(value: string | string[] | null) {
    if (value === null) {
    return "nothing";
    }
    if (Array.isArray(value)) {
    return value.join(", ");
    }
    return value.toUpperCase();
    }

Create a type for API responses that can be success (with data) or error (with message).


Goal: Type functions precisely.

  1. Function types

    // Function type
    type MathOp = (a: number, b: number) => number;
    const add: MathOp = (a, b) => a + b;
    const multiply: MathOp = (a, b) => a * b;
  2. Optional and default parameters

    function greet(name: string, greeting: string = "Hello"): string {
    return `${greeting}, ${name}!`;
    }
    function log(message: string, userId?: number): void {
    console.log(message, userId ?? "anonymous");
    }
  3. Rest parameters

    function sum(...nums: number[]): number {
    return nums.reduce((a, b) => a + b, 0);
    }
    sum(1, 2, 3, 4); // 10
  4. Overloads

    function parse(input: string): string[];
    function parse(input: string[]): string;
    function parse(input: string | string[]): string | string[] {
    if (typeof input === "string") {
    return input.split(",");
    }
    return input.join(",");
    }

Create a typed event handler function that accepts different event types.


Goal: Write reusable, type-safe code.

  1. Generic functions

    function identity<T>(value: T): T {
    return value;
    }
    identity<string>("hello"); // Explicit
    identity(42); // Inferred as number
  2. Generic interfaces

    interface Box<T> {
    value: T;
    }
    const stringBox: Box<string> = { value: "hello" };
    const numberBox: Box<number> = { value: 42 };
    interface Result<T, E> {
    data?: T;
    error?: E;
    }
  3. Generic constraints

    interface HasLength {
    length: number;
    }
    function logLength<T extends HasLength>(item: T): void {
    console.log(item.length);
    }
    logLength("hello"); // OK
    logLength([1, 2, 3]); // OK
    // logLength(42); // Error: number has no length
  4. Generic with keyof

    function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
    }
    const user = { name: "Alice", age: 30 };
    getProperty(user, "name"); // string
    getProperty(user, "age"); // number
    // getProperty(user, "email"); // Error!

Create a generic Stack<T> class with push, pop, and peek methods.


Goal: Transform types with built-in utilities.

  1. Partial and Required

    interface User {
    name: string;
    email: string;
    age?: number;
    }
    // All properties optional
    type PartialUser = Partial<User>;
    // All properties required
    type RequiredUser = Required<User>;
    // Good for updates
    function updateUser(id: number, updates: Partial<User>) {}
  2. Pick and Omit

    interface User {
    id: number;
    name: string;
    email: string;
    password: string;
    }
    // Only specific properties
    type PublicUser = Pick<User, "id" | "name" | "email">;
    // All except specific properties
    type SafeUser = Omit<User, "password">;
  3. Record

    type Status = "pending" | "active" | "completed";
    // Map status to counts
    type StatusCounts = Record<Status, number>;
    const counts: StatusCounts = {
    pending: 5,
    active: 10,
    completed: 25,
    };
  4. Readonly and NonNullable

    interface Config {
    host: string;
    port: number;
    }
    const config: Readonly<Config> = {
    host: "localhost",
    port: 3000,
    };
    // config.port = 8080; // Error!
    type MaybeString = string | null | undefined;
    type DefiniteString = NonNullable<MaybeString>; // string

Use utility types to create a read-only version of an API response type.


Goal: Master complex type patterns.

  1. Mapped types

    type Flags<T> = {
    [K in keyof T]: boolean;
    };
    interface Features {
    darkMode: string;
    notifications: string;
    }
    type FeatureFlags = Flags<Features>;
    // { darkMode: boolean; notifications: boolean }
  2. Conditional types

    type IsString<T> = T extends string ? true : false;
    type A = IsString<"hello">; // true
    type B = IsString<42>; // false
    // Extract array element type
    type Unwrap<T> = T extends Array<infer U> ? U : T;
    type C = Unwrap<string[]>; // string
  3. Template literal types

    type EventName = "click" | "focus" | "blur";
    type Handler = `on${Capitalize<EventName>}`;
    // "onClick" | "onFocus" | "onBlur"
    type Greeting = `Hello, ${string}!`;
    const g: Greeting = "Hello, World!";
  4. Type guards

    interface Cat {
    meow(): void;
    }
    interface Dog {
    bark(): void;
    }
    function isCat(animal: Cat | Dog): animal is Cat {
    return (animal as Cat).meow !== undefined;
    }
    function speak(animal: Cat | Dog) {
    if (isCat(animal)) {
    animal.meow(); // TypeScript knows it's Cat
    } else {
    animal.bark(); // TypeScript knows it's Dog
    }
    }

Create a type that makes all properties of a nested object optional (DeepPartial).


Goal: Organize code and use external libraries.

  1. ES modules

    math.ts
    export function add(a: number, b: number): number {
    return a + b;
    }
    export const PI = 3.14159;
    export default class Calculator {}
    // main.ts
    import Calculator, { add, PI } from "./math";
    import * as math from "./math";
  2. Type-only imports

    import type { User } from "./types";
    // Only import the type, not the value
    // Removed at compile time
  3. Declaration files

    types.d.ts
    declare module "untyped-lib" {
    export function doSomething(input: string): string;
    export const version: string;
    }
    // Ambient declarations
    declare global {
    interface Window {
    myApp: {
    version: string;
    };
    }
    }
  4. tsconfig essentials

    {
    "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "outDir": "./dist",
    "rootDir": "./src"
    },
    "include": ["src/**/*"],
    "exclude": ["node_modules"]
    }

Create a module with types, export them, and import in another file.


Build an API client:

  • Generic request/response types
  • Error handling with discriminated unions
  • Type-safe query parameters

Build a simple store:

  • Generic state type
  • Type-safe actions
  • Selector functions with proper return types

Build a command-line tool:

  • Use Commander.js with types
  • Validate input with Zod
  • Type-safe configuration

StageTopics
BeginnerBasic types, interfaces, functions
IntermediateGenerics, union types, utility types
AdvancedMapped types, conditional types, declaration