typescript Curriculum
Explore our comprehensive collection of typescript exercises. From basics to advanced patterns.
Primitives
TypeScript builds on JavaScript by adding **types**, so we can describe exactly what kind of values a variable can hold. The three most common primitive types are: - **`string`** – text values like `"Hello"` or `'TypeScript'` - **`number`** – all numbers, including decimals - **`boolean`** – logical values `true` or `false` You can assign these types explicitly with `:type` annotations, or you can let TypeScript **infer** the type automatically when you give a variable a value. Inference makes code shorter and easier to read, while annotations help catch mistakes in more complex situations. In this category, you’ll: - See the difference between explicit annotations and inferred types - Work with strings, numbers, and booleans - Write small functions that use these primitive types - Learn how TypeScript can catch mistakes before you run your code These exercises are designed to be a gentle introduction: short, simple problems that give you a quick win while showing the core idea of TypeScript’s type system.
Functions Basics
Functions are how we structure behavior. In TypeScript, you can describe parameter types and return types so that invalid calls are caught early: ```typescript function greet(name: string): string { return `Hello, ${name}!`; } ``` You can also let the compiler **infer** types from values and contexts, making callbacks and arrow functions pleasant to write. **In this category, you'll:** - Annotate parameters and return types explicitly - Use optional (`name?: string`) and default (`width = 100`) parameters - Write arrow functions with type inference - See how TypeScript catches type errors before runtime By the end, you'll be comfortable reading and writing function signatures, choosing when to be explicit, and relying on inference where it helps clarity.
Object Types Basics
Objects model real-world entities. In TypeScript, you describe an object's shape by listing its properties and their types: ```typescript const user: { id: number; name: string } = { id: 1, name: "Alice" }; ``` Shapes are **structural**—if a value has at least those properties with the right types, it matches the shape. **Key concepts:** - **Optional properties** – Mark with `?` like `{ first: string; last?: string }` - **Readonly properties** – Prevent changes with `readonly id: number` - **Excess property checks** – Catch typos when passing object literals **In this category, you'll:** - Define object shapes for entities like users, products, and points - Handle optional data safely (checking for `undefined`) - Return new objects without mutating inputs - Understand structural typing and common pitfalls By the end, you'll be comfortable modeling real-world data with TypeScript's object types.
Arrays
Arrays are ordered lists of values written as `T[]` or `Array<T>`: ```typescript const numbers: number[] = [1, 2, 3]; const users: User[] = [{ id: 1, name: "Alice" }]; ``` Now that you can model a single object, it's natural to work with arrays of objects like `User[]` or `Product[]`. **In this category, you'll:** - Work with arrays of primitives (summing and filtering numbers) - Transform arrays of objects (plucking fields, filtering by property) - Use methods like `.map()`, `.filter()`, and `.find()` with proper types - Return precise types like `T | undefined` from `.find()` - Make immutable updates without mutating inputs By the end, you'll be comfortable writing small, typed helpers that transform arrays safely and predictably.
Union & Litteral Types
Union types let a value be one of several possibilities, while literal types restrict values to exact constants: ```typescript type Status = 'idle' | 'loading' | 'success' | 'error'; type Id = string | number; let status: Status = 'loading'; // ✅ OK let status: Status = 'pending'; // ❌ Error ``` TypeScript performs **control-flow narrowing**: inside branches guarded by `typeof`, equality checks, or `switch`, the compiler refines a union to a specific member, giving you type safety and better autocompletion. **In this category, you'll:** - Create unions of primitives (`string | number`) - Use literal types for statuses, directions, and modes - Narrow unions with `typeof`, equality checks, and `switch` - Work with discriminated unions (tagged with fields like `kind` or `type`) This category focuses on practical everyday patterns: clear narrowing and small, well-defined state sets.
Type Aliases
Type aliases give a name to any type so you can reuse it throughout your code: ```typescript type UserId = string | number; type Status = 'idle' | 'loading' | 'success' | 'error'; type Point = { x: number; y: number }; ``` This improves readability, reduces duplication, and keeps APIs consistent. **You can create aliases for:** - **Unions** – `string | number` or `'red' | 'green' | 'blue'` - **Object shapes** – `{ id: number; name: string }` - **Tuples** – `[number, number]` for fixed-length arrays - **Function signatures** – `(x: number) => number` **In this category, you'll:** - Create type aliases for common patterns in your code - Use literal unions for small state sets (status, direction, etc.) - Apply aliases across multiple functions to reduce duplication - Work with optional and readonly properties in aliases Keep implementations simple, avoid mutation unless required, and use narrowing to work safely with unions.
Interfaces
Interfaces describe the shape of an object: ```typescript interface User { id: number; name: string; email?: string; // optional readonly created: Date; // can't be changed } ``` You've already used type aliases. Both `type` and `interface` can describe object shapes, and TypeScript cares about **structural compatibility**: if two shapes match, values can be used interchangeably. **Interfaces offer special features:** - **Extension** – `interface Admin extends User { role: string }` - **Declaration merging** – Multiple interface declarations combine - **Index signatures** – `[key: string]: any` for flexible keys **In this category, you'll:** - Define clear object shapes with optional and readonly properties - Extend interfaces to build on existing types - Use index signatures for dictionary-like objects - Merge interface declarations (useful for augmenting libraries) - See how interfaces interoperate with type aliases By the end, you'll know when to use interfaces versus type aliases and how to leverage their unique features.
null & undefined
When **strict null checks** are on, `null` and `undefined` must be handled explicitly. This helps you avoid crashes from missing values: ```typescript function greet(name: string | null) { if (name === null) return "Hello, guest!"; return `Hello, ${name}!`; } ``` **Key operators for handling maybe-values:** - **Nullish coalescing** – `value ?? defaultValue` (fallback for `null`/`undefined`) - **Optional chaining** – `user?.address?.city` (safe nested property access) ```typescript const displayName = user.name ?? "Anonymous"; const city = user?.address?.city; ``` **In this category, you'll:** - Handle `null` and `undefined` with explicit checks - Use `??` for clean default values - Use `?.` to safely access nested properties - Distinguish between nullish values and falsy values (`0`, `""`, `false`) **Best practice:** Prefer small, clear checks. Don't rely on general truthy/falsy rules when you specifically want to treat `null` or `undefined` as missing.
Type Assertions
Type assertions tell TypeScript how to treat a value. They **do not change runtime behavior**—they only affect types at compile time: ```typescript const input = document.getElementById('input') as HTMLInputElement; const data = JSON.parse(text) as User; ``` **Important:** Prefer real runtime checks first! If you can test with `typeof`, `Array.isArray`, or property checks, do that. Assertions are for when the type system can't see what you know. **Special assertion: `as const`** ```typescript const colors = ['red', 'green', 'blue'] as const; // Type: readonly ['red', 'green', 'blue'] // Not: string[] ``` This freezes literal types, keeping exact strings and numbers instead of widening them to `string` or `number`. Perfect for small enums and tuples. **In this category, you'll:** - Use `value as Type` when you have more information than the compiler - Apply `as const` to preserve literal types - Understand when assertions are safe vs. risky - See practical scenarios where assertions solve real problems Use assertions sparingly and deliberately—they're an escape hatch when the type system needs your help.
any (vs unknown)
`any` turns off type checking completely. It spreads quickly through your code and hides bugs. **Prefer `unknown`** when you don't know a type yet: ```typescript // ❌ Dangerous - no checks function process(data: any) { return data.value.toUpperCase(); // Might crash! } // ✅ Safe - forces checks function process(data: unknown) { if (typeof data === "object" && data !== null && "value" in data) { const obj = data as { value: string }; return obj.value.toUpperCase(); } } ``` `unknown` forces you to check a value before using it, making your code safer. **Narrowing techniques:** - `typeof` for primitives – `typeof x === 'string'` - `Array.isArray()` for arrays - `in` operator for properties – `'name' in obj` - Custom type guards – `function isUser(x: unknown): x is User` **In this category, you'll:** - Use `unknown` for inputs you need to validate - Narrow safely with runtime checks - Parse JSON and validate structures - Avoid leaking `any` into your code - Migrate gradually from `any` to `unknown` By the end, you'll understand why `unknown` is safer than `any` and how to work with it confidently.
Type Guards & Narrowing
Type guards help the compiler figure out which shape a value has at a given point. When a check succeeds, TypeScript **narrows** the type so you can access the right properties safely: ```typescript function process(value: string | number) { if (typeof value === 'string') { return value.toUpperCase(); // ✅ TypeScript knows it's a string } return value.toFixed(2); // ✅ TypeScript knows it's a number } ``` **Built-in type guards:** - **`typeof`** – for primitives (`'string'`, `'number'`, `'boolean'`) - **`instanceof`** – for classes (`value instanceof Date`) - **`in`** – for property checks (`'name' in obj`) - **`Array.isArray()`** – for arrays **Custom type guards:** ```typescript function isUser(value: unknown): value is User { return typeof value === 'object' && value !== null && 'id' in value; } ``` **In this category, you'll:** - Narrow unions with built-in type guards - Write custom type predicates with `value is Type` - Use exhaustive `switch` statements with discriminated unions - Leverage the `never` type to catch unhandled cases By the end, you'll be able to narrow any union type safely and teach TypeScript about your own runtime checks.
Functions: Call Signatures & Overloads
Functions are the heart of TypeScript programs. You can describe a function's shape with a **call signature**: ```typescript type MathOperation = (a: number, b: number) => number; const add: MathOperation = (a, b) => a + b; ``` **Function overloads** let one function name support different input shapes with different return types: ```typescript function parse(input: string): string[]; function parse(input: number): number[]; function parse(input: string | number) { // Implementation handles both cases } ``` You write multiple signatures (overloads), then one implementation that handles all cases. The compiler picks the right signature based on what you pass in. **In this category, you'll:** - Write function type expressions (`type Fn = (x: number) => string`) - Use optional parameters (`name?: string`) and rest parameters (`...args: number[]`) - Create overloaded functions that return the correct type based on inputs - Understand when to use overloads vs. union types **Best practice:** Keep each branch simple and predictable. The focus is on readable types with small, safe implementations.
Object Types (Deep Dive)
This deep dive builds on basic object types with advanced patterns: **Intersections** – Combine multiple types into one with all properties: ```typescript type Named = { name: string }; type Aged = { age: number }; type Person = Named & Aged; // Has both name and age ``` **Object Unions** – Model values that could be one of several shapes: ```typescript type Shape = | { kind: 'circle'; radius: number } | { kind: 'square'; size: number }; ``` Use the `in` operator to narrow unions by checking for specific properties. **Index Signatures** – Create dictionary-like objects: ```typescript type Dictionary = { [key: string]: number }; ``` **Excess Property Checks** – TypeScript catches typos in fresh object literals: ```typescript const user: User = { name: "Alice", agee: 30 }; // ❌ Error: 'agee' ``` **In this category, you'll:** - Compose shapes with intersections (`A & B`) - Model alternatives with object unions - Narrow unions with property checks (`in` operator) - Use index signatures for flexible dictionaries - Understand why excess property checks prevent bugs By the end, you'll be comfortable with advanced object type patterns and their tradeoffs.
Generics Fundamentals
Generics let you write components that work over many types without losing type safety: ```typescript function identity<T>(value: T): T { return value; } const num = identity(42); // T is number const str = identity("hello"); // T is string ``` A type parameter like `T` is a **placeholder** for a concrete type that callers supply or TypeScript infers. **Key generic patterns:** **Constraints** – Require certain properties: ```typescript function getLength<T extends { length: number }>(value: T) { return value.length; // Safe! We know T has length } ``` **Related type parameters** – Link keys to objects: ```typescript function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; // Return type matches the property type } ``` **Generic types and classes:** ```typescript type Box<T> = { value: T }; class Stack<T> { /* ... */ } ``` **In this category, you'll:** - Write generic functions that preserve type relationships - Add constraints with `extends` to require specific properties - Use `keyof` and indexed access for safe property lookups - Create generic types and classes - Understand when and why to use generics **Note:** Type arguments are erased at runtime, so implementations rely on normal JavaScript while types keep the API safe.
Creating Types from Types (Utility Patterns)
Utility types help you reshape and remix existing types without rewriting them: ```typescript type User = { id: number; name: string; email: string }; type UserPreview = Pick<User, 'id' | 'name'>; // { id, name } type PartialUser = Partial<User>; // All properties optional type ReadonlyUser = Readonly<User>; // Can't modify properties ``` They're **building blocks** for safer refactors and clearer intent. **Essential utility types:** **Slicing objects:** - **`Pick<T, Keys>`** – Keep only specific properties - **`Omit<T, Keys>`** – Remove specific properties **Adjusting optionality:** - **`Partial<T>`** – Make all properties optional - **`Required<T>`** – Make all properties required **Other patterns:** - **`Readonly<T>`** – Make properties read-only - **`Record<Keys, Type>`** – Build dictionary from keys and value type - **`ReturnType<F>`** – Extract function return type - **`Parameters<F>`** – Extract function parameter types as tuple **In this category, you'll:** - Use `Pick` and `Omit` to create focused types from larger ones - Apply `Partial` and `Required` to adjust optionality - Use `Readonly` to communicate immutability - Build dictionaries with `Record<K, V>` - Extract types from functions with `ReturnType` and `Parameters` These patterns help you connect APIs without duplicating type declarations, making refactoring safer and code more maintainable.
keyof, typeof & Indexed Access
This category links **runtime values** to **static types** using powerful type operators: **`keyof`** – Get all property names as a union: ```typescript type User = { id: number; name: string }; type UserKeys = keyof User; // "id" | "name" ``` **`T[K]`** – Indexed access to look up property types: ```typescript type User = { id: number; name: string }; type IdType = User["id"]; // number ``` **`typeof`** – Turn a runtime value into a type: ```typescript const config = { apiUrl: "...", timeout: 5000 }; type Config = typeof config; // { apiUrl: string; timeout: number } ``` **`as const`** – Preserve literal types: ```typescript const colors = ['red', 'green', 'blue'] as const; type Color = typeof colors[number]; // "red" | "green" | "blue" ``` **In this category, you'll:** - Use `keyof` to get property names safely - Look up property types with indexed access `T[K]` - Convert runtime values to types with `typeof` - Preserve literals with `as const` - Combine these tools to connect values and types Each exercise focuses on practical type connections while keeping the runtime code small and clear.