Complete TypeScript Tutorial

Master TypeScript with our comprehensive tutorial.



Getting Started with TypeScript: Your First Steps

Learn to set up TypeScript and create your first type-safe application

Key Concept: TypeScript is a superset of JavaScript that adds static typing. You write .ts files, compile them to .js, and run the JavaScript in any environment. TypeScript catches errors at compile time before your code runs.

What You'll Need

  • Node.js: Download from nodejs.org (includes npm)
  • Text Editor: VS Code (recommended - built-in TypeScript support)
  • Terminal/Command Line: For running TypeScript compiler
  • Basic JavaScript Knowledge: Understanding of JS fundamentals

Installing TypeScript

TypeScript is installed via npm (Node Package Manager). You can install it globally or per project.

Global Installation (Recommended for Beginners)
# Install TypeScript globally
npm install -g typescript

# Verify installation
tsc --version
# Output: Version 5.3.3 (or latest)
Project-Specific Installation
# Create project directory
mkdir my-typescript-project
cd my-typescript-project

# Initialize npm project
npm init -y

# Install TypeScript as dev dependency
npm install --save-dev typescript

# Create TypeScript config
npx tsc --init

Your First TypeScript File

Let's create a simple TypeScript file and compile it to JavaScript.

hello.ts - Your First TypeScript File
// Define a function with typed parameters
function greet(name: string): string {
    return `Hello, ${name}!`;
}

// TypeScript will catch type errors
const message = greet("TypeScript");
console.log(message);

// This would cause a compile error:
// greet(42); // Error: Argument of type 'number' not assignable to 'string'

Compiling TypeScript

Use the TypeScript compiler (tsc) to convert .ts files to .js files.

Compilation Commands
# Compile single file
tsc hello.ts
# Creates: hello.js

# Compile and watch for changes
tsc hello.ts --watch

# Compile with target ES version
tsc hello.ts --target ES2020

# Compile entire project using tsconfig.json
tsc

Understanding the Output

hello.js - Compiled JavaScript Output
// Type annotations are removed
function greet(name) {
    return `Hello, ${name}!`;
}

const message = greet("TypeScript");
console.log(message);

Setting Up VS Code for TypeScript

Feature Description Shortcut
IntelliSense Auto-completion and type info Ctrl+Space
Go to Definition Navigate to type definitions F12
Find References Find all usages Shift+F12
Rename Symbol Refactor names safely F2
Quick Fix Auto-fix type errors Ctrl+.

Creating a TypeScript Configuration

The tsconfig.json file controls compiler behavior and project settings.

Basic tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}
Practice Tasks
  • Task 1: Install TypeScript globally using npm.
  • Task 2: Create hello.ts and add a typed greet function.
  • Task 3: Compile hello.ts using tsc command.
  • Task 4: Run the compiled JavaScript with node hello.js.
  • Task 5: Initialize a project with tsconfig.json.
  • Task 6: Create a calculator function with typed parameters.
  • Task 7: Set up TypeScript watch mode and test live compilation.

Common Setup Issues

Troubleshooting
  • tsc not found: Make sure Node.js and npm are installed, then reinstall TypeScript globally
  • Permission errors: On Mac/Linux, use sudo: sudo npm install -g typescript
  • Old TypeScript version: Update with: npm install -g typescript@latest
  • VS Code not recognizing TypeScript: Reload window (Ctrl+Shift+P → "Reload Window")

Key Takeaways

  • TypeScript = JavaScript + Types: All valid JS is valid TS
  • Compile to JavaScript: TypeScript doesn't run directly
  • Catch errors early: Type checking happens at compile time
  • Use tsconfig.json: Configure compiler for your project
  • VS Code recommended: Best TypeScript development experience
  • Start simple: Add types gradually to existing projects

What's Next?

Next Topic: Now that you have TypeScript set up, let's explore what TypeScript is, why it was created, and how it enhances JavaScript development in the Introduction section.



TypeScript Introduction: Understanding Microsoft's JavaScript Enhancement

Discover why TypeScript has become the preferred choice for building scalable JavaScript applications

What is TypeScript? A Complete Overview

TypeScript is a strongly typed programming language that builds on JavaScript, developed and maintained by Microsoft. It's a superset of JavaScript, meaning any valid JavaScript code is also valid TypeScript. TypeScript adds optional static type checking, interfaces, enums, and advanced features that compile down to clean, readable JavaScript.

TypeScript was created to address the challenges of building and maintaining large-scale JavaScript applications. As codebases grow, JavaScript's dynamic typing can lead to runtime errors that are hard to catch during development. TypeScript solves this by introducing a type system that catches errors at compile time, providing developers with confidence and productivity gains.

Why TypeScript? Key Benefits

TypeScript has rapidly gained adoption among developers and organizations worldwide. Here's why TypeScript matters:

Type Safety

Catch errors at compile time before they reach production. TypeScript's type system prevents common bugs like null references, undefined properties, and type mismatches.

Enhanced Productivity

IntelliSense, auto-completion, and refactoring tools work better with type information, making development faster and reducing cognitive load.

Self-Documenting Code

Type annotations serve as inline documentation, making code more readable and maintainable. New developers can understand code structure instantly.

Modern JavaScript Features

Use the latest ECMAScript features today. TypeScript compiles to any JavaScript version, ensuring compatibility with older browsers.

TypeScript vs JavaScript: The Difference

Aspect JavaScript TypeScript
Type System Dynamic, runtime checking Static, compile-time checking
Error Detection Runtime errors Compile-time errors
Tooling Support Limited IntelliSense Rich IntelliSense & refactoring
Learning Curve Easier for beginners Requires type system knowledge
Browser Support Runs directly in browsers Must be compiled to JavaScript
File Extension .js .ts

TypeScript in Action: Before and After

JavaScript - Runtime Error
// JavaScript - error only discovered when code runs
function calculateTotal(price, quantity) {
    return price * quantity;
}

// This looks fine but will cause issues
calculateTotal("50", "3"); // Returns "5050" instead of 150
calculateTotal(null, 5);   // Returns 0 (unexpected)
calculateTotal();          // Returns NaN (undefined * undefined)
TypeScript - Compile-Time Protection
// TypeScript catches errors before runtime
function calculateTotal(price: number, quantity: number): number {
    return price * quantity;
}

// TypeScript prevents these mistakes
calculateTotal("50", "3");  // ❌ Error: string not assignable to number
calculateTotal(null, 5);    // ❌ Error: null not assignable to number
calculateTotal();           // ❌ Error: expected 2 arguments

// Only correct usage compiles
calculateTotal(50, 3);      // ✅ Returns 150

Key TypeScript Features

Core TypeScript Capabilities
  • Static Typing: Add type annotations to variables, parameters, and return values
  • Interfaces: Define contracts for object shapes and class implementations
  • Classes & OOP: Full-featured object-oriented programming with access modifiers
  • Generics: Write reusable, type-safe code that works with any data type
  • Enums: Define named constants for better code readability
  • Type Inference: Smart type detection without explicit annotations
  • Union & Intersection Types: Combine types in powerful ways
  • Advanced Types: Mapped types, conditional types, utility types
  • Decorators: Meta-programming for classes and methods
  • Modules & Namespaces: Organize code into logical units

Who Uses TypeScript?

TypeScript has been adopted by major tech companies and popular open-source projects:

🏢

Tech Giants

Microsoft, Google, Airbnb, Slack, Asana, Bloomberg

⚛️

Frameworks

Angular (built with TS), React, Vue 3, NestJS, Deno

📦

Libraries

RxJS, TypeORM, Jest, Prettier, ESLint

Real-World Example: User Management

TypeScript Ensures Data Integrity
// Define user structure with interface
interface User {
    id: number;
    name: string;
    email: string;
    role: 'admin' | 'user' | 'guest';
    createdAt: Date;
}

// Function with strong typing
function createUser(name: string, email: string): User {
    return {
        id: Date.now(),
        name,
        email,
        role: 'user',
        createdAt: new Date()
    };
}

// Type-safe array operations
const users: User[] = [];
users.push(createUser("Alice", "alice@example.com"));

// Autocomplete works perfectly
users[0].name; // ✅ IDE suggests: id, name, email, role, createdAt
users[0].age;  // ❌ Error: Property 'age' does not exist

When to Use TypeScript

✅ TypeScript is Great For
  • Large-scale applications
  • Team projects with multiple developers
  • Long-term maintained codebases
  • Complex business logic
  • Libraries and frameworks
  • When refactoring frequently
⚠️ Consider Carefully For
  • Small prototypes or proofs of concept
  • Simple scripts or one-off tools
  • Projects with tight deadlines
  • When team lacks TS experience
  • Very dynamic codebases
  • Quick experiments

Key Takeaways

  • Superset of JavaScript: All JavaScript is valid TypeScript
  • Optional static typing: Add types where they provide value
  • Compiles to JavaScript: Works anywhere JavaScript runs
  • Catches errors early: Type checking at development time
  • Better tooling: Enhanced IDE support and refactoring
  • Industry standard: Widely adopted for enterprise applications
  • Future-proof: Use modern features while targeting older JS versions

What's Next?

Next Topic: Learn about TypeScript's evolution and development history. Understand how it went from a Microsoft internal project to one of the most popular programming languages.



TypeScript History: From Microsoft Project to Industry Standard

Explore the journey of TypeScript from its inception to becoming one of the world's most popular programming languages

The Birth of TypeScript (2012)

TypeScript was created at Microsoft in 2012 by Anders Hejlsberg, the legendary developer behind Turbo Pascal, Delphi, and C#. The project emerged from Microsoft's need to build large-scale JavaScript applications with the reliability and maintainability of statically-typed languages.

As JavaScript applications grew in complexity, Microsoft teams struggled with common issues: runtime type errors, poor refactoring support, and difficulty maintaining large codebases. TypeScript was born to address these challenges while maintaining full JavaScript compatibility.

Major Milestones

Year Version Key Features
2012 0.8 Initial public release at Microsoft's Build conference
2014 1.0 First stable release, production-ready
2015 1.5 ES6 support, decorators, module resolution
2016 2.0 Non-nullable types, control flow analysis
2017 2.4 String enums, weak type detection
2018 3.0 Project references, tuple types
2020 4.0 Variadic tuple types, labeled tuples
2023 5.0 Decorators (stage 3), const type parameters
2024 5.3+ Import attributes, type-only imports refinements

Anders Hejlsberg: The Architect

Anders Hejlsberg brought decades of programming language design experience to TypeScript. His previous work on C# heavily influenced TypeScript's design philosophy: maintaining simplicity while providing powerful features for large-scale development.

  • Turbo Pascal (1980s): Fast, efficient compiler
  • Delphi (1990s): Visual development environment
  • C# (2000): Microsoft's flagship language
  • TypeScript (2012): JavaScript with types

Growth and Adoption Timeline

Early Days (2012-2015)
  • Initially met with skepticism
  • Angular 2 adoption (2015) boosted popularity
  • Google's internal adoption
  • Growing community support
Mainstream Adoption (2016-2020)
  • Major companies switching to TypeScript
  • React officially supports TypeScript
  • Vue 3 written in TypeScript
  • Top 10 most popular languages
Industry Standard (2020-Present)
  • 80%+ of npm packages provide TS types
  • Default for new enterprise projects
  • Stack Overflow's most loved language
  • Over 6 million npm downloads per week
Global Impact
  • Used by millions of developers
  • GitHub's 4th most used language
  • Essential for modern web development
  • Influences other languages (Flow, Dart)

Key Technical Evolution

TypeScript 1.0 (2014) - Basic Types
// Early TypeScript was simpler
function greet(name: string): string {
    return "Hello, " + name;
}

interface Person {
    name: string;
    age: number;
}
TypeScript 5.0 (2023) - Advanced Features
// Modern TypeScript is incredibly powerful
type AsyncResult = Promise<{ data: T; error: null } | { data: null; error: Error }>;

function createFetcher>() {
    return async (url: string): AsyncResult => {
        try {
            const response = await fetch(url);
            const data = await response.json() as T;
            return { data, error: null };
        } catch (error) {
            return { data: null, error: error as Error };
        }
    };
}

// Decorators (Stage 3 standard)
function logged(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const original = descriptor.value;
    descriptor.value = function(...args: any[]) {
        console.log(`Calling ${propertyKey} with`, args);
        return original.apply(this, args);
    };
}

Why TypeScript Succeeded

Success Factors
  • JavaScript Compatibility: Any valid JS is valid TS - easy migration
  • Gradual Adoption: Add types incrementally, no rewrite needed
  • Excellent Tooling: First-class VS Code support from day one
  • Open Source: Community-driven development on GitHub
  • Framework Support: Angular, React, Vue all embrace TypeScript
  • Regular Updates: New features every few months
  • Backward Compatible: Upgrading rarely breaks existing code
  • Type Definitions: DefinitelyTyped provides types for JavaScript libraries

Impact on the JavaScript Ecosystem

TypeScript fundamentally changed JavaScript development:

🛠️

Better Tooling

Inspired better IDE support for all JavaScript, not just TypeScript

📝

JSDoc Types

JavaScript can now use JSDoc comments for type checking without TS

📦

Type Definitions

80,000+ packages on DefinitelyTyped provide TypeScript types

Statistics and Adoption

By the Numbers (2024)
  • 38% of npm packages are written in TypeScript
  • 73% of developers want to continue using TypeScript (Stack Overflow Survey)
  • Top 10 GitHub language by repositories and contributions
  • 6+ million weekly downloads from npm
  • 95%+ of Fortune 500 tech companies use TypeScript
  • 4th most wanted language by developers learning new technologies

Key Takeaways

  • Created by Anders Hejlsberg at Microsoft in 2012
  • Evolved from experimental to industry standard in 12 years
  • Regular updates maintain relevance and add features
  • Open-source development model drives innovation
  • JavaScript compatibility ensured widespread adoption
  • Framework support accelerated mainstream acceptance
  • Now essential skill for modern web developers

What's Next?

Next Topic: Now that you understand TypeScript's history and significance, let's dive into Basic Types and learn how to add type safety to your code.



TypeScript Basic Types: Foundation of Type Safety

Master the fundamental types that form the building blocks of TypeScript's type system

Understanding TypeScript Types

Types in TypeScript define what kind of values a variable can hold. TypeScript's type system includes primitive types (string, number, boolean), special types (any, unknown, void, never), and complex types (arrays, tuples, objects). Type annotations use colon syntax: variable: type.

Primitive Types

TypeScript provides types for all JavaScript primitives plus a few additions.

String Type
// String type for text values
let username: string = "Alice";
let email: string = 'alice@example.com';
let message: string = `Hello, ${username}!`; // Template literals

// Type inference - TypeScript infers string type
let city = "New York"; // type: string

city = "London";  // ✅ OK
city = 12345;     // ❌ Error: Type 'number' is not assignable to type 'string'
Number Type
// Number type for all numeric values
let age: number = 25;
let price: number = 99.99;
let hexValue: number = 0xf00d;      // Hexadecimal
let binaryValue: number = 0b1010;   // Binary
let octalValue: number = 0o744;     // Octal

// Special numeric values
let infinity: number = Infinity;
let notANumber: number = NaN;

age = 26;      // ✅ OK
age = "26";    // ❌ Error: Type 'string' is not assignable to type 'number'
Boolean Type
// Boolean type for true/false values
let isActive: boolean = true;
let hasPermission: boolean = false;

// From expressions
let isAdult: boolean = age >= 18;
let isEmpty: boolean = items.length === 0;

isActive = false;     // ✅ OK
isActive = "false";   // ❌ Error: Type 'string' is not assignable to type 'boolean'
isActive = 0;         // ❌ Error: Type 'number' is not assignable to type 'boolean'

Arrays and Tuples

Array Types - Two Syntaxes
// Syntax 1: Type[]
let numbers: number[] = [1, 2, 3, 4, 5];
let names: string[] = ["Alice", "Bob", "Charlie"];

// Syntax 2: Array (generic syntax)
let scores: Array = [95, 87, 92];
let tags: Array = ["typescript", "javascript"];

// Mixed types require union types
let mixed: (string | number)[] = ["age", 25, "name", "Alice"];

// Arrays of objects
let users: { name: string; age: number }[] = [
    { name: "Alice", age: 25 },
    { name: "Bob", age: 30 }
];
Tuple Types - Fixed-Length Arrays
// Tuple: array with fixed length and specific types for each position
let person: [string, number] = ["Alice", 25];
let coordinate: [number, number] = [10.5, 20.3];

// Labeled tuples (TypeScript 4.0+)
let employee: [name: string, age: number, salary: number] = ["Bob", 30, 75000];

// Accessing tuple elements
console.log(person[0]); // "Alice" - type: string
console.log(person[1]); // 25 - type: number

// Type safety
person[0] = "Bob";     // ✅ OK
person[0] = 123;       // ❌ Error: Type 'number' is not assignable to type 'string'
person[2] = "extra";   // ❌ Error: Tuple of length 2 has no element at index 2

// Optional tuple elements
let point: [number, number, number?] = [10, 20]; // z-coordinate is optional

Special Types

Type Description When to Use
any Disables type checking Avoid when possible; use during migration or with dynamic data
unknown Type-safe version of any When you don't know the type but want safety
void Absence of a return value Functions that don't return anything
never Value that never occurs Functions that throw errors or infinite loops
null Intentional absence of value Explicitly represent "no value"
undefined Variable not assigned Optional parameters or properties
any vs unknown
// any: No type safety - avoid when possible
let anything: any = "hello";
anything = 42;
anything = true;
anything.nonExistent(); // ✅ No error, but will crash at runtime!

// unknown: Type-safe alternative
let something: unknown = "hello";
something = 42;
something = true;

// Must check type before using
if (typeof something === "string") {
    console.log(something.toUpperCase()); // ✅ OK - type narrowed to string
}

something.toUpperCase(); // ❌ Error: Object is of type 'unknown'
void and never
// void: Function returns nothing
function logMessage(message: string): void {
    console.log(message);
    // No return statement
}

// Can explicitly return undefined
function doNothing(): void {
    return undefined; // ✅ OK
}

// never: Function never returns
function throwError(message: string): never {
    throw new Error(message);
    // Never reaches end
}

function infiniteLoop(): never {
    while (true) {
        // Runs forever
    }
}

// Exhaustiveness checking with never
type Shape = Circle | Square;

function getArea(shape: Shape): number {
    switch (shape.kind) {
        case "circle":
            return Math.PI * shape.radius ** 2;
        case "square":
            return shape.size ** 2;
        default:
            const _exhaustive: never = shape; // Ensures all cases handled
            return _exhaustive;
    }
}

null and undefined

Handling null and undefined
// With strictNullChecks enabled (recommended)
let username: string = "Alice";
username = null;      // ❌ Error: Type 'null' is not assignable to type 'string'
username = undefined; // ❌ Error: Type 'undefined' is not assignable to type 'string'

// Explicitly allow null/undefined
let nullableString: string | null = "hello";
nullableString = null; // ✅ OK

let optionalString: string | undefined = "hello";
optionalString = undefined; // ✅ OK

// Both null and undefined
let flexible: string | null | undefined = "hello";
flexible = null;      // ✅ OK
flexible = undefined; // ✅ OK

// Optional properties (shorthand for | undefined)
interface User {
    name: string;
    email?: string; // Same as: email: string | undefined
}

const user: User = { name: "Alice" }; // ✅ OK - email is optional

Type Inference

TypeScript Infers Types Automatically
// TypeScript infers types without explicit annotations
let inferredString = "Hello";    // type: string
let inferredNumber = 42;         // type: number
let inferredBoolean = true;      // type: boolean
let inferredArray = [1, 2, 3];   // type: number[]

// Function return type inferred
function add(a: number, b: number) {
    return a + b; // return type inferred as number
}

// Object type inferred
const person = {
    name: "Alice",
    age: 25
}; // type: { name: string; age: number }

// Best practice: Let TypeScript infer when obvious
let message = "Hello";          // ✅ Good - inference is clear
let message: string = "Hello";  // ⚠️ Redundant - type is obvious

Type Annotations Best Practices

When to Add Type Annotations
  • ✅ Function parameters: Always annotate - inference doesn't work
  • ✅ Function return types: Be explicit for documentation and safety
  • ✅ Object properties: Annotate when structure is complex
  • ✅ Variables initialized later: TypeScript can't infer without initial value
  • ❌ Simple variable assignments: Let TypeScript infer obvious types
  • ❌ Return types of simple functions: Inference usually works perfectly
Practice Tasks
  • Task 1: Create variables with string, number, and boolean types.
  • Task 2: Create an array of numbers and an array of strings.
  • Task 3: Define a tuple representing a person: [name, age, city].
  • Task 4: Create a function that returns void and one that returns never.
  • Task 5: Practice with nullable types: string | null.
  • Task 6: Create an object and let TypeScript infer its type.
  • Task 7: Use unknown type safely with type checking.

Common Mistakes to Avoid

⚠️ Type Pitfalls
  • Overusing any: Defeats purpose of TypeScript
  • Forgetting strictNullChecks: Enable it for better safety
  • Over-annotating: Trust inference for simple cases
  • Confusing tuples and arrays: Tuples have fixed length
  • Using loose equality (==): Prefer strict (===) for type safety

Key Takeaways

  • Primitive types: string, number, boolean
  • Arrays: type[] or Array<type>
  • Tuples: Fixed-length arrays with specific types per position
  • any vs unknown: Prefer unknown for type safety
  • void: Functions that don't return
  • never: Functions that never return
  • Type inference: Let TypeScript infer obvious types
  • strictNullChecks: Enable for better null safety

What's Next?

Next Topic: Learn about Interfaces - TypeScript's way to define object shapes and contracts for your code.



TypeScript Interfaces: Defining Object Shapes

Master the art of creating type-safe contracts for objects, classes, and functions using TypeScript interfaces

What are Interfaces?

Interfaces in TypeScript are powerful contracts that define the structure of objects. They specify what properties and methods an object should have, along with their types. Unlike classes, interfaces exist only at compile-time and are used purely for type checking—they don't generate any JavaScript code.

Interfaces are one of TypeScript's core features for enforcing type safety and creating self-documenting code. They help catch errors early in development by ensuring objects conform to expected shapes, making your code more maintainable and less prone to runtime errors.

Why Use Interfaces?

Type Safety

Catch type-related bugs at compile time before they reach production, reducing runtime errors significantly.

Self-Documentation

Interfaces serve as clear contracts that document expected object structures, making code easier to understand.

IDE Support

Get excellent autocomplete, IntelliSense, and refactoring support when working with interfaces.

Reusability

Define once and reuse across multiple functions, classes, and modules for consistent type checking.

Basic Interface Syntax

Let's start with a simple interface definition:

Example: Basic Interface

// Define a simple interface
interface User {
    id: number;
    name: string;
    email: string;
    age: number;
}

// Use the interface
const user: User = {
    id: 1,
    name: "Alice Johnson",
    email: "alice@example.com",
    age: 28
};

// This will cause an error - missing 'age' property
const invalidUser: User = {
    id: 2,
    name: "Bob Smith",
    email: "bob@example.com"
    // Error: Property 'age' is missing
};

Optional Properties

Use the ? symbol to make properties optional. This is useful when some properties may or may not be present:

Example: Optional Properties

interface Product {
    id: number;
    name: string;
    price: number;
    description?: string;  // Optional property
    category?: string;     // Optional property
}

// Valid - optional properties can be omitted
const product1: Product = {
    id: 101,
    name: "Laptop",
    price: 999.99
};

// Also valid - optional properties included
const product2: Product = {
    id: 102,
    name: "Mouse",
    price: 29.99,
    description: "Wireless gaming mouse",
    category: "Accessories"
};

Readonly Properties

The readonly modifier prevents properties from being modified after object creation:

Example: Readonly Properties

interface Configuration {
    readonly apiKey: string;
    readonly apiUrl: string;
    timeout: number;
}

const config: Configuration = {
    apiKey: "abc123xyz",
    apiUrl: "https://api.example.com",
    timeout: 5000
};

// Valid - timeout is not readonly
config.timeout = 10000;

// Error - cannot modify readonly property
config.apiKey = "newkey123";  // Error!

// Readonly arrays
interface DataSet {
    readonly values: readonly number[];
}

const data: DataSet = {
    values: [1, 2, 3, 4, 5]
};

// Error - cannot modify readonly array
data.values.push(6);  // Error!

Function Type Interfaces

Interfaces can describe function signatures:

Example: Function Interfaces

// Interface for a function type
interface MathOperation {
    (a: number, b: number): number;
}

const add: MathOperation = (x, y) => x + y;
const subtract: MathOperation = (x, y) => x - y;

// Interface with multiple methods
interface Calculator {
    add(a: number, b: number): number;
    subtract(a: number, b: number): number;
    multiply(a: number, b: number): number;
    divide(a: number, b: number): number;
}

const calculator: Calculator = {
    add: (a, b) => a + b,
    subtract: (a, b) => a - b,
    multiply: (a, b) => a * b,
    divide: (a, b) => a / b
};

Interface Extension (Inheritance)

Interfaces can extend other interfaces using the extends keyword:

Example: Extending Interfaces

interface Person {
    name: string;
    age: number;
}

interface Employee extends Person {
    employeeId: number;
    department: string;
    salary: number;
}

const employee: Employee = {
    name: "John Doe",
    age: 30,
    employeeId: 12345,
    department: "Engineering",
    salary: 75000
};

// Multiple interface extension
interface Timestamped {
    createdAt: Date;
    updatedAt: Date;
}

interface AuditableEmployee extends Employee, Timestamped {
    lastModifiedBy: string;
}

const auditableEmployee: AuditableEmployee = {
    name: "Jane Smith",
    age: 28,
    employeeId: 67890,
    department: "Marketing",
    salary: 68000,
    createdAt: new Date("2024-01-01"),
    updatedAt: new Date("2024-06-15"),
    lastModifiedBy: "admin"
};

Index Signatures

Allow interfaces to have properties with dynamic keys:

Example: Index Signatures

// String index signature
interface StringMap {
    [key: string]: string;
}

const translations: StringMap = {
    hello: "Hola",
    goodbye: "Adiós",
    thanks: "Gracias"
};

// Number index signature
interface NumberArray {
    [index: number]: number;
}

const fibonacci: NumberArray = [0, 1, 1, 2, 3, 5, 8, 13];

// Mixed with known properties
interface UserDatabase {
    [userId: string]: User;
    count: number;  // Known property must match index signature type
}

const userDb: UserDatabase = {
    "user_001": { id: 1, name: "Alice", email: "alice@example.com", age: 28 },
    "user_002": { id: 2, name: "Bob", email: "bob@example.com", age: 32 },
    count: 2
};

Interface Merging (Declaration Merging)

💡 Did you know? TypeScript automatically merges multiple interface declarations with the same name. This is called declaration merging and is unique to interfaces.

Example: Interface Merging

// First declaration
interface Settings {
    theme: string;
    language: string;
}

// Second declaration - automatically merged
interface Settings {
    fontSize: number;
    notifications: boolean;
}

// The merged interface has all properties
const appSettings: Settings = {
    theme: "dark",
    language: "en",
    fontSize: 14,
    notifications: true
};

// Useful for extending library types
interface Window {
    myCustomProperty: string;
}

window.myCustomProperty = "Hello World";

Implementing Interfaces in Classes

Classes can implement interfaces using the implements keyword:

Example: Class Implementation

interface Vehicle {
    brand: string;
    model: string;
    year: number;
    start(): void;
    stop(): void;
}

class Car implements Vehicle {
    brand: string;
    model: string;
    year: number;

    constructor(brand: string, model: string, year: number) {
        this.brand = brand;
        this.model = model;
        this.year = year;
    }

    start(): void {
        console.log(`${this.brand} ${this.model} is starting...`);
    }

    stop(): void {
        console.log(`${this.brand} ${this.model} is stopping...`);
    }
}

const myCar = new Car("Toyota", "Camry", 2024);
myCar.start();  // Output: Toyota Camry is starting...

Hybrid Types

Interfaces can describe objects that act as both a function and an object with properties:

Example: Hybrid Types

interface Counter {
    (start: number): string;
    interval: number;
    reset(): void;
}

function createCounter(): Counter {
    const counter = function(start: number) {
        return `Counter started at ${start}`;
    } as Counter;

    counter.interval = 1000;
    counter.reset = function() {
        console.log("Counter reset");
    };

    return counter;
}

const myCounter = createCounter();
console.log(myCounter(0));  // Counter started at 0
console.log(myCounter.interval);  // 1000
myCounter.reset();  // Counter reset

Interface vs Type Alias

Feature Interface Type Alias
Declaration Merging ✅ Supported ❌ Not Supported
Extends Classes ✅ Yes ❌ No
Union Types ❌ No ✅ Yes
Intersection Types ❌ No (use extends) ✅ Yes
Primitives & Tuples ❌ No ✅ Yes
Performance Slightly better for objects Same for most cases

Best Practices

Interface Best Practices
  • Naming Convention: Use PascalCase and descriptive names (e.g., UserProfile, ApiResponse)
  • Prefer Interfaces for Objects: Use interfaces when defining object shapes; use type aliases for unions and primitives
  • Keep Interfaces Focused: Follow the Single Responsibility Principle—each interface should represent one concept
  • Use Optional Properties Wisely: Don't make everything optional; be intentional about what's required
  • Document Complex Interfaces: Add JSDoc comments for interfaces with non-obvious purposes
  • Avoid Empty Interfaces: Empty interfaces provide no type safety; use {} or Record<string, unknown> instead

Common Pitfalls

⚠️ Watch Out: Remember that interfaces are compile-time only. They don't exist in runtime JavaScript, so you can't use instanceof to check if an object matches an interface. Use type guards or runtime validation libraries instead.

Practice Exercise

Try creating these interfaces to practice what you've learned:

  1. Create a BlogPost interface with title, content, author, publishedDate, and optional tags array
  2. Extend it to create a FeaturedPost interface that adds featuredImage and priority properties
  3. Create a Repository<T> interface with methods: getAll(), getById(id: number), create(item: T), update(id: number, item: T), delete(id: number)
  4. Implement the Repository interface in a BlogPostRepository class
  5. Create an interface with an index signature to represent a dictionary of products where keys are SKU codes

Key Takeaways

  • Interfaces define contracts for object shapes and provide strong type safety
  • Use ? for optional properties and readonly for immutable properties
  • Interfaces can extend other interfaces for code reusability
  • Declaration merging allows multiple interface declarations with the same name to be automatically merged
  • Classes can implement interfaces using the implements keyword
  • Index signatures enable dynamic property names with type constraints
  • Prefer interfaces for object shapes; use type aliases for unions and primitives
  • Interfaces exist only at compile-time and generate no runtime JavaScript code

What's Next?

Next Step: Now that you understand interfaces, let's explore Classes in TypeScript. You'll learn how to combine interfaces with classes to create robust, object-oriented code with full type safety and powerful inheritance capabilities.



TypeScript Classes: Object-Oriented Programming

Master class syntax, access modifiers, inheritance, and object-oriented design patterns in TypeScript

What are Classes in TypeScript?

Classes are blueprints for creating objects with predefined properties and methods. TypeScript enhances JavaScript's class syntax with static typing, access modifiers, abstract classes, and more powerful object-oriented programming features. Classes compile to constructor functions in JavaScript for backward compatibility.

Classes are fundamental to object-oriented programming (OOP) and help you organize code into reusable, maintainable structures. TypeScript's class system builds upon ES6 classes while adding type safety and additional features that make your code more robust and self-documenting.

Why Use Classes?

Encapsulation

Bundle data and methods that operate on that data together, hiding internal implementation details.

Reusability

Create reusable blueprints that can be instantiated multiple times with different data.

Inheritance

Build class hierarchies where child classes inherit and extend functionality from parent classes.

Type Safety

Get compile-time type checking for properties, methods, and method parameters.

Basic Class Syntax

Let's start with a simple class definition:

Example: Basic Class

class Person {
    // Properties
    name: string;
    age: number;

    // Constructor
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }

    // Method
    greet(): string {
        return `Hello, my name is ${this.name} and I'm ${this.age} years old.`;
    }
}

// Creating instances
const person1 = new Person("Alice", 28);
const person2 = new Person("Bob", 32);

console.log(person1.greet());  // Hello, my name is Alice and I'm 28 years old.
console.log(person2.greet());  // Hello, my name is Bob and I'm 32 years old.

Access Modifiers

TypeScript provides three access modifiers to control the visibility of class members:

Modifier Description Accessible From
public Default. Accessible from anywhere Class, subclasses, and external code
private Only accessible within the class Only the defining class
protected Accessible in class and subclasses Class and its subclasses

Example: Access Modifiers

class BankAccount {
    public accountNumber: string;      // Accessible everywhere
    private balance: number;            // Only within this class
    protected owner: string;            // This class and subclasses

    constructor(accountNumber: string, owner: string, initialBalance: number) {
        this.accountNumber = accountNumber;
        this.owner = owner;
        this.balance = initialBalance;
    }

    // Public method to access private balance
    public getBalance(): number {
        return this.balance;
    }

    public deposit(amount: number): void {
        if (amount > 0) {
            this.balance += amount;
            console.log(`Deposited $${amount}. New balance: $${this.balance}`);
        }
    }

    private validateTransaction(amount: number): boolean {
        return amount > 0 && amount <= this.balance;
    }

    public withdraw(amount: number): boolean {
        if (this.validateTransaction(amount)) {
            this.balance -= amount;
            console.log(`Withdrew $${amount}. New balance: $${this.balance}`);
            return true;
        }
        console.log("Invalid transaction");
        return false;
    }
}

const account = new BankAccount("123456", "Alice", 1000);
console.log(account.accountNumber);  // OK: public
console.log(account.getBalance());   // OK: accessing via public method
// console.log(account.balance);     // Error: private
// console.log(account.owner);       // Error: protected

Constructor Shorthand

TypeScript provides a shorthand syntax for declaring and initializing properties in the constructor:

Example: Constructor Shorthand

// Long form
class Product {
    name: string;
    price: number;
    category: string;

    constructor(name: string, price: number, category: string) {
        this.name = name;
        this.price = price;
        this.category = category;
    }
}

// Shorthand - much cleaner!
class ProductShort {
    constructor(
        public name: string,
        public price: number,
        public category: string
    ) {}
}

const product = new ProductShort("Laptop", 999.99, "Electronics");
console.log(product.name);  // Laptop

Getters and Setters

Getters and setters allow you to control access to class properties:

Example: Getters and Setters

class Temperature {
    private _celsius: number;

    constructor(celsius: number) {
        this._celsius = celsius;
    }

    // Getter
    get celsius(): number {
        return this._celsius;
    }

    // Setter with validation
    set celsius(value: number) {
        if (value < -273.15) {
            throw new Error("Temperature cannot be below absolute zero");
        }
        this._celsius = value;
    }

    // Computed property
    get fahrenheit(): number {
        return (this._celsius * 9/5) + 32;
    }

    set fahrenheit(value: number) {
        this._celsius = (value - 32) * 5/9;
    }
}

const temp = new Temperature(25);
console.log(temp.celsius);     // 25
console.log(temp.fahrenheit);  // 77

temp.celsius = 30;
console.log(temp.fahrenheit);  // 86

temp.fahrenheit = 68;
console.log(temp.celsius);     // 20

Readonly Properties

The readonly modifier makes properties immutable after initialization:

Example: Readonly Properties

class User {
    readonly id: number;
    readonly createdAt: Date;
    name: string;

    constructor(id: number, name: string) {
        this.id = id;
        this.name = name;
        this.createdAt = new Date();
    }

    updateName(newName: string): void {
        this.name = newName;  // OK
    }
}

const user = new User(1, "Alice");
user.name = "Alice Johnson";  // OK
// user.id = 2;               // Error: readonly
// user.createdAt = new Date(); // Error: readonly

Inheritance

Classes can inherit from other classes using the extends keyword:

Example: Class Inheritance

class Animal {
    constructor(public name: string) {}

    move(distance: number = 0): void {
        console.log(`${this.name} moved ${distance} meters.`);
    }

    makeSound(): void {
        console.log("Some generic animal sound");
    }
}

class Dog extends Animal {
    constructor(name: string, public breed: string) {
        super(name);  // Call parent constructor
    }

    // Override parent method
    makeSound(): void {
        console.log("Woof! Woof!");
    }

    // New method specific to Dog
    fetch(): void {
        console.log(`${this.name} is fetching the ball!`);
    }
}

class Cat extends Animal {
    constructor(name: string) {
        super(name);
    }

    makeSound(): void {
        console.log("Meow!");
    }

    climb(): void {
        console.log(`${this.name} climbed a tree!`);
    }
}

const dog = new Dog("Buddy", "Golden Retriever");
dog.move(10);      // Buddy moved 10 meters.
dog.makeSound();   // Woof! Woof!
dog.fetch();       // Buddy is fetching the ball!

const cat = new Cat("Whiskers");
cat.move(5);       // Whiskers moved 5 meters.
cat.makeSound();   // Meow!
cat.climb();       // Whiskers climbed a tree!

Static Members

Static properties and methods belong to the class itself rather than instances:

Example: Static Members

class MathUtils {
    static PI: number = 3.14159;
    static E: number = 2.71828;

    static circleArea(radius: number): number {
        return this.PI * radius * radius;
    }

    static circleCircumference(radius: number): number {
        return 2 * this.PI * radius;
    }
}

// Access static members without creating instance
console.log(MathUtils.PI);                    // 3.14159
console.log(MathUtils.circleArea(5));         // 78.53975
console.log(MathUtils.circleCircumference(5)); // 31.4159

class Counter {
    static count: number = 0;

    constructor() {
        Counter.count++;
    }

    static getCount(): number {
        return Counter.count;
    }
}

new Counter();
new Counter();
new Counter();
console.log(Counter.getCount());  // 3

Abstract Classes

Abstract classes are base classes that cannot be instantiated directly. They define a contract for subclasses:

Example: Abstract Classes

abstract class Shape {
    constructor(public color: string) {}

    // Abstract method - must be implemented by subclasses
    abstract calculateArea(): number;
    abstract calculatePerimeter(): number;

    // Concrete method - inherited by subclasses
    describe(): string {
        return `A ${this.color} shape with area ${this.calculateArea()}`;
    }
}

class Circle extends Shape {
    constructor(color: string, public radius: number) {
        super(color);
    }

    calculateArea(): number {
        return Math.PI * this.radius ** 2;
    }

    calculatePerimeter(): number {
        return 2 * Math.PI * this.radius;
    }
}

class Rectangle extends Shape {
    constructor(color: string, public width: number, public height: number) {
        super(color);
    }

    calculateArea(): number {
        return this.width * this.height;
    }

    calculatePerimeter(): number {
        return 2 * (this.width + this.height);
    }
}

// const shape = new Shape("red");  // Error: Cannot instantiate abstract class

const circle = new Circle("blue", 5);
console.log(circle.describe());  // A blue shape with area 78.54...
console.log(circle.calculatePerimeter());  // 31.41...

const rectangle = new Rectangle("green", 4, 6);
console.log(rectangle.describe());  // A green shape with area 24
console.log(rectangle.calculatePerimeter());  // 20

Implementing Interfaces

Classes can implement one or more interfaces:

Example: Implementing Interfaces

interface Flyable {
    fly(): void;
    altitude: number;
}

interface Swimmable {
    swim(): void;
    depth: number;
}

class Duck implements Flyable, Swimmable {
    altitude: number = 0;
    depth: number = 0;

    constructor(public name: string) {}

    fly(): void {
        this.altitude = 100;
        console.log(`${this.name} is flying at ${this.altitude} feet`);
    }

    swim(): void {
        this.depth = 5;
        console.log(`${this.name} is swimming at ${this.depth} feet deep`);
    }

    quack(): void {
        console.log("Quack! Quack!");
    }
}

const duck = new Duck("Donald");
duck.fly();   // Donald is flying at 100 feet
duck.swim();  // Donald is swimming at 5 feet deep
duck.quack(); // Quack! Quack!

Class Expressions

Classes can also be defined as expressions:

Example: Class Expressions

const MyClass = class {
    constructor(public value: number) {}

    multiply(n: number): number {
        return this.value * n;
    }
};

const instance = new MyClass(5);
console.log(instance.multiply(3));  // 15

// Anonymous class
const createPoint = () => {
    return new class {
        constructor(public x: number, public y: number) {}

        distance(): number {
            return Math.sqrt(this.x ** 2 + this.y ** 2);
        }
    }(3, 4);
};

const point = createPoint();
console.log(point.distance());  // 5

Parameter Properties

💡 Pro Tip: You can declare and initialize class properties directly in the constructor parameters. This is called parameter properties and is a TypeScript-specific feature that reduces boilerplate code.

Example: Parameter Properties with All Modifiers

class Employee {
    // All properties declared and initialized in constructor
    constructor(
        public readonly id: number,
        public name: string,
        private salary: number,
        protected department: string
    ) {}

    getSalary(): number {
        return this.salary;
    }

    raiseSalary(percentage: number): void {
        this.salary *= (1 + percentage / 100);
    }
}

const emp = new Employee(1, "John Doe", 50000, "Engineering");
console.log(emp.id);    // 1
console.log(emp.name);  // John Doe
// console.log(emp.salary);  // Error: private

Best Practices

Class Design Best Practices
  • Single Responsibility: Each class should have one clear purpose and responsibility
  • Encapsulation: Use private/protected modifiers to hide implementation details
  • Favor Composition Over Inheritance: Don't overuse inheritance; consider composition for flexibility
  • Use Access Modifiers: Be explicit about public, private, and protected members
  • Initialize in Constructor: Always initialize properties, or mark them as optional
  • Immutability: Use readonly for properties that shouldn't change after construction
  • Abstract Base Classes: Use abstract classes to define contracts for related subclasses
  • Interface Implementation: Implement interfaces to ensure classes conform to contracts

Common Patterns

Example: Singleton Pattern

class Database {
    private static instance: Database;
    private constructor() {
        console.log("Database instance created");
    }

    static getInstance(): Database {
        if (!Database.instance) {
            Database.instance = new Database();
        }
        return Database.instance;
    }

    query(sql: string): void {
        console.log(`Executing: ${sql}`);
    }
}

// const db = new Database();  // Error: constructor is private
const db1 = Database.getInstance();  // Database instance created
const db2 = Database.getInstance();  // Uses existing instance
console.log(db1 === db2);  // true

Practice Exercise

Build a library management system with the following requirements:

  1. Create an abstract LibraryItem class with properties: id, title, and year. Include an abstract method getDescription()
  2. Create Book and Magazine classes that extend LibraryItem
  3. Add a Borrowable interface with methods: borrow(), return(), and property isAvailable
  4. Implement the Borrowable interface in your classes
  5. Create a Library class with static property for tracking total items and methods to add/remove items
  6. Use appropriate access modifiers, getters/setters, and readonly properties

Key Takeaways

  • Classes are blueprints for creating objects with properties and methods
  • Access modifiers (public, private, protected) control visibility of class members
  • Constructor shorthand allows declaring and initializing properties in one place
  • Getters and setters provide controlled access to private properties
  • Classes can inherit from other classes using extends
  • Static members belong to the class itself, not instances
  • Abstract classes define contracts that subclasses must implement
  • Classes can implement one or more interfaces
  • Use readonly to prevent property modification after initialization

What's Next?

Next Step: Now that you've mastered classes, let's explore Functions in TypeScript. You'll learn about function types, optional and default parameters, rest parameters, overloading, and arrow functions with full type safety.



TypeScript Functions: Types, Parameters, and Overloads

Learn to type parameters, returns, overloads, and callbacks while leveraging inference and safety

Why Type Functions?

Functions are contracts. Typing parameters and return values catches mistakes early, improves IntelliSense, and documents intent. Always type parameters; return types can often be inferred but should be explicit on public APIs.

Typed Parameters and Returns

Parameter and Return Types
function add(a: number, b: number): number {
    return a + b;
}

// Inference works too, but return type can be explicit for APIs
const multiply = (a: number, b: number): number => a * b;

// Void for functions that don't return
function logMessage(message: string): void {
    console.log(message);
}

// Never for functions that never complete
function fail(message: string): never {
    throw new Error(message);
}

Optional and Default Parameters

Optional (?) and Defaults
function greet(name: string, title?: string): string {
    return title ? `${title} ${name}` : name;
}

function formatPrice(amount: number, currency: string = 'USD'): string {
    return `${currency} ${amount.toFixed(2)}`;
}

greet('Alice');          // "Alice"
greet('Alice', 'Dr.');   // "Dr. Alice"
formatPrice(9.5);        // "USD 9.50"
formatPrice(9.5, 'EUR'); // "EUR 9.50"

Rest Parameters

Variable Arguments
function sum(...values: number[]): number {
    return values.reduce((total, n) => total + n, 0);
}

sum(1, 2, 3);       // 6
sum(10, 20, 30, 5); // 65

Function Types and Callbacks

Typing Functions as Values
// Function type alias
type Transformer = (input: string) => string;

const toUpper: Transformer = (text) => text.toUpperCase();
const trim: Transformer = (text) => text.trim();

// Passing callbacks
function process(text: string, transform: Transformer): string {
    return transform(text);
}

process(' hello ', trim);       // "hello"
process('typescript', toUpper); // "TYPESCRIPT"

Function Overloads

Declaring Overloads
// Overload signatures
function format(input: number): string;
function format(input: Date): string;
function format(input: string): string;

// Implementation signature
function format(input: number | Date | string): string {
    if (typeof input === 'number') return input.toFixed(2);
    if (input instanceof Date) return input.toISOString();
    return input.trim();
}

format(12.345);          // "12.35"
format(new Date());      // ISO string
format('  hello  ');     // "hello"

Rule: Overload signatures come first; the implementation uses the union of allowed types and performs runtime narrowing.

this Parameters

Typing this Explicitly
interface Button {
    label: string;
    click(this: Button): void;
}

const button: Button = {
    label: 'Save',
    click() {
        console.log(`Clicked ${this.label}`);
    },
};

// Arrow functions capture this lexically, so rarely need explicit this

Generic Functions

Reusability with Generics
function wrap(value: T): { value: T } {
    return { value };
}

const wrappedNumber = wrap(42);          // { value: number }
const wrappedUser = wrap({ name: 'A' }); // { value: { name: string } }

Avoid quick fixes: Skip @ts-ignore and any. Prefer unions, generics, or type guards to maintain safety.

Practice Tasks
  • Create a function with optional and default parameters (e.g., greet).
  • Write a function type alias and use it to type a callback.
  • Implement function overloads for parsing numbers, dates, and strings.
  • Add a generic function that returns the first element of an array.
  • Type a method that uses an explicit this parameter.
  • Refactor a function to remove all any types using unions or generics.

Key Takeaways

  • Type all parameters; annotate return types on public APIs.
  • Use optional (?), defaults, and rest parameters for flexibility.
  • Overloads document multiple call signatures; implement with unions and narrowing.
  • Arrow functions inherit this; add explicit this typing when needed.
  • Prefer generics and guards over any and assertions.

What's Next?

Next Topic: Explore Union and Intersection Types to combine shapes safely.



TypeScript Generics: Writing Reusable, Type-Safe Code

Master generics to create flexible, reusable functions and classes that work with any data type while maintaining type safety

What are Generics?

Generics allow you to write code that works with multiple types while preserving type information. Instead of using any, generics let you create reusable components that maintain type safety. Think of generics as "type variables" or "type parameters" that are filled in when the code is used.

The Problem Without Generics

Type Safety Lost with any
// Without generics - loses type safety
function identity(value: any): any {
    return value;
}

const result1 = identity("hello");  // result1 is any
const result2 = identity(42);       // result2 is any

// No autocomplete, no type checking
result1.toUpperCase(); // Works
result2.toUpperCase(); // No error, but crashes at runtime!

Basic Generic Functions

Generic Function Syntax
// Generic function with type parameter T
function identity(value: T): T {
    return value;
}

// TypeScript infers the type
const str = identity("hello");      // str: string
const num = identity(42);            // num: number
const bool = identity(true);         // bool: boolean

// Explicitly specify the type
const result = identity("hello");

// Now we have full type safety
str.toUpperCase(); // ✅ Works - TypeScript knows it's a string
num.toUpperCase(); // ❌ Error - Property 'toUpperCase' does not exist on type 'number'

Generic Arrays and Functions

Working with Generic Arrays
// Get first element of array
function first(arr: T[]): T | undefined {
    return arr[0];
}

const numbers = [1, 2, 3];
const firstNum = first(numbers);  // type: number | undefined

const names = ["Alice", "Bob"];
const firstName = first(names);   // type: string | undefined

// Get last element
function last(arr: T[]): T | undefined {
    return arr[arr.length - 1];
}

// Filter array
function filter(arr: T[], predicate: (item: T) => boolean): T[] {
    const result: T[] = [];
    for (const item of arr) {
        if (predicate(item)) {
            result.push(item);
        }
    }
    return result;
}

const even = filter([1, 2, 3, 4, 5, 6], n => n % 2 === 0);
// even: number[] = [2, 4, 6]

Generic Interfaces

Interface with Type Parameters
// Generic interface for API response
interface ApiResponse {
    data: T;
    status: number;
    message: string;
}

// Use with different data types
interface User {
    id: number;
    name: string;
}

const userResponse: ApiResponse = {
    data: { id: 1, name: "Alice" },
    status: 200,
    message: "Success"
};

const numbersResponse: ApiResponse = {
    data: [1, 2, 3, 4, 5],
    status: 200,
    message: "Success"
};

// Generic interface for key-value pairs
interface KeyValuePair {
    key: K;
    value: V;
}

const pair1: KeyValuePair = {
    key: "age",
    value: 25
};

const pair2: KeyValuePair = {
    key: 1,
    value: "First"
};

Generic Classes

Creating Generic Classes
// Generic Stack data structure
class Stack {
    private items: T[] = [];

    push(item: T): void {
        this.items.push(item);
    }

    pop(): T | undefined {
        return this.items.pop();
    }

    peek(): T | undefined {
        return this.items[this.items.length - 1];
    }

    isEmpty(): boolean {
        return this.items.length === 0;
    }

    size(): number {
        return this.items.length;
    }
}

// Use with numbers
const numberStack = new Stack();
numberStack.push(1);
numberStack.push(2);
numberStack.push(3);
console.log(numberStack.pop()); // 3

// Use with strings
const stringStack = new Stack();
stringStack.push("hello");
stringStack.push("world");
console.log(stringStack.pop()); // "world"

// Type safety enforced
// numberStack.push("string"); // ❌ Error: Argument of type 'string' not assignable to parameter of type 'number'

Generic Constraints

Constraining Type Parameters with extends
// Constraint: T must have a length property
function getLength(item: T): number {
    return item.length;
}

getLength("hello");        // ✅ OK - strings have length
getLength([1, 2, 3]);      // ✅ OK - arrays have length
getLength({ length: 10 }); // ✅ OK - object has length
getLength(42);             // ❌ Error: number doesn't have length

// Constraint with interface
interface Printable {
    print(): void;
}

function printItem(item: T): void {
    item.print();
}

// Multiple constraints
function merge(obj1: T, obj2: U): T & U {
    return { ...obj1, ...obj2 };
}

const merged = merge({ name: "Alice" }, { age: 25 });
// merged: { name: string } & { age: number }
console.log(merged.name); // "Alice"
console.log(merged.age);  // 25

Multiple Type Parameters

Using Multiple Generics
// Function with two type parameters
function pair(first: T, second: U): [T, U] {
    return [first, second];
}

const result1 = pair("age", 25);        // [string, number]
const result2 = pair(true, "yes");     // [boolean, string]

// Generic Map-like structure
class Dictionary {
    private items: Map = new Map();

    set(key: K, value: V): void {
        this.items.set(key, value);
    }

    get(key: K): V | undefined {
        return this.items.get(key);
    }

    has(key: K): boolean {
        return this.items.has(key);
    }

    entries(): [K, V][] {
        return Array.from(this.items.entries());
    }
}

const userAges = new Dictionary();
userAges.set("Alice", 25);
userAges.set("Bob", 30);
console.log(userAges.get("Alice")); // 25

Default Type Parameters

Providing Default Types
// Generic with default type
interface Box {
    value: T;
}

// Uses default (string)
const box1: Box = { value: "hello" };

// Explicitly specify different type
const box2: Box = { value: 42 };

// Function with default type parameter
function create(value?: T): T {
    return value ?? { id: Date.now() } as T;
}

const obj1 = create();                    // type: { id: number }
const obj2 = create({ name: "Alice" });   // type: { name: string }

Real-World Example: HTTP Client

Generic API Client
// Generic HTTP client
class HttpClient {
    async get(url: string): Promise {
        const response = await fetch(url);
        return response.json() as Promise;
    }

    async post(url: string, data: T): Promise {
        const response = await fetch(url, {
            method: 'POST',
            body: JSON.stringify(data),
            headers: { 'Content-Type': 'application/json' }
        });
        return response.json() as Promise;
    }
}

// Define data types
interface User {
    id: number;
    name: string;
    email: string;
}

interface CreateUserRequest {
    name: string;
    email: string;
}

// Use with type safety
const client = new HttpClient();

// GET request - response typed as User[]
const users = await client.get('/api/users');
users.forEach(user => console.log(user.name)); // Full autocomplete

// POST request - request and response typed
const newUser = await client.post(
    '/api/users',
    { name: "Alice", email: "alice@example.com" }
);
console.log(newUser.id); // Full type safety
Practice Tasks
  • Task 1: Create a generic function that swaps two values in a tuple.
  • Task 2: Implement a generic Queue class with enqueue and dequeue methods.
  • Task 3: Create a generic function to find an item in an array by property.
  • Task 4: Build a generic Result type for error handling (success/failure).
  • Task 5: Implement a generic cache with get/set/has methods.
  • Task 6: Create a generic debounce function with proper typing.
  • Task 7: Build a generic form state manager with validation.

Key Takeaways

  • Generics preserve type information: Better than using any
  • Type parameters use angle brackets: <T> convention
  • TypeScript infers types: Usually don't need to specify explicitly
  • Constraints narrow possibilities: Use extends for requirements
  • Multiple type parameters: <T, U, V> for complex cases
  • Default type parameters: Provide fallback types
  • Works with functions, classes, interfaces: Universal feature

What's Next?

Next Topic: Learn about Enums in TypeScript - a way to define named constants with better type safety than plain objects or strings.



TypeScript Enums: Named Constants for Better Code

Learn to use enums for creating sets of named constants that improve code readability and type safety

What are Enums?

Enums (enumerations) allow you to define a set of named constants. They provide a way to give friendly names to sets of numeric or string values, making your code more readable and self-documenting. Unlike most TypeScript features, enums produce real JavaScript code at runtime.

Numeric Enums

Basic Numeric Enum
// Numeric enum - auto-increments from 0
enum Direction {
    Up,      // 0
    Down,    // 1
    Left,    // 2
    Right    // 3
}

// Usage
let direction: Direction = Direction.Up;
console.log(direction); // 0

if (direction === Direction.Up) {
    console.log("Moving up!");
}

// With custom starting value
enum Status {
    Pending = 1,
    Approved,     // 2
    Rejected,     // 3
    Completed     // 4
}

console.log(Status.Pending);    // 1
console.log(Status.Approved);   // 2

String Enums

String Enum Values
// String enum - must initialize all members
enum Color {
    Red = "RED",
    Green = "GREEN",
    Blue = "BLUE"
}

console.log(Color.Red);  // "RED"

// More descriptive string values
enum LogLevel {
    Error = "ERROR",
    Warning = "WARNING",
    Info = "INFO",
    Debug = "DEBUG"
}

function log(message: string, level: LogLevel): void {
    console.log(`[${level}] ${message}`);
}

log("Application started", LogLevel.Info);
// Output: [INFO] Application started

// String enums are great for API responses
enum UserRole {
    Admin = "ADMIN",
    Moderator = "MODERATOR",
    User = "USER",
    Guest = "GUEST"
}

function checkPermission(role: UserRole): boolean {
    return role === UserRole.Admin || role === UserRole.Moderator;
}

Heterogeneous Enums

Mixed String and Number Enums
// Mixing string and number values (not recommended)
enum BooleanLike {
    No = 0,
    Yes = "YES"
}

// Better approach - use separate enums
enum NumericBoolean {
    False = 0,
    True = 1
}

enum StringBoolean {
    False = "FALSE",
    True = "TRUE"
}

Computed and Constant Members

Enum with Computed Values
// Constant enum members
enum FileAccess {
    None,
    Read = 1 << 1,      // Computed: 2
    Write = 1 << 2,     // Computed: 4
    ReadWrite = Read | Write,  // Computed: 6
    All = ReadWrite | 1        // Computed: 7
}

console.log(FileAccess.Read);      // 2
console.log(FileAccess.Write);     // 4
console.log(FileAccess.ReadWrite); // 6

// Check permissions using bitwise operations
function hasPermission(access: FileAccess, permission: FileAccess): boolean {
    return (access & permission) === permission;
}

const userAccess = FileAccess.ReadWrite;
console.log(hasPermission(userAccess, FileAccess.Read));  // true
console.log(hasPermission(userAccess, FileAccess.Write)); // true

Reverse Mapping

Numeric Enums Have Reverse Mapping
// Numeric enums support reverse mapping
enum Status {
    Active = 1,
    Inactive = 2,
    Pending = 3
}

// Forward mapping: name -> value
console.log(Status.Active);  // 1

// Reverse mapping: value -> name
console.log(Status[1]);      // "Active"
console.log(Status[2]);      // "Inactive"

// Iterate over enum
function printEnumValues(enumObj: any): void {
    for (const key in enumObj) {
        if (isNaN(Number(key))) {
            console.log(`${key} = ${enumObj[key]}`);
        }
    }
}

printEnumValues(Status);
// Output:
// Active = 1
// Inactive = 2
// Pending = 3

// Note: String enums do NOT have reverse mapping
enum Color {
    Red = "RED"
}
console.log(Color["RED"]); // undefined

Const Enums

Const Enums for Performance
// Regular enum - creates JavaScript object
enum RegularEnum {
    A,
    B,
    C
}

const value1 = RegularEnum.A; // Compiles to: const value1 = RegularEnum.A;

// Const enum - inlined at compile time
const enum ConstEnum {
    A,
    B,
    C
}

const value2 = ConstEnum.A; // Compiles to: const value2 = 0;

// Advantages of const enums:
// - No runtime object created
// - Values are inlined
// - Smaller bundle size
// - Better performance

// Disadvantage:
// - Cannot use reverse mapping
// - Cannot iterate over values

Enums vs Union Types

Feature Enum Union Type
Runtime representation ✅ Yes - real object ❌ No - compile-time only
Autocomplete ✅ Excellent ✅ Excellent
Reverse mapping ✅ Yes (numeric only) ❌ No
Bundle size ⚠️ Larger ✅ Zero runtime cost
Iteration ✅ Possible ❌ Not possible
API integration ✅ Direct mapping ⚠️ Need conversion
Enum vs Union Type Comparison
// Using enum
enum Color {
    Red = "RED",
    Green = "GREEN",
    Blue = "BLUE"
}

function paintWithEnum(color: Color): void {
    console.log(`Painting with ${color}`);
}

paintWithEnum(Color.Red); // ✅ OK

// Using union type
type ColorUnion = "RED" | "GREEN" | "BLUE";

function paintWithUnion(color: ColorUnion): void {
    console.log(`Painting with ${color}`);
}

paintWithUnion("RED"); // ✅ OK

// Enum provides namespace
Color.Red; // Clear where it comes from

// Union type requires quotes
"RED"; // Less clear, could be any string

Real-World Examples

HTTP Status Codes
enum HttpStatus {
    OK = 200,
    Created = 201,
    BadRequest = 400,
    Unauthorized = 401,
    Forbidden = 403,
    NotFound = 404,
    InternalServerError = 500
}

function handleResponse(status: HttpStatus): string {
    switch (status) {
        case HttpStatus.OK:
            return "Success!";
        case HttpStatus.Created:
            return "Resource created!";
        case HttpStatus.BadRequest:
            return "Invalid request";
        case HttpStatus.Unauthorized:
            return "Please login";
        case HttpStatus.NotFound:
            return "Resource not found";
        default:
            return "An error occurred";
    }
}

console.log(handleResponse(HttpStatus.OK)); // "Success!"
Days of Week
enum DayOfWeek {
    Sunday,
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday
}

function isWeekend(day: DayOfWeek): boolean {
    return day === DayOfWeek.Saturday || day === DayOfWeek.Sunday;
}

function getWorkingHours(day: DayOfWeek): string {
    if (isWeekend(day)) {
        return "Closed";
    }
    return "9 AM - 5 PM";
}

console.log(getWorkingHours(DayOfWeek.Monday));   // "9 AM - 5 PM"
console.log(getWorkingHours(DayOfWeek.Saturday)); // "Closed"
Practice Tasks
  • Task 1: Create an enum for months of the year with numeric values.
  • Task 2: Define a string enum for user roles in your application.
  • Task 3: Build an enum for file types with appropriate string values.
  • Task 4: Create a const enum for common colors to optimize bundle size.
  • Task 5: Implement an enum for card suits in a card game.
  • Task 6: Use bitwise enum for file permissions (read/write/execute).
  • Task 7: Create an enum for HTTP methods (GET, POST, PUT, DELETE).

Key Takeaways

  • Enums create named constants: Better than magic numbers/strings
  • Numeric enums auto-increment: Start from 0 by default
  • String enums require explicit values: Must initialize all members
  • Reverse mapping: Only for numeric enums
  • Const enums: Inlined for better performance
  • Use enums for fixed sets of values: HTTP status, days, roles
  • Consider union types: When runtime object not needed

What's Next?

Next Topic: Learn about Type Assertions - how to tell TypeScript "I know better than you" about a value's type when you have more information than the compiler.



TypeScript Type Assertions: Overriding Type Inference

Learn when and how to use type assertions to tell TypeScript the specific type of a value

What are Type Assertions?

Type assertions are a way to tell the TypeScript compiler "trust me, I know what I'm doing." They let you override the compiler's inferred or assigned type when you have more specific information. Type assertions don't change the runtime behavior—they're purely a compile-time construct.

Basic Syntax: as Keyword

Using the as Keyword (Recommended)
// DOM manipulation - common use case
const input = document.getElementById("email") as HTMLInputElement;
input.value = "test@example.com"; // ✅ OK - TypeScript knows it has value property

// Without assertion
const input2 = document.getElementById("email"); // type: HTMLElement | null
// input2.value = "test"; // ❌ Error: Property 'value' does not exist

// API response with unknown type
const response: unknown = await fetch("/api/data").then(r => r.json());
const data = response as { id: number; name: string };
console.log(data.name); // ✅ OK

// String to specific format
const userId: any = "12345";
const id = userId as string;
const numericId = parseInt(id);

Angle-Bracket Syntax

Alternative Syntax (Not in JSX)
// Angle-bracket syntax - older style
const input = document.getElementById("email");
input.value = "test@example.com";

// Note: This syntax conflicts with JSX/TSX
// Always use 'as' syntax in React files

// Multiple assertions
const value = someValue;

⚠️ JSX Conflict: Use the as keyword in .tsx files because angle brackets are used for JSX elements. The as syntax is now the recommended approach everywhere.

Common Use Cases

DOM Element Type Assertions
// Specific element types
const canvas = document.getElementById("myCanvas") as HTMLCanvasElement;
const context = canvas.getContext("2d");

const video = document.querySelector("video") as HTMLVideoElement;
video.play();

const form = document.forms[0] as HTMLFormElement;
form.submit();

// Query selector with assertions
const button = document.querySelector(".submit-btn") as HTMLButtonElement;
button.disabled = false;

// Multiple element assertions
const inputs = document.querySelectorAll("input") as NodeListOf;
inputs.forEach(input => {
    console.log(input.value);
});

Type Assertions with Objects

Asserting Object Types
// Define interface
interface User {
    id: number;
    name: string;
    email: string;
}

// Assertion from generic object
const userData: object = {
    id: 1,
    name: "Alice",
    email: "alice@example.com"
};

const user = userData as User;
console.log(user.name); // ✅ OK

// Partial object assertion
const partialUser = {
    name: "Bob"
} as User; // ⚠️ Compiles but missing required properties!

// Better: Use Partial utility type
const partialUser2: Partial = {
    name: "Bob"
};

Const Assertions

as const for Immutable Values
// Without const assertion
const colors1 = ["red", "green", "blue"];
// type: string[]

// With const assertion
const colors2 = ["red", "green", "blue"] as const;
// type: readonly ["red", "green", "blue"]

colors1[0] = "yellow"; // ✅ OK
colors2[0] = "yellow"; // ❌ Error: Cannot assign to readonly

// Object with const assertion
const config = {
    apiUrl: "https://api.example.com",
    timeout: 5000
} as const;
// All properties become readonly

// config.timeout = 10000; // ❌ Error

// Useful for literal types
type Method = typeof methods[number];
const methods = ["GET", "POST", "PUT", "DELETE"] as const;

function request(method: Method) {
    // method can only be one of the array values
}

Double Assertions

When Types Are Incompatible
// Sometimes TypeScript won't allow direct assertion
const num = 42;
// const str = num as string; // ❌ Error: Conversion may be a mistake

// Double assertion via unknown or any
const str = num as unknown as string; // ⚠️ Compiles but dangerous!

// Better approach: actual conversion
const str2 = String(num); // ✅ Safe runtime conversion

// Valid use case: complex type transformations
interface Dog {
    bark(): void;
}

interface Cat {
    meow(): void;
}

const dog: Dog = {
    bark: () => console.log("Woof!")
};

// Force incompatible types (rarely needed)
const cat = dog as unknown as Cat; // ⚠️ Runtime error if you call cat.meow()

Non-Null Assertions

Using ! to Assert Non-Null
// Non-null assertion operator !
const element = document.getElementById("myElement")!;
// Tells TypeScript: "I guarantee this is not null"

// Without ! operator
const element2 = document.getElementById("myElement");
// Type: HTMLElement | null
// element2.classList.add("active"); // ❌ Error

// With ! operator
element.classList.add("active"); // ✅ OK

// Array access
const numbers = [1, 2, 3];
const first = numbers[0]!; // Asserts value exists

// Optional chaining vs non-null assertion
const user = getUser();
const name1 = user?.name; // Safe: string | undefined
const name2 = user!.name; // Dangerous: assumes user exists

⚠️ Danger Zone: Non-null assertions bypass TypeScript's safety checks. Only use them when you're absolutely certain a value exists. Runtime errors can still occur!

Type Guards vs Assertions

Feature Type Assertion Type Guard
Runtime safety ❌ No checking ✅ Validates at runtime
Compile-time only ✅ Yes ❌ Runs in JavaScript
Type narrowing ⚠️ Forces type ✅ Safe narrowing
Use case When you know better When you need to check
Type Guard Alternative
// Type assertion - no runtime check
function processInput(input: unknown) {
    const str = input as string;
    console.log(str.toUpperCase()); // ⚠️ Crashes if input isn't string
}

// Type guard - safe runtime check
function processInputSafe(input: unknown) {
    if (typeof input === "string") {
        console.log(input.toUpperCase()); // ✅ Safe
    }
}

// User-defined type guard
interface Fish {
    swim(): void;
}

interface Bird {
    fly(): void;
}

function isFish(pet: Fish | Bird): pet is Fish {
    return (pet as Fish).swim !== undefined;
}

function move(pet: Fish | Bird) {
    if (isFish(pet)) {
        pet.swim(); // TypeScript knows it's Fish
    } else {
        pet.fly(); // TypeScript knows it's Bird
    }
}

Best Practices

When to Use Type Assertions
  • ✅ DO: Use for DOM elements when you know the specific type
  • ✅ DO: Use with third-party libraries that lack type definitions
  • ✅ DO: Use as const for literal types
  • ✅ DO: Use when migrating JavaScript to TypeScript gradually
  • ❌ DON'T: Use to bypass type errors you should fix
  • ❌ DON'T: Use instead of proper type guards for validation
  • ❌ DON'T: Overuse non-null assertions (!)
  • ❌ DON'T: Use double assertions unless absolutely necessary
Practice Tasks
  • Task 1: Get a canvas element and assert it as HTMLCanvasElement.
  • Task 2: Use as const to create a readonly configuration object.
  • Task 3: Assert an API response to a specific interface type.
  • Task 4: Practice non-null assertions with array access.
  • Task 5: Compare type assertion vs type guard approaches.
  • Task 6: Create a helper function that safely asserts types.
  • Task 7: Use querySelectorAll with proper type assertion.

Key Takeaways

  • Type assertions override compiler inference: Use sparingly
  • as syntax recommended: Works everywhere, including JSX
  • Angle brackets conflict with JSX: Avoid in .tsx files
  • as const creates readonly: Useful for literal types
  • Non-null assertion (!) is dangerous: Only when absolutely certain
  • Prefer type guards: Safer than assertions for validation
  • No runtime effect: Assertions are compile-time only

What's Next?

Next Topic: Learn about Union and Intersection Types - powerful ways to combine multiple types for flexible yet type-safe code.



Union and Intersection Types: Composing Flexible Contracts

Combine types safely to model real-world data with confidence

Why Use Union and Intersection Types?

Unions model “this or that” scenarios (A | B), while intersections model “this and that” (A & B). Together they let you express flexible yet type-safe data shapes.

Union Types (|)

Basic Unions
type Status = 'pending' | 'approved' | 'rejected';

function setStatus(status: Status) {
  // autocomplete only allows the three literals
}

setStatus('approved'); // ✅
// setStatus('done');   // ❌ Error

// Value can be number or string
function toId(id: number | string): string {
  return typeof id === 'number' ? id.toString() : id;
}

Type Narrowing with Unions

Runtime Checks to Narrow Types
function format(value: string | number) {
  if (typeof value === 'string') {
    return value.toUpperCase(); // value is string here
  }
  return value.toFixed(2);      // value is number here
}

// Discriminated union
type Success = { type: 'success'; data: string };
type Failure = { type: 'error'; message: string };

type Result = Success | Failure;

function handle(result: Result) {
  if (result.type === 'success') {
    console.log(result.data);
  } else {
    console.error(result.message);
  }
}

Intersection Types (&)

Combining Multiple Contracts
type WithTimestamps = { createdAt: Date; updatedAt: Date };
type User = { id: number; name: string };

type TimestampedUser = User & WithTimestamps;

const alice: TimestampedUser = {
  id: 1,
  name: 'Alice',
  createdAt: new Date(),
  updatedAt: new Date(),
};

Intersection caveat: When properties collide with incompatible types, the result becomes never. Ensure overlapping keys are compatible.

Unions vs Intersections at a Glance

ConceptUnion (|)Intersection (&)
MeaningEither A or BBoth A and B
FlexibilityMore flexibleMore strict
Use forMultiple variantsCombining features
NarrowingRequired at runtimeNo narrowing needed

Real-World Example: Payment Methods

Modeling Variant Shapes
type CardPayment = {
  method: 'card';
  cardNumber: string;
  cvv: string;
};

type UpiPayment = {
  method: 'upi';
  upiId: string;
};

type CashPayment = {
  method: 'cash';
};

type Payment = CardPayment | UpiPayment | CashPayment;

function processPayment(p: Payment) {
  switch (p.method) {
    case 'card':
      return `Charging card ${p.cardNumber}`;
    case 'upi':
      return `Requesting UPI ${p.upiId}`;
    case 'cash':
      return 'Collecting cash';
  }
}

Mixing Both: Smart Composition

Intersection + Union
type Base = { id: string; createdAt: Date };

type Article = Base & { kind: 'article'; title: string; body: string };
type Video   = Base & { kind: 'video'; title: string; duration: number };

type Content = Article | Video;
Practice Tasks
  • Create a discriminated union for API responses: success, validationError, serverError.
  • Model a Vehicle that can be Car | Bike | Truck and narrow by a type field.
  • Build an intersection type that adds audit fields (createdBy, updatedBy) to an existing entity.
  • Experiment with incompatible intersections to see how conflicts become never.

Key Takeaways

  • Use unions for “one of many” variants; narrow with runtime checks.
  • Use intersections to combine capabilities; avoid conflicting property types.
  • Discriminated unions (tagged with type field) make narrowing simple and safe.
  • Watch for incompatible intersections that collapse to never.

What's Next?

Next Topic: Dive into Type Guards to master runtime narrowing.



Type Guards: Runtime Type Checking

Narrow types with confidence using guards and predicates

What Are Type Guards?

Type guards are code patterns that narrow a union type to a more specific type. They use runtime checks to guarantee type safety.

Built-in Type Guards

typeof Guard
function printLength(x: string | number) {
  if (typeof x === 'string') {
    console.log(x.length); // x is string
  } else {
    console.log(x.toFixed(2)); // x is number
  }
}

// Works with: string, number, boolean, symbol, undefined, object, function
instanceof Guard
class Dog {
  bark() { return 'Woof!'; }
}

class Cat {
  meow() { return 'Meow!'; }
}

function animalSound(animal: Dog | Cat): string {
  if (animal instanceof Dog) {
    return animal.bark();
  }
  return animal.meow();
}
in Operator Guard
interface Admin {
  name: string;
  role: 'admin';
}

interface User {
  name: string;
  permissions: string[];
}

function checkAccess(user: Admin | User) {
  if ('role' in user) {
    console.log(`Admin: ${user.role}`);
  } else {
    console.log(`User with ${user.permissions.length} permissions`);
  }
}

Custom Type Predicates

Type Predicate Function (is keyword)
interface Fish {
  swim(): void;
}

interface Bird {
  fly(): void;
}

function isFish(animal: Fish | Bird): animal is Fish {
  return 'swim' in animal;
}

function move(animal: Fish | Bird) {
  if (isFish(animal)) {
    animal.swim(); // type is narrowed to Fish
  } else {
    animal.fly();  // type is narrowed to Bird
  }
}

Exhaustiveness Checking

Ensure All Cases Covered
type Status = 'pending' | 'completed' | 'failed';

function handleStatus(status: Status): string {
  switch (status) {
    case 'pending':
      return 'Processing...';
    case 'completed':
      return 'Done!';
    case 'failed':
      return 'Error occurred';
    default:
      // Compiler error if Status is extended without handling
      const _exhaustive: never = status;
      return _exhaustive;
  }
}

Type Narrowing with Truthiness

Falsy Value Checks
function printValue(x: string | null | undefined) {
  if (x) {
    console.log(x.toUpperCase()); // x is string
  } else {
    console.log('No value provided');
  }
}

function validateEmail(email: string | null) {
  if (!email) return false;
  return email.includes('@'); // email is string
}

Real-World Pattern: API Response Handler

Handling Multiple Response Types
type ApiResponse =
  | { status: 'success'; data: any }
  | { status: 'error'; message: string }
  | { status: 'pending'; progress: number };

function isSuccessResponse(resp: ApiResponse): resp is { status: 'success'; data: any } {
  return resp.status === 'success';
}

function handleResponse(response: ApiResponse) {
  if (isSuccessResponse(response)) {
    console.log('Data:', response.data);
  } else if (response.status === 'error') {
    console.error('Error:', response.message);
  } else {
    console.log(`Loading: ${response.progress}%`);
  }
}
Practice Tasks
  • Write a custom type predicate for checking if a value is a valid email string.
  • Create a union of 3 types and handle each with type guards using exhaustiveness checking.
  • Implement a function that narrows unknown to a specific interface using guards.

Key Takeaways

  • Use typeof for primitives, instanceof for class instances.
  • Use in operator for interface properties.
  • Define custom predicates with is keyword for reusable guards.
  • Exhaustiveness checking ensures all union cases are handled.

What's Next?

Next Topic: Explore Async/Await for handling asynchronous operations elegantly.



Decorators: Annotate and Transform Code

Use powerful metadata and transformation patterns with decorators

Note: Decorators are an experimental feature. Enable with "experimentalDecorators": true in tsconfig.json

What Are Decorators?

Decorators are functions that annotate or modify class declarations, methods, accessors, properties, and parameters at design time.

Class Decorators

Basic Class Decorator
// Decorator function
function Sealed(constructor: Function) {
  Object.seal(constructor);
  Object.seal(constructor.prototype);
}

@Sealed
class User {
  name: string = 'John';
}

// Class receives decorator as argument
// Can't add properties to User or User.prototype

Method Decorators

Logging Decorator
function Log(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${propertyKey}`, args);
    const result = originalMethod.apply(this, args);
    console.log(`Result:`, result);
    return result;
  };
  return descriptor;
}

class Calculator {
  @Log
  add(a: number, b: number) {
    return a + b;
  }
}

const calc = new Calculator();
calc.add(2, 3); // Logs: Calling add [2, 3], Result: 5

Property Decorators

Property Validation
function Validate(target: any, propertyKey: string) {
  let value: any;

  Object.defineProperty(target, propertyKey, {
    get() { return value; },
    set(newValue: any) {
      if (newValue < 0) {
        throw new Error(`${propertyKey} cannot be negative`);
      }
      value = newValue;
    },
  });
}

class Product {
  @Validate
  price: number = 0;
}

const p = new Product();
p.price = 10; // ✅
// p.price = -5; // ❌ Error

Parameter Decorators

Required Parameter
function Required(
  target: any,
  propertyKey: string,
  parameterIndex: number
) {
  console.log(`Parameter ${parameterIndex} of ${propertyKey} is required`);
}

class User {
  setEmail(@Required  email: string) {
    this.email = email;
  }
}

// Metadata is stored; validation logic must be implemented separately

Decorator Factory Pattern

Parameterized Decorators
function Retry(times: number) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const originalMethod = descriptor.value;

    descriptor.value = async function (...args: any[]) {
      for (let i = 0; i < times; i++) {
        try {
          return await originalMethod.apply(this, args);
        } catch (error) {
          if (i === times - 1) throw error;
          console.log(`Retry ${i + 1}/${times}`);
        }
      }
    };
    return descriptor;
  };
}

class ApiClient {
  @Retry(3)
  async fetchData() {
    return fetch('/api/data');
  }
}

Real-World Example: ORM Pattern

Entity and Column Decorators
function Entity(tableName: string) {
  return (constructor: Function) => {
    Reflect.defineMetadata('table', tableName, constructor);
  };
}

function Column(options?: { type?: string; nullable?: boolean }) {
  return (target: any, propertyKey: string) => {
    Reflect.defineMetadata('column', options, target, propertyKey);
  };
}

@Entity('users')
class User {
  @Column({ type: 'int' })
  id: number;

  @Column({ type: 'string' })
  name: string;
}

// Metadata can be used for ORM operations
Practice Tasks
  • Create a logging decorator that tracks method execution time.
  • Write a caching decorator that memoizes function results.
  • Build a validation decorator for class properties.
  • Implement a throttle decorator that limits method call frequency.

Key Takeaways

  • Enable decorators in tsconfig.json with experimentalDecorators.
  • Class, method, property, and parameter decorators have different signatures.
  • Decorator factories allow parameterized configuration.
  • Use with care; they add complexity but enable powerful patterns.

What's Next?

Next Topic: Learn Modules for organizing and reusing code across files.



Modules: Code Organization and Reuse

Master ES6 modules for scalable, maintainable codebases

Why Modules?

Modules encapsulate code, manage dependencies, and prevent global namespace pollution. They're essential for large-scale applications.

Named Exports

Export Multiple Items
// math.ts
export const PI = 3.14159;

export function add(a: number, b: number): number {
  return a + b;
}

export const subtract = (a: number, b: number): number => a - b;
Import Named Exports
// app.ts
import { add, subtract, PI } from './math';

console.log(add(2, 3));      // 5
console.log(PI);             // 3.14159

// Import everything
import * as Math from './math';
console.log(Math.add(1, 2));

Default Export

Export Default
// logger.ts
class Logger {
  log(message: string) {
    console.log(`[LOG] ${message}`);
  }
}

export default Logger;

// ===

// app.ts
import Logger from './logger';

const log = new Logger();
log.log('Hello'); // [LOG] Hello

Mixing Default and Named Exports

Combined Exports
// database.ts
class Database {
  connect() { /* ... */ }
}

export default Database;
export const createConnection = () => new Database();
export const tables = ['users', 'posts'];
Import Mixed Exports
// app.ts
import Database, { createConnection, tables } from './database';

const db = new Database();
const conn = createConnection();
console.log(tables); // ['users', 'posts']

Re-exports

Barrel Pattern (index.ts)
// utils/index.ts
export { add, subtract } from './math';
export { default as Logger } from './logger';
export * as StringUtils from './strings';

// ===

// app.ts
import { add, Logger, StringUtils } from './utils';

// All imports from single file!

Renaming Imports and Exports

Aliasing
// Export with different name
export { User as AuthUser, User as DbUser };

// Import with different name
import { add as addition, subtract as subtraction } from './math';

const result = addition(5, 3);

// Rename default
import Logger as DebugLogger from './logger';

Organizing Code: Module Structure

Common Project Structure
// src/
//   models/
//     User.ts
//     Post.ts
//     index.ts (barrel)
//   services/
//     UserService.ts
//     PostService.ts
//     index.ts (barrel)
//   utils/
//     formatters.ts
//     validators.ts
//     index.ts (barrel)
//   index.ts (root barrel)

// src/models/index.ts
export { default as User } from './User';
export { default as Post } from './Post';

// app.ts
import { User, Post } from './models'; // Clean!

Module Resolution

Common strategies: "node" (CommonJS style), "bundler" (modern), "classic". Configure in tsconfig.json under compilerOptions.moduleResolution

Practice Tasks
  • Create a module with both default and named exports; import both types.
  • Organize related functions/classes into a barrel index.ts.
  • Write a utility module with pure functions and re-export from a main file.
  • Practice aliasing imports to avoid naming conflicts.

Key Takeaways

  • Use named exports for multiple exports; default export for primary entity.
  • Barrel files (index.ts) simplify imports and enable clean APIs.
  • Re-exports aggregate modules; great for organizing large codebases.
  • Alias imports to prevent conflicts and improve readability.

What's Next?

Next Topic: Learn about Utility Types to transform and manipulate existing types.



Namespaces: Module Organization (Legacy)

Understand namespaces, their uses, and modern alternatives

What Are Namespaces?

Namespaces are TypeScript's way to organize code into logical groups. They're largely replaced by ES6 modules but remain useful in specific scenarios.

Basic Namespace

Namespace Declaration
namespace Math {
  export function add(a: number, b: number): number {
    return a + b;
  }

  export function subtract(a: number, b: number): number {
    return a - b;
  }

  export const PI = 3.14159;
}

// Usage
Math.add(2, 3);     // 5
console.log(Math.PI); // 3.14159

Nested Namespaces

Hierarchical Organization
namespace App {
  export namespace Utils {
    export function formatDate(date: Date): string {
      return date.toISOString();
    }
  }

  export namespace Services {
    export class UserService {
      getUser(id: number) { /* ... */ }
    }
  }
}

// Usage
App.Utils.formatDate(new Date());
const userService = new App.Services.UserService();

Namespace Merging

Combining Multiple Declarations
// validation.ts
namespace Validation {
  export function isEmail(email: string): boolean {
    return email.includes('@');
  }
}

// utils.ts
namespace Validation {
  export function isPhone(phone: string): boolean {
    return /\\d{10}/.test(phone);
  }
}

// Both functions in one namespace
console.log(Validation.isEmail('test@example.com'));
console.log(Validation.isPhone('1234567890'));

Using Namespaces with Modules

Namespace Export
// math.ts
export namespace Math {
  export function add(a: number, b: number): number {
    return a + b;
  }
}

// app.ts
import { Math as MathNamespace } from './math';
MathNamespace.add(2, 3);

Namespaces vs ES6 Modules

Best Practice: Use ES6 modules (import/export) for new projects. Namespaces are primarily for legacy code or browser-based scripts.

FeatureNamespacesES6 Modules
ScopeGlobal namespace pollutionFile scope (isolated)
UsageBrowser globals, legacyModern standard
ToolingLimited supportExcellent support
DependenciesManual file orderingAutomatic resolution
Practice Tasks
  • Create a simple namespace with multiple functions.
  • Merge two namespace declarations.
  • Compare namespace usage with ES6 modules.
  • Understand when namespaces are still useful (browser scripts, libraries).

Key Takeaways

  • Namespaces organize code into logical groups at runtime.
  • ES6 modules are the modern standard and preferred approach.
  • Namespaces can merge declarations across files.
  • Use namespaces for legacy code compatibility, not new projects.

What's Next?

Congratulations! You've mastered TypeScript. All sections complete. Continue with Bootstrap, jQuery, or Tailwind CSS learning paths.



Async and Await: Modern Async Programming

Write asynchronous code that reads like synchronous code

Why Async/Await?

Async/await simplifies Promise handling by letting you write async code in a synchronous style, making it easier to read, write, and debug.

Async Functions

Basic Async Function
// Function always returns a Promise
async function fetchUser(id: number) {
  return { id, name: 'Alice', email: 'alice@example.com' };
}

// These are equivalent
fetchUser(1); // Promise<{id: number; name: string; email: string}>

// Usage
fetchUser(1).then(user => console.log(user));

Await Keyword

Using Await
async function getUserData(id: number) {
  // Pause until promise resolves
  const response = await fetch(`/api/users/${id}`);
  const user = await response.json();
  return user;
}

// Can only use await inside async function
// const user = await fetchUser(1); // ❌ Error

// Top-level await (modern)
await getUserData(1);

Error Handling

Try-Catch with Async/Await
async function safeGetUser(id: number) {
  try {
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    return await response.json();
  } catch (error) {
    console.error('Failed to fetch user:', error);
    return null;
  }
}

Sequential vs Parallel Operations

Sequential Awaits (slow)
async function slowProcess() {
  const user = await fetchUser(1);
  const posts = await fetchPosts(user.id);
  const comments = await fetchComments(posts[0].id);
  // Takes: sum of all times
  return { user, posts, comments };
}
Parallel Awaits (fast)
async function fastProcess() {
  const user = await fetchUser(1);
  // Start both in parallel
  const [posts, friends] = await Promise.all([
    fetchPosts(user.id),
    fetchFriends(user.id),
  ]);
  // Takes: max of the two times
  return { user, posts, friends };
}

Typing Async Functions

Type Annotations
interface User {
  id: number;
  name: string;
}

// Explicit return type
async function getUser(id: number): Promise {
  return { id, name: 'Alice' };
}

// Function type with async
type FetchFn = (id: number) => Promise;

const fetchUser: FetchFn = async (id: number) => {
  return { id, name: 'Bob' };
};

Real-World Example: Data Pipeline

Async Workflow
async function loadDashboard(userId: number) {
  try {
    // 1. Fetch user
    const user = await fetchUser(userId);
    if (!user) throw new Error('User not found');

    // 2. Fetch related data in parallel
    const [projects, tasks, reports] = await Promise.all([
      fetchProjects(userId),
      fetchTasks(userId),
      fetchReports(userId),
    ]);

    return { user, projects, tasks, reports };
  } catch (error) {
    console.error('Dashboard load failed:', error);
    throw error;
  }
}
Practice Tasks
  • Convert a Promise chain (using .then()) to async/await.
  • Write a function that retries a failed async operation up to 3 times.
  • Implement Promise.all() to fetch multiple resources in parallel.
  • Create a timeout wrapper that rejects after a specified duration.

Key Takeaways

  • async functions always return Promises.
  • await pauses execution until promise resolves; use only in async functions.
  • Use try-catch for error handling in async contexts.
  • Use Promise.all() for parallel operations; avoid unnecessary sequential awaits.

What's Next?

Next Topic: Master Decorators for annotating and modifying classes and methods.



Advanced Types: Master Complex Type Patterns

Unlock powerful patterns for sophisticated type systems

Template Literal Types

String Type Manipulation
type Color = 'red' | 'green' | 'blue';
type Size = 'small' | 'large';

// Combine literals
type ColoredSize = `${Color}-${Size}`;
// 'red-small' | 'red-large' | 'green-small' | ...

// Event names
type EventName = `on${Capitalize<'click' | 'submit'>}`;
// 'onClick' | 'onSubmit'

// Key patterns
type Getter = `get${Capitalize}`;
type GetterName = Getter<'name'>; // 'getName'

Recursive Types

Self-Referential Type Definitions
// Deeply flatten nested arrays
type DeepFlat =
  T extends Array
    ? DeepFlat
    : T;

type X = DeepFlat<[[[string]]]>; // string

// Tree structure
type TreeNode = {
  value: T;
  left?: TreeNode;
  right?: TreeNode;
};

const tree: TreeNode = {
  value: 1,
  left: { value: 2 },
  right: { value: 3, left: { value: 4 } },
};

Branded Types

Phantom Types for Type Safety
// Create distinct types from primitives
type UserId = string & { readonly __brand: 'UserId' };
type Email = string & { readonly __brand: 'Email' };

function createUserId(id: string): UserId {
  return id as UserId;
}

function sendEmail(to: Email, message: string) {
  console.log(`Sending to ${to}: ${message}`);
}

const id = createUserId('user-123');
const email = 'alice@example.com' as Email;

sendEmail(email, 'Hello'); // ✅
// sendEmail(id, 'Hello'); // ❌ Type error

Type-Level Programming: Linked Lists

Building Complex Structures
type Node = [T, ...Next];

// Build a list
type List1 = Node<1, Node<2, Node<3, []>>>;
// [1, 2, 3]

// Type-level length
type Length = T['length'];
type Len = Length; // 3

// Reverse
type Reverse<
  T extends any[],
  Acc extends any[] = []
> = T extends [infer Head, ...infer Tail]
  ? Reverse
  : Acc;

type Reversed = Reverse<[1, 2, 3]>; // [3, 2, 1]

Function Overload Signatures

Multiple Type-Safe Signatures
function process(input: string): string;
function process(input: number): number;
function process(input: string | number): string | number {
  if (typeof input === 'string') {
    return input.toUpperCase();
  }
  return input * 2;
}

const r1 = process('hello'); // string
const r2 = process(5); // number

// Generic overloads
function merge(obj1: T, obj2: U): T & U;
function merge(obj1: T, obj2: Partial): T;
function merge(obj1: any, obj2: any) {
  return { ...obj1, ...obj2 };
}

Real-World Pattern: Builder Pattern

Type-Safe Fluent API
class QueryBuilder {
  private fields: (keyof T)[] = [];

  select(field: K): QueryBuilder {
    this.fields.push(field as any);
    return this as any;
  }

  build(): T {
    return {} as T;
  }
}

const query = new QueryBuilder()
  .select('name')
  .select('email')
  .select('age')
  .build(); // { name?: any; email?: any; age?: any }
Practice Tasks
  • Create branded types for database IDs to prevent mixing different ID types.
  • Build a recursive tree type and instantiate it with sample data.
  • Write template literal types to generate CSS class name types.
  • Implement function overloads for flexible APIs.

Key Takeaways

  • Template literal types manipulate strings at the type level.
  • Recursive types enable self-referential definitions for trees and graphs.
  • Branded types create distinct types from primitives for safety.
  • Function overloads provide multiple type-safe signatures.

What's Next?

Next Topic: Explore Compiler Options and tsconfig.json configuration.



Utility Types: Transform Your Types

Master TypeScript's built-in type transformations for maximum code reuse

What Are Utility Types?

Utility types are generic type constructors that transform existing types. They enable DRY principles by letting you build new types from existing ones.

Partial <T>

Make All Properties Optional
interface User {
  id: number;
  name: string;
  email: string;
}

type UpdateUser = Partial; // All properties optional

const update: UpdateUser = { name: 'Bob' }; // ✅

// Useful for update operations
function updateUser(id: number, changes: Partial) {
  // Only update provided properties
}

Required <T>

Make All Properties Required
interface User {
  id: number;
  name?: string;
  email?: string;
}

type CompleteUser = Required; // All properties required

const user: CompleteUser = {
  id: 1,
  name: 'Alice',
  email: 'alice@example.com', // ✅
};

Pick <T, K>

Select Specific Properties
interface User {
  id: number;
  name: string;
  email: string;
  password: string;
}

type UserPreview = Pick;

const preview: UserPreview = {
  id: 1,
  name: 'Alice',
  email: 'alice@example.com',
  // password omitted
};

Omit <T, K>

Exclude Specific Properties
interface User {
  id: number;
  name: string;
  email: string;
  password: string;
}

type UserPublic = Omit;

const publicUser: UserPublic = {
  id: 1,
  name: 'Alice',
  email: 'alice@example.com',
  // password excluded
};

Record <K, T>

Create Object with Specific Keys
type Status = 'pending' | 'completed' | 'failed';

type StatusCounts = Record;

const counts: StatusCounts = {
  pending: 5,
  completed: 10,
  failed: 2,
};

// Useful for lookup maps
type UserRoles = Record<'admin' | 'user' | 'guest', string[]>;
const permissions: UserRoles = {
  admin: ['read', 'write', 'delete'],
  user: ['read', 'write'],
  guest: ['read'],
};

Exclude <T, U>

Remove Union Members
type Status = 'pending' | 'completed' | 'failed' | 'cancelled';

type TerminalStatus = Exclude; // 'completed' | 'failed' | 'cancelled'

function markDone(status: TerminalStatus) {
  // Only accepts terminal statuses
}

Extract <T, U>

Keep Only Matching Union Members
type Status = 'pending' | 'completed' | 'failed';

type SuccessStatus = Extract; // 'completed'

type StringOrNumber = Extract; // string | number

Readonly <T>

Make All Properties Readonly
interface User {
  id: number;
  name: string;
}

type ImmutableUser = Readonly;

const user: ImmutableUser = { id: 1, name: 'Alice' };
// user.name = 'Bob'; // ❌ Error

Real-World Example: API Response

Building Types from Base Type
interface UserData {
  id: number;
  email: string;
  password: string;
  createdAt: Date;
  role: string;
}

// API response (public)
type UserResponse = Pick;

// Update form (allow partial updates)
type UserUpdate = Partial>;

// Internal storage (everything)
type UserRecord = Required;
Practice Tasks
  • Create a User type and derive PublicUser using Pick or Omit.
  • Build a Settings type with all properties optional using Partial.
  • Use Record to create a lookup table for permissions by role.
  • Combine utility types: Readonly> for immutable partial objects.

Key Takeaways

  • Partial makes properties optional; Required makes them mandatory.
  • Pick includes specific keys; Omit excludes them.
  • Record creates objects with specific keys and value types.
  • Combine utility types for complex transformations.

What's Next?

Next Topic: Explore Mapped Types for creating types that transform existing types dynamically.



Mapped Types: Transform Types by Iterating Properties

Create flexible types by dynamically transforming existing ones

What Are Mapped Types?

Mapped types iterate over properties of existing types and create new types by transforming each property. They're powerful for avoiding repetition.

Basic Mapped Type

Simple Property Transform
// Make all properties getters
type Getters = {
  [K in keyof T]: () => T[K];
};

interface User {
  name: string;
  email: string;
}

type UserGetters = Getters;
// Results in:
// {
//   name: () => string;
//   email: () => string;
// }

Common Mapped Type Patterns

Making Properties Optional or Readonly
// Make all properties optional
type Optional = {
  [K in keyof T]?: T[K];
};

// Make all properties readonly
type ReadonlyVersion = {
  readonly [K in keyof T]: T[K];
};

// Wrap each value in Promise
type Promises = {
  [K in keyof T]: Promise;
};

type UserPromises = Promises;
// {
//   name: Promise;
//   email: Promise;
// }

Filtering Properties with as

Key Remapping and Filtering
// Add prefix to keys
type WithGetPrefix = {
  [K in keyof T as `get${Capitalize}`]: () => T[K];
};

interface User {
  name: string;
  age: number;
}

type UserGetters = WithGetPrefix;
// Results in:
// {
//   getName: () => string;
//   getAge: () => number;
// }

// Filter only string properties
type StringProperties = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};

type UserStrings = StringProperties; // { name: string }

Conditional Mapping

Transform Based on Type
type Flatten = {
  [K in keyof T]: T[K] extends Array ? U : T[K];
};

interface Data {
  items: string[];
  count: number;
  tags: string[];
}

type FlatData = Flatten;
// Results in:
// {
//   items: string;
//   count: number;
//   tags: string;
// }

Real-World Example: Event Handlers

Generating Handler Types
interface Events {
  click: { x: number; y: number };
  submit: { formData: any };
  error: { message: string };
}

type EventHandlers = {
  [K in keyof T]: (event: T[K]) => void;
};

type AllHandlers = EventHandlers;
// {
//   click: (event: { x: number; y: number }) => void;
//   submit: (event: { formData: any }) => void;
//   error: (event: { message: string }) => void;
// }

const handlers: AllHandlers = {
  click: (e) => console.log(e.x, e.y),
  submit: (e) => console.log(e.formData),
  error: (e) => console.error(e.message),
};

Advanced Pattern: Getters and Setters

Generate API from Model
interface User {
  id: number;
  name: string;
  email: string;
}

type Getters = {
  [K in keyof T as `get${Capitalize}`]: () => T[K];
};

type Setters = {
  [K in keyof T as `set${Capitalize}`]: (value: T[K]) => void;
};

type API = Getters & Setters;

type UserAPI = API;
// {
//   getId: () => number;
//   getName: () => string;
//   getEmail: () => string;
//   setId: (value: number) => void;
//   setName: (value: string) => void;
//   setEmail: (value: string) => void;
// }
Practice Tasks
  • Create a mapped type that makes all properties optional.
  • Build a mapped type that prefixes all property names with "is".
  • Implement a type that wraps each property value in a Promise.
  • Write a mapped type that filters and keeps only numeric properties.

Key Takeaways

  • Use keyof T to iterate over type properties.
  • Use as clause for key remapping and filtering.
  • Combine with conditional types for sophisticated transformations.
  • Mapped types eliminate duplication in type definitions.

What's Next?

Next Topic: Master Conditional Types for logic-based type selection.



Conditional Types: Logic-Based Type Selection

Use type-level conditionals to create intelligent, context-aware types

What Are Conditional Types?

Conditional types enable type-level logic using a ternary syntax: T extends U ? X : Y. They select types based on type relationships.

Basic Conditional Type

Simple Type Selection
type IsString = T extends string ? true : false;

type A = IsString<'hello'>; // true
type B = IsString; // false

// Practical example: Extract return type
type Flatten = T extends Array ? U : T;

type Str = Flatten; // string
type Num = Flatten; // number

Using infer for Type Extraction

Extract Types from Complex Structures
// Extract return type from function
type ReturnType = T extends (...args: any[]) => infer R ? R : never;

type FuncType = (x: number) => string;
type Result = ReturnType; // string

// Extract Promise inner type
type Awaited = T extends Promise ? U : T;

type P = Awaited>; // string
type N = Awaited; // number

Distributive Conditional Types

Automatic Distribution Over Unions
type IsArray = T extends Array ? true : false;

// Single type
type A = IsArray; // true

// Union: applies to EACH member
type B = IsArray; // true | false

// Filter union types
type Extract = T extends U ? T : never;

type StringOrNumber = Extract;
// Results in: string | number

Chaining Conditions

Complex Type Logic
type Flatten =
  T extends Array
    ? Flatten // Recursively flatten
    : T;

type Deep = Flatten<[[[string]]]>; // string

// Type narrowing chain
type TypeName =
  T extends string ? 'string' :
  T extends number ? 'number' :
  T extends boolean ? 'boolean' :
  T extends undefined ? 'undefined' :
  T extends Function ? 'function' :
  'object';

type T1 = TypeName<42>; // 'number'
type T2 = TypeName<{ x: 1 }>; // 'object'

Real-World Example: Flexible API

Context-Aware Return Types
type ApiCall =
  U extends void
    ? Promise
    : Promise;

// Single result
type SingleUser = ApiCall<{ id: number; name: string }>;
// Promise<{ id: number; name: string }>

// Multiple results (U provided)
type MultipleUsers = ApiCall<{ id: number; name: string }, 'multiple'>;
// Promise>

Avoiding Distribution

Non-Distributive Conditionals
// Distributive (applies to each union member)
type IsArray1 = T extends Array ? true : false;
type Result1 = IsArray1; // boolean | true

// Non-distributive (tuple prevents distribution)
type IsArray2 = [T] extends [Array] ? true : false;
type Result2 = IsArray2; // false
Practice Tasks
  • Write a conditional type that returns "primitive" or "object" based on input.
  • Create a type that extracts the inner type from a Promise, Array, or returns the type unchanged.
  • Implement distributive logic to filter a union to only string types.
  • Build a recursive flatten type that deeply unwraps nested arrays.

Key Takeaways

  • Conditional types use extends ? : syntax for type-level logic.
  • infer keyword extracts types from complex structures.
  • Distributive conditionals automatically apply to each union member.
  • Wrap in tuples to prevent distribution when needed.

What's Next?

Next Topic: Understand Type Inference to let TypeScript determine types automatically.



Type Inference: Let TypeScript Figure It Out

Reduce verbosity by letting the compiler infer types from context

What Is Type Inference?

Type inference automatically determines types based on assigned values, function returns, and context. It reduces manual type annotations without sacrificing safety.

Variable Inference

Automatic Type Detection
// Type inferred from value
const name = 'Alice'; // string
const age = 30; // number
const active = true; // boolean

const items = ['a', 'b']; // string[]
const coords = { x: 0, y: 0 }; // { x: number; y: number }

// Explicit type overrides inference
const count: number = 5;

// const assertion (literal types)
const mode = 'dev' as const; // 'dev' (not string)

Function Return Type Inference

Inferred from Return Statement
// Return type inferred
function add(a: number, b: number) {
  return a + b; // number
}

function greet(name: string) {
  if (name === 'Admin') {
    return { role: 'admin' }; // { role: string }
  }
  return { role: 'user' }; // { role: string }
}

// Complex return with multiple paths
function process(id: string | number) {
  return typeof id === 'string' ? id.toUpperCase() : id.toString();
  // string
}

Contextual Typing

Type Flows from Context
// Callback parameters inferred from context
const items = ['a', 'b', 'c'];
items.forEach(item => {
  // item is automatically inferred as string
  console.log(item.toUpperCase());
});

const squared = items.map((item, index) => {
  // item: string, index: number
  return item.length + index;
});

type Callback = (error: Error | null, data: string) => void;
const handler: Callback = (err, data) => {
  // err: Error | null, data: string (inferred)
};

Generic Type Inference

Automatic Generic Parameter Resolution
function identity(value: T): T {
  return value;
}

// T inferred from argument
const result1 = identity('hello'); // T = string
const result2 = identity(42); // T = number

function createArray(item: T): T[] {
  return [item];
}

const arr = createArray([1, 2, 3]); // T = number[]

const Assertion

Literal Types with as const
// Without as const: inferred as string
const direction1 = 'up'; // string

// With as const: inferred as literal 'up'
const direction2 = 'up' as const; // 'up'

const colors = ['red', 'green', 'blue'] as const;
// readonly ['red', 'green', 'blue']

const status = { code: 200, message: 'OK' } as const;
// {
//   readonly code: 200;
//   readonly message: 'OK';
// }

When to Annotate Explicitly

Cases Where Annotations Help
// 1. Ambiguous structure
const person = { name: 'Alice' }; // Could be more specific
const employee: { name: string; role: string } = { name: 'Alice', role: 'dev' };

// 2. Public API contracts
export function process(data: string[]): boolean {
  return data.length > 0;
}

// 3. Explicit intent
const config: { [key: string]: any } = { debug: true, port: 3000 };

// 4. Breaking type widening
let response = { status: 200 }; // { status: number }
let response2: { status: 200 } = { status: 200 }; // { status: 200 }
Practice Tasks
  • Write functions and let inference determine return types; verify with IDE.
  • Use contextual typing with callbacks in array methods (map, filter).
  • Apply as const to create literal types for configuration objects.
  • Experiment with generic functions where type parameters are inferred.

Key Takeaways

  • TypeScript infers types from values, returns, and context automatically.
  • Annotate when intent is ambiguous or for public API contracts.
  • Use as const for literal types and configuration objects.
  • Let generic types be inferred from arguments when possible.

What's Next?

Next Topic: Explore Compiler Options to configure TypeScript behavior.



Compiler Options: Tune TypeScript Behavior

Control how TypeScript compiles and validates your code

Essential Compiler Options

Compiler options in tsconfig.json control type checking strictness, output format, module resolution, and more.

Strict Mode Options

Enable Maximum Type Safety
{
  "compilerOptions": {
    "strict": true, // Enable all strict options
    "noImplicitAny": true, // Error on implicit any
    "strictNullChecks": true, // Treat null/undefined strictly
    "strictFunctionTypes": true, // Strict function parameter types
    "strictBindCallApply": true, // Strict bind/call/apply
    "strictPropertyInitialization": true, // Properties must be initialized
    "noImplicitThis": true, // Error on implicit this
    "alwaysStrict": true // Use strict mode in output
  }
}

Module and Target Options

Output Format Configuration
{
  "compilerOptions": {
    // Output target
    "target": "ES2020", // ES2015, ES2020, ES2021, ESNext

    // Module system
    "module": "ESNext", // commonjs, esnext, umd, amd, es2015

    // Module resolution
    "moduleResolution": "node", // classic, node

    // Path mapping
    "baseUrl": "./",
    "paths": {
      "@/*": ["src/*"],
      "@utils/*": ["src/utils/*"]
    }
  }
}

Output Options

Compilation Output Control
{
  "compilerOptions": {
    // Output directory
    "outDir": "./dist",
    "rootDir": "./src",

    // Source maps for debugging
    "sourceMap": true,
    "inlineSourceMap": true,

    // Declaration files
    "declaration": true,
    "declarationMap": true,

    // Remove comments, whitespace
    "removeComments": true,

    // Import helpers
    "importHelpers": true,
    "lib": ["ES2020", "DOM"]
  }
}

Type Checking Options

Enhanced Validation
{
  "compilerOptions": {
    // Strict checks
    "noUnusedLocals": true, // Warn about unused variables
    "noUnusedParameters": true, // Warn about unused parameters
    "noImplicitReturns": true, // Ensure all code paths return
    "noFallthroughCasesInSwitch": true, // Error on case fall-through

    // Looser checks (if needed)
    "allowJs": true, // Allow JavaScript files
    "checkJs": true, // Type-check JavaScript files
    "skipLibCheck": true // Skip type checking of libraries
  }
}

Practical Configuration Example

Production-Ready tsconfig.json
{
  "compilerOptions": {
    // Strict
    "strict": true,
    "noImplicitReturns": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,

    // Output
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "node",
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,

    // Libraries
    "lib": ["ES2020", "DOM"],
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "**/*.test.ts"]
}
Practice Tasks
  • Start with "strict": true and observe which code fails type checks.
  • Configure path aliases in tsconfig and use them in imports.
  • Enable noUnusedLocals and refactor code to remove unused variables.
  • Generate declaration files and explore the .d.ts output.

Key Takeaways

  • Enable strict mode for maximum type safety.
  • Configure target and module for your runtime environment.
  • Use noUnusedLocals/Parameters and noImplicitReturns for quality code.
  • Path aliases simplify imports and enable easy refactoring.

What's Next?

Next Topic: Dive into TypeScript Configuration (tsconfig.json) to master all settings.



TypeScript Configuration (tsconfig.json): Master Your Setup

Organize complex TypeScript projects with strategic configuration

tsconfig.json Structure

Complete Configuration File
{
  "compilerOptions": {
    // Strictness
    "strict": true,
    // Output
    "outDir": "./dist",
    "rootDir": "./src",
    // Modules
    "module": "ESNext",
    "target": "ES2020"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"],
  "compileOnSave": false,
  "ts-node": { "transpileOnly": true }
}

File Include/Exclude

Controlling Which Files Compile
{
  "include": [
    "src/**/*", // All files under src
    "tests/**/*.ts" // Test files
  ],
  "exclude": [
    "node_modules", // Never include dependencies
    "dist",
    "**/*.spec.ts", // Exclude test files if not wanted
    "**/node_modules/**"
  ]
}

Configuration Inheritance with extends

Share Configuration Across Configs
// tsconfig.base.json
{
  "compilerOptions": {
    "strict": true,
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "target": "ES2020",
    "module": "ESNext"
  }
}

// tsconfig.json (extends base)
{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "outDir": "./dist"
  },
  "include": ["src/**/*"]
}

// tsconfig.test.json (extends base, different settings)
{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "module": "commonjs", // Override for tests
    "sourceMap": true
  },
  "include": ["tests/**/*"]
}

Development vs Production Config

Environment-Specific Settings
// tsconfig.dev.json
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "sourceMap": true, // Include source maps
    "skipLibCheck": false, // Full checking
    "declaration": false // Don't generate .d.ts
  }
}

// tsconfig.prod.json
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "sourceMap": false,
    "skipLibCheck": true, // Speed up builds
    "declaration": true, // Generate .d.ts for library
    "removeComments": true // Smaller output
  }
}

Monorepo Configuration

Project References for Monorepos
// tsconfig.json (root)
{
  "compilerOptions": {
    "composite": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "strict": true
  },
  "files": [],
  "references": [
    { "path": "./packages/core" },
    { "path": "./packages/ui" },
    { "path": "./packages/cli" }
  ]
}

// packages/core/tsconfig.json
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*"],
  "references": []
}

Path Aliases (baseUrl)

Simplify Import Paths
{
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"],
      "@utils/*": ["src/utils/*"],
      "@types/*": ["src/types/*"],
      "@api/*": ["src/api/*"]
    }
  }
}

// In code:
// Instead of: import { Button } from '../../../components/Button'
// Use: import { Button } from '@components/Button'
Practice Tasks
  • Create a tsconfig.base.json and have tsconfig.json extend it.
  • Set up separate configs for development, production, and testing.
  • Configure path aliases and refactor imports to use them.
  • Create a monorepo structure with project references.

Key Takeaways

  • Use extends to share configuration across multiple tsconfig files.
  • Separate configs for dev, test, and prod optimize for each environment.
  • Project references enable efficient monorepo builds.
  • Path aliases make imports cleaner and refactoring easier.

What's Next?

Next Topic: Learn Declaration Files to provide types for JavaScript libraries.



Declaration Files (.d.ts): Add Types to JavaScript

Provide type information for JavaScript libraries and legacy code

What Are Declaration Files?

Declaration files (.d.ts) describe the shape and types of JavaScript code. They enable TypeScript type checking without TypeScript source files.

Creating Declaration Files

Hand-Written Declaration File
// math.d.ts
export function add(a: number, b: number): number;
export function subtract(a: number, b: number): number;
export const PI: number;

// Corresponding JavaScript (math.js)
// export function add(a, b) { return a + b; }
// export function subtract(a, b) { return a - b; }
// export const PI = 3.14159;

Declaring Classes

Class Type Definitions
// database.d.ts
export class Database {
  constructor(connectionString: string);

  connect(): Promise;
  disconnect(): Promise;
  query(sql: string, params?: any[]): Promise;
  close(): void;
}

export interface QueryResult {
  rowCount: number;
  rows: any[];
}

Ambient Declarations

Declaring Global Types
// globals.d.ts
declare global {
  interface Window {
    appConfig: { apiUrl: string; debug: boolean };
    gtag: (command: string, ...args: any[]) => void;
  }

  namespace NodeJS {
    interface ProcessEnv {
      API_KEY: string;
      DATABASE_URL: string;
    }
  }
}

// Now available everywhere
const config = window.appConfig;
const apiKey = process.env.API_KEY;

Generic Type Declarations

Flexible Type Definitions
// api.d.ts
export interface ApiResponse {
  success: boolean;
  data?: T;
  error?: string;
}

export function fetchData(
  endpoint: string,
  options?: RequestInit
): Promise>;

export class ApiClient {
  baseUrl: string;
  get(path: string): Promise;
  post(path: string, body: any): Promise;
}

Namespace and Module Augmentation

Extending Existing Types
// express-extensions.d.ts
// Augment Express Request type
declare global {
  namespace Express {
    interface Request {
      user?: { id: string; role: string };
      correlationId?: string;
    }
  }
}

// lodash-extensions.d.ts
// Add custom lodash method
import * as _ from 'lodash';

declare module 'lodash' {
  function customSort(arr: any[], key: string): any[];
}

Auto-Generating Declaration Files

TypeScript Configuration for .d.ts Generation
// tsconfig.json
{
  "compilerOptions": {
    "declaration": true, // Generate .d.ts files
    "declarationMap": true, // Include source maps
    "declarationDir": "./types", // Output directory
    "emitDeclarationOnly": false // Also emit JS
  }
}

// package.json
{
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "typings": "dist/index.d.ts" // Alternative
}

Publishing Types to DefinitelyTyped

For External Libraries
// DefinitelyTyped/types/my-library/index.d.ts
export interface Options {
  timeout?: number;
  debug?: boolean;
}

export function createInstance(options?: Options): Instance;

export class Instance {
  start(): void;
  stop(): void;
  on(event: string, callback: Function): void;
}

// Users install via npm
// npm install @types/my-library
Practice Tasks
  • Write a .d.ts file for a simple JavaScript module.
  • Create ambient declarations for global variables in your project.
  • Enable declaration: true in tsconfig and auto-generate types from TypeScript source.
  • Use module augmentation to extend existing library types.

Key Takeaways

  • Declaration files provide type information for JavaScript code.
  • Ambient declarations add types to globals and external libraries.
  • TypeScript can auto-generate .d.ts files; set declaration: true.
  • Module augmentation extends existing type definitions safely.

What's Next?

Next Topic: Learn Testing TypeScript with Jest and best practices.



Testing TypeScript: Jest and Best Practices

Write reliable tests for TypeScript code with confidence

Setting Up Jest with TypeScript

Jest Configuration
npm install --save-dev jest @types/jest ts-jest typescript
jest.config.js
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['/src'],
  testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
  moduleFileExtensions: ['ts', 'js'],
  collectCoverageFrom: [
    'src/**/*.ts',
    '!src/**/*.d.ts',
    '!src/**/*.test.ts',
  ],
};

Basic Testing Pattern

Simple Unit Test
// math.ts
export function add(a: number, b: number): number {
  return a + b;
}

export function multiply(a: number, b: number): number {
  return a * b;
}

// math.test.ts
import { add, multiply } from './math';

describe('Math functions', () => {
  it('should add two numbers', () => {
    expect(add(2, 3)).toBe(5);
  });

  it('should multiply two numbers', () => {
    expect(multiply(2, 3)).toBe(6);
  });
});

Testing Classes and Types

Class Method Tests
// User.ts
export class User {
  constructor(private name: string, private email: string) {}

  getName(): string {
    return this.name;
  }

  isValidEmail(): boolean {
    return this.email.includes('@');
  }
}

// User.test.ts
import { User } from './User';

describe('User class', () => {
  let user: User;

  beforeEach(() => {
    user = new User('Alice', 'alice@example.com');
  });

  it('should return the name', () => {
    expect(user.getName()).toBe('Alice');
  });

  it('should validate email', () => {
    expect(user.isValidEmail()).toBe(true);
  });
});

Mocking and Spying

Mock External Dependencies
// api.ts
export async function fetchUser(id: number) {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
}

// api.test.ts
import * as api from './api';

describe('API functions', () => {
  it('should fetch user data', async () => {
    // Mock fetch
    global.fetch = jest.fn(() =>
      Promise.resolve({
        json: () => Promise.resolve({ id: 1, name: 'Alice' }),
      })
    ) as jest.Mock;

    const user = await api.fetchUser(1);
    expect(user.name).toBe('Alice');
    expect(fetch).toHaveBeenCalledWith('/api/users/1');
  });
});

Testing Async Code

Async/Await and Promises
// service.ts
export async function processData(data: string[]): Promise {
  return new Promise((resolve) => {
    setTimeout(() => resolve(data.length), 100);
  });
}

// service.test.ts
import { processData } from './service';

describe('Async service', () => {
  it('should process data asynchronously', async () => {
    const result = await processData(['a', 'b', 'c']);
    expect(result).toBe(3);
  });

  // Alternative: return promise
  it('should work with return', () => {
    return processData(['x', 'y']).then((result) => {
      expect(result).toBe(2);
    });
  });
});

Testing with Type Safety

Generic Test Helpers
// testUtils.ts
export function expectType(value: T, expected: T): void {
  expect(value).toEqual(expected);
}

// Usage
type User = { id: number; name: string };
const user: User = { id: 1, name: 'Alice' };
expectType(user, { id: 1, name: 'Alice' });

// Mock with types
export function createMock(partial: Partial): T {
  return partial as T;
}

const mockUser = createMock({ name: 'Bob' });

Test Coverage

Running Tests with Coverage
npm test -- --coverage

# Output
# --------|----------|----------|----------|----------|---|
# File    | % Stmts | % Branch | % Funcs | % Lines |
# --------|----------|----------|----------|----------|---|
# math.ts | 100     | 100      | 100     | 100     |
Practice Tasks
  • Set up Jest with ts-jest in a new project.
  • Write unit tests for a class with multiple methods.
  • Mock an external API call using jest.fn().
  • Test async functions with async/await.
  • Run tests with coverage and aim for >80% coverage.

Key Takeaways

  • Use ts-jest preset for seamless TypeScript testing in Jest.
  • Mock external dependencies to isolate code under test.
  • Use async/await for testing async code.
  • Aim for high coverage but prioritize meaningful tests.

What's Next?

Next Topic: Learn Testing and Linting together for code quality.



Migrating to TypeScript: Gradual Adoption

Convert existing JavaScript projects to TypeScript incrementally

Why Migrate?

TypeScript provides early error detection, better tooling, self-documenting code, and easier refactoring—all while maintaining JavaScript flexibility.

Phase 1: Setup and Configuration

Initial tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "esnext",
    "allowJs": true,
    "checkJs": false,
    "strict": false
  }
}

Phase 2: Convert Files Incrementally

Recommended Approach
// Step 1: Setup tsconfig with allowJs
// Step 2: Add @types  packages for dependencies
// npm install --save-dev @types/node

// Step 3: Convert utilities first (leaf modules)
// utils/formatters.ts
export function formatDate(date: Date): string {
  return date.toISOString();
}

Key Takeaways

  • Start with allowJs: true and strict: false.
  • Convert modules from leaf to root (bottom-up approach).
  • Incrementally enable stricter compiler options over time.

What's Next?

Next Topic: Master TypeScript with React for modern web development.



TypeScript Best Practices: Writing Professional Code

Master the patterns and practices that lead to maintainable, type-safe TypeScript applications

Essential TypeScript Best Practices

Following TypeScript best practices ensures your code is type-safe, maintainable, and leverages the full power of the type system. These guidelines are drawn from years of community experience and official recommendations.

1. Always Enable Strict Mode

tsconfig.json Configuration
{
  "compilerOptions": {
    "strict": true,  // Enable all strict checks

    // Or enable individually:
    "strictNullChecks": true,        // Prevent null/undefined errors
    "strictFunctionTypes": true,     // Strict function parameter checking
    "strictBindCallApply": true,     // Strict bind/call/apply
    "strictPropertyInitialization": true,  // Check class properties initialized
    "noImplicitThis": true,          // Require explicit 'this' types
    "noImplicitAny": true,           // Ban implicit any types
    "alwaysStrict": true             // Parse in strict mode
  }
}

2. Avoid the any Type

Better Alternatives to any
// ❌ BAD: Defeats purpose of TypeScript
function process(data: any) {
    return data.value; // No type safety
}

// ✅ GOOD: Use unknown when type is truly unknown
function processSafe(data: unknown) {
    if (typeof data === "object" && data !== null && "value" in data) {
        return (data as { value: any }).value;
    }
}

// ✅ GOOD: Use generics for flexibility with safety
function processGeneric(data: T): T {
    return data;
}

// ✅ GOOD: Use union types for known possibilities
function processUnion(data: string | number | boolean) {
    // Handle each type
}

// ✅ GOOD: Define proper interfaces
interface Data {
    value: string;
    count: number;
}

function processTyped(data: Data) {
    return data.value; // Full type safety
}

3. Leverage Type Inference

Let TypeScript Infer When Obvious
// ❌ BAD: Redundant type annotations
const name: string = "Alice";
const age: number = 25;
const items: number[] = [1, 2, 3];

// ✅ GOOD: Let TypeScript infer obvious types
const name = "Alice";      // inferred as string
const age = 25;            // inferred as number
const items = [1, 2, 3];   // inferred as number[]

// ✅ GOOD: Annotate when helpful
let userId: string | number; // Can't infer without value
userId = "abc123";
userId = 42;

// ✅ GOOD: Always annotate function parameters
function greet(name: string, age: number) {
    return `Hello ${name}, you are ${age}`;
}

// ✅ GOOD: Annotate return types for clarity
function calculateTotal(items: number[]): number {
    return items.reduce((sum, item) => sum + item, 0);
}

4. Use Interfaces for Object Shapes

Interfaces vs Type Aliases
// ✅ GOOD: Use interfaces for objects
interface User {
    id: number;
    name: string;
    email: string;
}

// ✅ GOOD: Interfaces can be extended
interface AdminUser extends User {
    permissions: string[];
}

// ✅ GOOD: Use type aliases for unions/primitives
type Status = "pending" | "approved" | "rejected";
type ID = string | number;

// ✅ GOOD: Type aliases for complex types
type AsyncResult = Promise<{ data: T; error: null } | { data: null; error: Error }>;

// ❌ AVOID: Type alias for simple objects (use interface)
type UserType = {
    id: number;
    name: string;
};

5. Use Readonly and Const Appropriately

Immutability Patterns
// ✅ GOOD: Readonly for immutable properties
interface Config {
    readonly apiUrl: string;
    readonly apiKey: string;
    timeout?: number;
}

// ✅ GOOD: Readonly arrays
function processItems(items: readonly string[]) {
    // items.push("new"); // ❌ Error: readonly
    return items.map(item => item.toUpperCase()); // ✅ OK
}

// ✅ GOOD: as const for literal types
const ROUTES = {
    HOME: "/",
    ABOUT: "/about",
    CONTACT: "/contact"
} as const;

type Route = typeof ROUTES[keyof typeof ROUTES];

// ✅ GOOD: Readonly utility type
type ReadonlyUser = Readonly;

// ✅ GOOD: ReadonlyArray
const numbers: ReadonlyArray = [1, 2, 3];

6. Prefer Union Types Over Enums

Union Types vs Enums
// ✅ GOOD: Union types (no runtime cost)
type Status = "pending" | "approved" | "rejected";

function updateStatus(status: Status) {
    // Full autocomplete and type safety
}

// ✅ GOOD: Const object with as const
const STATUS = {
    PENDING: "pending",
    APPROVED: "approved",
    REJECTED: "rejected"
} as const;

type StatusValue = typeof STATUS[keyof typeof STATUS];

// ⚠️ OK: Enums when you need iteration or reverse mapping
enum HttpMethod {
    GET = "GET",
    POST = "POST",
    PUT = "PUT",
    DELETE = "DELETE"
}

// Benefit: Can iterate
Object.values(HttpMethod).forEach(method => {
    console.log(method);
});

7. Use Type Guards for Runtime Checking

Safe Runtime Type Checking
// ✅ GOOD: User-defined type guards
interface Dog {
    bark(): void;
}

interface Cat {
    meow(): void;
}

function isDog(pet: Dog | Cat): pet is Dog {
    return (pet as Dog).bark !== undefined;
}

function makeSound(pet: Dog | Cat) {
    if (isDog(pet)) {
        pet.bark(); // TypeScript knows it's Dog
    } else {
        pet.meow(); // TypeScript knows it's Cat
    }
}

// ✅ GOOD: Built-in type guards
function processValue(value: string | number) {
    if (typeof value === "string") {
        return value.toUpperCase();
    }
    return value.toFixed(2);
}

// ✅ GOOD: instanceof for classes
class CustomError extends Error {}

function handleError(error: Error | CustomError) {
    if (error instanceof CustomError) {
        // Handle custom error
    }
}

8. Avoid Type Assertions

Prefer Type Guards Over Assertions
// ❌ BAD: Type assertion without validation
function getUser(id: string): User {
    const response = fetch(`/api/users/${id}`);
    return response as User; // Dangerous!
}

// ✅ GOOD: Proper validation
function getUserSafe(id: string): User {
    const response = fetch(`/api/users/${id}`);

    if (!isUser(response)) {
        throw new Error("Invalid user data");
    }

    return response;
}

function isUser(obj: any): obj is User {
    return (
        typeof obj === "object" &&
        typeof obj.id === "number" &&
        typeof obj.name === "string" &&
        typeof obj.email === "string"
    );
}

// ✅ GOOD: Use assertions only when absolutely necessary
const input = document.getElementById("email") as HTMLInputElement;

9. Use Utility Types

Leverage Built-in Utility Types
interface User {
    id: number;
    name: string;
    email: string;
    password: string;
}

// ✅ GOOD: Partial for optional properties
type UserUpdate = Partial;

// ✅ GOOD: Pick for specific properties
type UserPublic = Pick;

// ✅ GOOD: Omit to exclude properties
type UserWithoutPassword = Omit;

// ✅ GOOD: Required for making all properties required
type RequiredUser = Required>;

// ✅ GOOD: Readonly for immutability
type ImmutableUser = Readonly;

// ✅ GOOD: Record for dictionaries
type UserCache = Record;

10. Organize Code with Modules

Proper Module Structure
// ✅ GOOD: Named exports for multiple items
// user.types.ts
export interface User {
    id: number;
    name: string;
}

export interface AdminUser extends User {
    permissions: string[];
}

// ✅ GOOD: Default export for primary export
// UserService.ts
export default class UserService {
    getUser(id: number): User {
        // Implementation
    }
}

// ✅ GOOD: Barrel exports for cleaner imports
// index.ts
export * from "./user.types";
export * from "./user.service";
export { default as UserService } from "./UserService";

// Usage
import { User, AdminUser, UserService } from "./users";

Configuration Best Practices

Setting Recommended Value Why
strict true Enables all strict checking options
noImplicitAny true Catches missing type annotations
strictNullChecks true Prevents null/undefined errors
esModuleInterop true Better CommonJS compatibility
skipLibCheck true Faster compilation
forceConsistentCasingInFileNames true Prevents case-sensitivity issues
Practice Tasks
  • Task 1: Enable strict mode in tsconfig.json and fix all errors.
  • Task 2: Refactor code to eliminate all any types.
  • Task 3: Replace type assertions with proper type guards.
  • Task 4: Use utility types to create derived types from base interfaces.
  • Task 5: Add readonly modifiers to prevent mutation.
  • Task 6: Convert enum to union type for better tree-shaking.
  • Task 7: Organize a project with proper module structure.

Common Anti-Patterns to Avoid

❌ Things to Avoid
  • Using any everywhere: Defeats TypeScript's purpose
  • Type assertions without validation: Runtime errors waiting to happen
  • Non-null assertions (!) carelessly: Bypasses safety checks
  • Disabling strict mode: Misses many type errors
  • Over-complicated types: Keep types simple and readable
  • Ignoring compiler errors: @ts-ignore is a last resort
  • Not leveraging inference: Redundant annotations clutter code

Key Takeaways

  • Enable strict mode always: Catches more errors early
  • Avoid any, use unknown: Maintain type safety
  • Leverage type inference: Let TypeScript do the work
  • Prefer interfaces for objects: Better for extension
  • Use readonly for immutability: Prevent accidental mutations
  • Type guards over assertions: Runtime safety matters
  • Utility types are your friends: Built-in helpers for common patterns
  • Organize with modules: Clean, maintainable structure

What's Next?

Next Topic: Congratulations on completing the TypeScript tutorial! Review the sections, practice the concepts, and start building type-safe applications. Consider exploring React with TypeScript or Node.js with TypeScript next.



TypeScript with React: Type-Safe Components

Build robust React applications with full type safety

Typing React Components

Function Component with Props
import React, { FC, ReactNode } from 'react';

interface ButtonProps {
  onClick: () => void;
  children: ReactNode;
  disabled?: boolean;
}

const Button: FC = ({
  onClick,
  children,
  disabled = false,
}) => {
  return (
    
  );
};

Typing Hooks

useState and useCallback
import React, { useState, useCallback } from 'react';

interface User {
  id: number;
  name: string;
}

const UserForm: React.FC = () => {
  const [user, setUser] = useState(null);
  const [count, setCount] = useState(0);

  const handleClick = useCallback((e: React.MouseEvent) => {
    e.preventDefault();
    setCount(prev => prev + 1);
  }, []);

  return ;
};

Key Takeaways

  • Use interfaces for component props.
  • Explicitly type useState generics.
  • Type React events properly.
  • Leverage generics for reusable components.

What's Next?

Next Topic: Master TypeScript with Node.js for backend development.



TypeScript with Node.js: Backend Type Safety

Build scalable, maintainable server applications with TypeScript

Project Setup

Initialize Node.js Project
npm init -y
npm install --save express
npm install --save-dev typescript ts-node @types/node @types/express nodemon

Express with TypeScript

Basic Express Server
import express, { Request, Response } from 'express';

const app = express();
const port = process.env.PORT || 3000;

app.use(express.json());

app.get('/health', (req: Request, res: Response) => {
  res.json({ status: 'ok' });
});

app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

Request and Response Typing

Type Route Handlers
import { Router, Request, Response } from 'express';

interface User {
  id: number;
  name: string;
  email: string;
}

const router = Router();

router.post('/users', (req: Request<{}, {}, User>, res: Response) => {
  const { name, email } = req.body;
  const user: User = { id: 1, name, email };
  res.json(user);
});

export default router;

Middleware with Types

Custom Typed Middleware
import { Request, Response, NextFunction, RequestHandler } from 'express';

interface AuthRequest extends Request {
  user?: { id: number; role: string };
}

const authMiddleware: RequestHandler = (
  req: AuthRequest,
  res: Response,
  next: NextFunction
) => {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  req.user = { id: 1, role: 'admin' };
  next();
};

Key Takeaways

  • Use ts-node for development.
  • Extend Express Request for custom properties.
  • Type route parameters and request bodies.
  • Create custom error handling middleware.

What's Next?

Next Topic: Continue learning with Bootstrap sections.

Last updated: February 2026