Skip to main content

🎯 Advanced Types

Welcome to the grand finale of TypeScript Fundamentals! You've built a solid foundation - now it's time to unlock TypeScript's true superpowers. Advanced types are where TypeScript transforms from "helpful" to "indispensable." These tools will let you express complex ideas with precision, catch bugs before they happen, and write code that practically documents itself. Get ready to level up! 🚀

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Use union and intersection types to model complex data structures
  • Implement type guards for runtime type safety
  • Create generic functions and components for maximum reusability
  • Master utility types to transform existing types
  • Write conditional types for advanced type logic
  • Use template literal types for type-safe string manipulation

Estimated Time: 75-90 minutes

Project: Build a type-safe data processor showcasing all advanced type features

📑 In This Lesson

🔀 Union Types

Union types let a value be one of several types. Think of it like a restaurant menu where you choose "soup OR salad" - the value must be one option, but you get to choose which. This is incredibly useful when a value can legitimately have different shapes in different contexts.

📖 Definition

Union Type: A type formed from two or more types, representing values that may be any one of those types. We use the pipe symbol | to create unions.

Basic Union Types

Let's start with simple examples:

// A value that can be a string OR a number
let id: string | number;

id = "abc123";      // ✅ Valid
id = 42;            // ✅ Valid
id = true;          // ❌ Error: Type 'boolean' is not assignable

// Union with literal types - perfect for status values
type Status = "pending" | "approved" | "rejected";

let orderStatus: Status;
orderStatus = "pending";    // ✅ Valid
orderStatus = "shipped";    // ❌ Error: not in union

// Function accepting union type
function formatId(id: string | number): string {
    return `ID: ${id}`;
}

console.log(formatId("ABC"));  // "ID: ABC"
console.log(formatId(123));    // "ID: 123"

Working with Union Types

When you have a union type, TypeScript only lets you access properties that exist on all types in the union:

interface Bird {
    fly(): void;
    layEggs(): void;
}

interface Fish {
    swim(): void;
    layEggs(): void;
}

function getPet(): Bird | Fish {
    // ... returns either a Bird or Fish
    return { layEggs: () => console.log("Laying eggs") } as Bird;
}

let pet = getPet();
pet.layEggs();  // ✅ OK - both Bird and Fish have layEggs()
// pet.fly();   // ❌ Error - Fish doesn't have fly()
// pet.swim();  // ❌ Error - Bird doesn't have swim()

Discriminated Unions (Tagged Unions)

One of the most powerful patterns in TypeScript! Add a common property to distinguish between union members:

// Discriminated union for payment methods
type PaymentMethod = 
    | { type: "credit_card"; cardNumber: string; cvv: string }
    | { type: "paypal"; email: string }
    | { type: "bank_transfer"; accountNumber: string; routingNumber: string };

function processPayment(payment: PaymentMethod): string {
    // TypeScript knows which properties are available based on 'type'
    switch (payment.type) {
        case "credit_card":
            return `Processing credit card ending in ${payment.cardNumber.slice(-4)}`;
        case "paypal":
            return `Processing PayPal payment for ${payment.email}`;
        case "bank_transfer":
            return `Processing bank transfer from account ${payment.accountNumber}`;
    }
}

// Usage with full type safety
const payment: PaymentMethod = {
    type: "credit_card",
    cardNumber: "1234-5678-9012-3456",
    cvv: "123"
};

console.log(processPayment(payment));

✅ Pro Tip: Discriminated Unions in React

Discriminated unions are perfect for React component props! They let you create components with different "modes" that have different required props. For example, a Button component that's either a regular button, a link, or a submit button - each with its own specific props.

graph LR A[Union Type] --> B[Option 1: string] A --> C[Option 2: number] A --> D[Option 3: boolean] style A fill:#667eea,stroke:#333,stroke-width:2px,color:#fff style B fill:#f0f0f0 style C fill:#f0f0f0 style D fill:#f0f0f0

🎨 Interactive: Union Type Selector

Click on different type options to see how a union type works. The value can be any one of these types:

value string number boolean Current Type: string | number | boolean | |

Click a type option to select it

🔗 Intersection Types

While union types represent "OR" logic, intersection types represent "AND" logic. An intersection combines multiple types into one. Think of it like creating a superhero with multiple powers - they need ALL the abilities! 🦸‍♂️

📖 Definition

Intersection Type: A type that combines multiple types. A value must satisfy ALL the combined types. We use the ampersand symbol & to create intersections.

Basic Intersection Types

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

interface Employee {
    employeeId: string;
    department: string;
}

// Intersection type - must have ALL properties from both types
type StaffMember = Person & Employee;

const john: StaffMember = {
    name: "John Doe",
    age: 30,
    employeeId: "EMP001",
    department: "Engineering"
};

// Missing any property would be an error
const jane: StaffMember = {
    name: "Jane Smith",
    age: 28
    // ❌ Error: Missing employeeId and department
};

Combining Multiple Types

Intersection types really shine when combining multiple behaviors:

// Base props for all components
interface BaseProps {
    id: string;
    className?: string;
}

// Props specific to clickable elements
interface Clickable {
    onClick: () => void;
    disabled?: boolean;
}

// Props specific to form inputs
interface FormField {
    name: string;
    value: string;
    onChange: (value: string) => void;
}

// Combine them for a clickable form input!
type ClickableInput = BaseProps & Clickable & FormField;

// Must provide ALL required properties
const inputProps: ClickableInput = {
    id: "user-input",
    className: "fancy-input",
    name: "username",
    value: "John",
    onChange: (val) => console.log(val),
    onClick: () => console.log("Clicked!"),
    disabled: false
};

Union + Intersection = Power! 💪

Combine both concepts for sophisticated type definitions:

type Admin = {
    role: "admin";
    permissions: string[];
};

type User = {
    role: "user";
    lastLogin: Date;
};

type Guest = {
    role: "guest";
};

// Union of all account types
type Account = Admin | User | Guest;

// Add common properties to all accounts
type TrackedAccount = Account & {
    id: string;
    email: string;
    createdAt: Date;
};

// Now every account must have id, email, createdAt
// PLUS the properties from their specific role type
const adminAccount: TrackedAccount = {
    id: "001",
    email: "admin@example.com",
    createdAt: new Date(),
    role: "admin",
    permissions: ["read", "write", "delete"]
};
Feature Union Types (|) Intersection Types (&)
Logic OR - can be one of several types AND - must have all properties
Properties Only common properties accessible All properties from all types
Use Case Multiple possible shapes Combining multiple behaviors
Example string | number Person & Employee
React Use Component variants/modes Extending component props
graph TD A[Intersection Type] --> B[Type 1 Properties] A --> C[Type 2 Properties] A --> D[Type 3 Properties] E[Result] --> B E --> C E --> D style A fill:#667eea,stroke:#333,stroke-width:2px,color:#fff style E fill:#4CAF50,stroke:#333,stroke-width:2px,color:#fff

🎨 Interactive: Union vs Intersection Comparison

This visualization shows the key difference between Union (|) and Intersection (&) types using a Venn diagram approach:

Union Type (A | B)

Value can be A OR B

A B ✅ Entire shaded area valid
Intersection Type (A & B)

Value must be A AND B

A B ✅ Only overlap is valid

Key Insight: With unions, you get fewer guaranteed properties (only those shared by all types). With intersections, you get more properties (all properties from all types combined).

🛡️ Type Guards and Type Narrowing

Type guards are like security checkpoints for your code. They help TypeScript understand what type a value is at runtime, allowing you to safely access type-specific properties. This process is called "type narrowing" - taking a broader type and narrowing it down to something more specific. 🔍

📖 Definition

Type Guard: A runtime check that narrows the type of a variable within a conditional block. Type guards help TypeScript understand what type you're working with at any given moment.

Built-in Type Guards

TypeScript recognizes several built-in type guards automatically:

typeof Type Guard

Perfect for primitive types:

function processValue(value: string | number) {
    if (typeof value === "string") {
        // TypeScript knows value is a string here
        console.log(value.toUpperCase());
        console.log(value.length);
    } else {
        // TypeScript knows value is a number here
        console.log(value.toFixed(2));
        console.log(value * 2);
    }
}

processValue("hello");  // "HELLO", 5
processValue(42);       // "42.00", 84

instanceof Type Guard

Great for checking class instances:

class Dog {
    bark() {
        return "Woof!";
    }
}

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

function makeSound(animal: Dog | Cat) {
    if (animal instanceof Dog) {
        // TypeScript knows animal is a Dog
        console.log(animal.bark());
    } else {
        // TypeScript knows animal is a Cat
        console.log(animal.meow());
    }
}

makeSound(new Dog());  // "Woof!"
makeSound(new Cat());  // "Meow!"

in Operator Type Guard

Check if a property exists on an object:

interface Car {
    drive(): void;
    wheels: number;
}

interface Boat {
    sail(): void;
    propeller: boolean;
}

function operateVehicle(vehicle: Car | Boat) {
    if ("drive" in vehicle) {
        // TypeScript knows vehicle is a Car
        console.log(`Driving with ${vehicle.wheels} wheels`);
        vehicle.drive();
    } else {
        // TypeScript knows vehicle is a Boat
        console.log(`Sailing with propeller: ${vehicle.propeller}`);
        vehicle.sail();
    }
}

Custom Type Guards

Create your own type guard functions using type predicates. This is where things get really powerful! 💪

interface Success {
    status: "success";
    data: any;
}

interface Error {
    status: "error";
    message: string;
}

type Response = Success | Error;

// Custom type guard function with type predicate
function isSuccess(response: Response): response is Success {
    return response.status === "success";
}

function handleResponse(response: Response) {
    if (isSuccess(response)) {
        // TypeScript knows response is Success
        console.log("Data:", response.data);
    } else {
        // TypeScript knows response is Error
        console.log("Error:", response.message);
    }
}

🏋️ Real-World Example: Form Validation

interface EmailField {
    type: "email";
    value: string;
    domain: string;
}

interface PhoneField {
    type: "phone";
    value: string;
    countryCode: string;
}

interface TextField {
    type: "text";
    value: string;
    maxLength: number;
}

type FormField = EmailField | PhoneField | TextField;

// Type guard functions
function isEmailField(field: FormField): field is EmailField {
    return field.type === "email";
}

function isPhoneField(field: FormField): field is PhoneField {
    return field.type === "phone";
}

// Validation function with type safety
function validateField(field: FormField): boolean {
    if (isEmailField(field)) {
        // Can safely access email-specific properties
        return field.value.includes("@") && field.domain.length > 0;
    } else if (isPhoneField(field)) {
        // Can safely access phone-specific properties
        return field.value.length >= 10 && field.countryCode.length > 0;
    } else {
        // Must be TextField
        return field.value.length <= field.maxLength;
    }
}

// Usage
const emailField: EmailField = {
    type: "email",
    value: "user@example.com",
    domain: "example.com"
};

console.log(validateField(emailField));  // true

⚠️ Watch Out: Type Guards Are Runtime Checks

Type guards only work at runtime! TypeScript uses them for compile-time type checking, but the actual checking happens when your code runs. Make sure your type guard logic accurately reflects the runtime structure of your data, or you might have type safety issues.

Truthiness Narrowing

TypeScript also narrows types based on truthiness checks:

function printName(name: string | null | undefined) {
    if (name) {
        // TypeScript narrows to string
        console.log(name.toUpperCase());
    } else {
        // TypeScript knows name is null or undefined
        console.log("No name provided");
    }
}

// Also works with arrays
function processItems(items: string[] | null) {
    if (items && items.length > 0) {
        // TypeScript knows items is string[] and not empty
        items.forEach(item => console.log(item));
    }
}
flowchart TD A["Union Type: Bird or Fish"] --> B["Type Guard Check"] B -->|"is Bird"| C["Bird type: can fly, layEggs"] B -->|"is Fish"| D["Fish type: can swim, layEggs"] style A fill:#667eea,stroke:#333,stroke-width:2px,color:#fff style B fill:#ffc107,stroke:#333,stroke-width:2px style C fill:#4CAF50,stroke:#333,stroke-width:2px,color:#fff style D fill:#2196F3,stroke:#333,stroke-width:2px,color:#fff

🎨 Interactive: Type Narrowing Funnel

Watch how TypeScript narrows types through type guards. Click the buttons to see different narrowing paths:

Click a type guard to see narrowing in action

🎁 Generics

Generics are one of the most powerful features in TypeScript. They allow you to write reusable code that works with multiple types while maintaining type safety. Think of generics as "type variables" - placeholders that get filled in when you use the code. This is the secret sauce that makes TypeScript libraries so flexible and type-safe! 📦

📖 Definition

Generics: Type parameters that allow you to write code that works with multiple types. They're like function parameters, but for types instead of values. We use angle brackets <T> to define them.

Why Generics?

Imagine creating a function that returns the first element of an array. Without generics, you'd need separate functions for each type:

// Without generics - not DRY! 😢
function firstString(arr: string[]): string {
    return arr[0];
}

function firstNumber(arr: number[]): number {
    return arr[0];
}

function firstBoolean(arr: boolean[]): boolean {
    return arr[0];
}

// With generics - one function for all types! 🎉
function first<T>(arr: T[]): T {
    return arr[0];
}

const firstStr = first<string>(["a", "b", "c"]);      // Type: string
const firstNum = first<number>([1, 2, 3]);           // Type: number
const firstBool = first<boolean>([true, false]);     // Type: boolean

// TypeScript can often infer the generic type!
const inferredStr = first(["x", "y", "z"]);  // Type: string (inferred)

Generic Functions

Generic functions use angle brackets to define type parameters:

// Single type parameter
function identity<T>(value: T): T {
    return value;
}

identity<string>("hello");  // Returns string
identity<number>(42);       // Returns number

// Multiple type parameters
function pair<T, U>(first: T, second: U): [T, U] {
    return [first, second];
}

const stringAndNumber = pair<string, number>("age", 30);  // [string, number]
const numberAndBoolean = pair(42, true);                   // [number, boolean] - inferred!

// Generic with constraints
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}

const person = { name: "Alice", age: 30 };
const name = getProperty(person, "name");    // Type: string
const age = getProperty(person, "age");      // Type: number
// const invalid = getProperty(person, "invalid");  // ❌ Error!

✅ Pro Tip: Generic Type Inference

TypeScript is smart about inferring generic types from arguments. You usually don't need to explicitly specify the type parameter - TypeScript figures it out! This makes your code cleaner and easier to read.

Generic Interfaces and Classes

You can make interfaces and classes generic too:

// Generic interface
interface Box<T> {
    value: T;
    getValue(): T;
    setValue(value: T): void;
}

const stringBox: Box<string> = {
    value: "hello",
    getValue() { return this.value; },
    setValue(value: string) { this.value = value; }
};

const numberBox: Box<number> = {
    value: 42,
    getValue() { return this.value; },
    setValue(value: number) { this.value = value; }
};

// Generic class
class DataStore<T> {
    private data: T[] = [];

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

    remove(item: T): void {
        const index = this.data.indexOf(item);
        if (index > -1) {
            this.data.splice(index, 1);
        }
    }

    getAll(): T[] {
        return [...this.data];
    }
}

// Usage with different types
const stringStore = new DataStore<string>();
stringStore.add("apple");
stringStore.add("banana");
console.log(stringStore.getAll());  // ["apple", "banana"]

const numberStore = new DataStore<number>();
numberStore.add(1);
numberStore.add(2);
console.log(numberStore.getAll());  // [1, 2]

Generic Constraints

Sometimes you want to limit what types can be used with a generic. Use the extends keyword:

// Constraint: T must have a length property
function logLength<T extends { length: number }>(item: T): void {
    console.log(`Length: ${item.length}`);
}

logLength("hello");        // ✅ strings have length
logLength([1, 2, 3]);      // ✅ arrays have length
logLength({ length: 10 }); // ✅ object with length
// logLength(42);          // ❌ Error: numbers don't have length

// Constraint: T must be an object type
function merge<T extends object, U extends object>(obj1: T, obj2: U): T & U {
    return { ...obj1, ...obj2 };
}

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

🚀 Real-World Example: API Response Handler

// Generic API response type
interface ApiResponse<T> {
    success: boolean;
    data?: T;
    error?: string;
    timestamp: Date;
}

// Generic function to fetch data
async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
    try {
        const response = await fetch(url);
        const data = await response.json();
        
        return {
            success: true,
            data: data as T,
            timestamp: new Date()
        };
    } catch (error) {
        return {
            success: false,
            error: error.message,
            timestamp: new Date()
        };
    }
}

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

interface Product {
    id: number;
    title: string;
    price: number;
}

// Use with different types - full type safety!
async function loadUserData() {
    const response = await fetchData<User>("/api/users/1");
    
    if (response.success && response.data) {
        // TypeScript knows response.data is a User
        console.log(response.data.name);
        console.log(response.data.email);
    }
}

async function loadProductData() {
    const response = await fetchData<Product>("/api/products/1");
    
    if (response.success && response.data) {
        // TypeScript knows response.data is a Product
        console.log(response.data.title);
        console.log(response.data.price);
    }
}

Generic Defaults

You can provide default types for generic parameters:

interface Response<T = any> {
    data: T;
    status: number;
}

// Using default type
const response1: Response = { 
    data: "anything", 
    status: 200 
};

// Specifying type
const response2: Response<string> = { 
    data: "hello", 
    status: 200 
};
graph LR A[Generic Function<T>] --> B[Input: T] A --> C[Process with type T] A --> D[Output: T] B -->|string| E[Returns string] B -->|number| F[Returns number] B -->|boolean| G[Returns boolean] style A fill:#667eea,stroke:#333,stroke-width:2px,color:#fff style C fill:#764ba2,stroke:#333,stroke-width:2px,color:#fff style E fill:#4CAF50,stroke:#333,stroke-width:2px style F fill:#4CAF50,stroke:#333,stroke-width:2px style G fill:#4CAF50,stroke:#333,stroke-width:2px

🎨 Interactive: Generic "Box" Metaphor

Generics are like a box that can hold any type. The <T> is a placeholder that gets filled in when you use it. Click to put different types in the box:

interface Box<T> { value: T } Box<T> ? string number User Resulting Type: Box<T> Click a type to fill in the generic parameter

🛠️ Utility Types

TypeScript includes several built-in utility types that help you transform existing types. These are like power tools for type manipulation - they save you from writing repetitive type definitions and make your code more maintainable. Let's explore the most useful ones! 🔧

📖 Definition

Utility Types: Built-in TypeScript types that perform transformations on existing types. They're like functions for types - you pass in a type, they return a modified version.

Partial<T>

Makes all properties of a type optional. Perfect for update functions where you might only change some properties:

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

// Partial makes all properties optional
function updateUser(id: number, updates: Partial<User>) {
    // Can update just some properties
    console.log(`Updating user ${id} with:`, updates);
}

updateUser(1, { name: "Alice" });              // ✅ Just name
updateUser(2, { email: "bob@example.com" });   // ✅ Just email
updateUser(3, { name: "Charlie", age: 30 });   // ✅ Multiple properties
updateUser(4, {});                              // ✅ No updates (valid but pointless)

Required<T>

The opposite of Partial - makes all properties required:

interface OptionalConfig {
    theme?: string;
    language?: string;
    notifications?: boolean;
}

// Required makes all properties mandatory
type RequiredConfig = Required<OptionalConfig>;

const config: RequiredConfig = {
    theme: "dark",
    language: "en",
    notifications: true
    // ❌ Error if any property is missing
};

Readonly<T>

Makes all properties read-only - perfect for immutable data structures:

interface Point {
    x: number;
    y: number;
}

const point: Readonly<Point> = { x: 10, y: 20 };

console.log(point.x);  // ✅ Can read
// point.x = 30;       // ❌ Error: Cannot assign to 'x' because it is a read-only property

Pick<T, K>

Creates a type by picking specific properties from another type:

interface Employee {
    id: number;
    name: string;
    email: string;
    department: string;
    salary: number;
    startDate: Date;
}

// Pick only the properties we need for display
type EmployeePreview = Pick<Employee, "id" | "name" | "department">;

const preview: EmployeePreview = {
    id: 1,
    name: "Alice",
    department: "Engineering"
    // ✅ No need for email, salary, or startDate
};

Omit<T, K>

Creates a type by omitting specific properties from another type:

interface User {
    id: number;
    username: string;
    password: string;
    email: string;
    createdAt: Date;
}

// Create a safe user type without password
type SafeUser = Omit<User, "password">;

function sendUserToClient(user: User): SafeUser {
    // Return user without password
    const { password, ...safeUser } = user;
    return safeUser;
}

const user: User = {
    id: 1,
    username: "alice",
    password: "secret123",
    email: "alice@example.com",
    createdAt: new Date()
};

const safeUser = sendUserToClient(user);
console.log(safeUser.username);  // ✅ OK
// console.log(safeUser.password);  // ❌ Error: password doesn't exist on SafeUser

Record<K, T>

Creates an object type with specified keys and value type:

// Record<Keys, ValueType>
type PageInfo = {
    title: string;
    description: string;
};

// Create a record where keys are page names and values are PageInfo
type SiteMap = Record<string, PageInfo>;

const site: SiteMap = {
    home: {
        title: "Home Page",
        description: "Welcome to our site"
    },
    about: {
        title: "About Us",
        description: "Learn more about our company"
    },
    contact: {
        title: "Contact",
        description: "Get in touch with us"
    }
};

// With specific keys
type WeekDays = "monday" | "tuesday" | "wednesday" | "thursday" | "friday";
type Schedule = Record<WeekDays, string>;

const workSchedule: Schedule = {
    monday: "9am - 5pm",
    tuesday: "9am - 5pm",
    wednesday: "9am - 5pm",
    thursday: "9am - 5pm",
    friday: "9am - 3pm"
    // ❌ Error if we try to add "saturday" or "sunday"
};

Exclude<T, U> and Extract<T, U>

Work with union types to exclude or extract specific members:

type AllColors = "red" | "green" | "blue" | "yellow" | "purple";

// Exclude removes types from union
type PrimaryColors = Exclude<AllColors, "yellow" | "purple">;
// Result: "red" | "green" | "blue"

// Extract keeps only specified types
type WarmColors = Extract<AllColors, "red" | "yellow">;
// Result: "red" | "yellow"

// Practical example
type Shape = 
    | { kind: "circle"; radius: number }
    | { kind: "square"; size: number }
    | { kind: "rectangle"; width: number; height: number };

// Extract only shapes with 'size' property
type ShapesWithSize = Extract<Shape, { size: number }>;
// Result: { kind: "square"; size: number }

NonNullable<T>

Removes null and undefined from a type:

type MaybeString = string | null | undefined;

type DefiniteString = NonNullable<MaybeString>;
// Result: string

function processValue(value: MaybeString) {
    // Type guard to ensure non-null
    if (value !== null && value !== undefined) {
        const definiteValue: DefiniteString = value;
        console.log(definiteValue.toUpperCase());
    }
}

ReturnType<T> and Parameters<T>

Extract return type and parameter types from functions:

function createUser(name: string, age: number) {
    return {
        id: Math.random(),
        name,
        age,
        createdAt: new Date()
    };
}

// Get the return type of the function
type User = ReturnType<typeof createUser>;
// Result: { id: number; name: string; age: number; createdAt: Date }

// Get the parameter types
type CreateUserParams = Parameters<typeof createUser>;
// Result: [name: string, age: number]

// Use them
const user: User = createUser("Alice", 30);
const params: CreateUserParams = ["Bob", 25];

🎯 Real-World Example: Form State Management

// Base form field type
interface FormField {
    value: string;
    error?: string;
    touched: boolean;
    required: boolean;
}

// Form with multiple fields
interface UserForm {
    username: FormField;
    email: FormField;
    password: FormField;
    confirmPassword: FormField;
    bio: FormField;
    agreeToTerms: FormField;
}

// For initial state - all fields have default values
type InitialFormState = Record<keyof UserForm, Partial<FormField>>;

// For validation - only need value and error
type ValidationState = Record<keyof UserForm, Pick<FormField, "value" | "error">>;

// For submission - omit UI-only fields
type SubmissionData = Record<keyof UserForm, Omit<FormField, "touched" | "error">>;

// For display - all fields readonly
type DisplayForm = Readonly<UserForm>;

// Initialize form
const initialForm: InitialFormState = {
    username: { value: "", touched: false },
    email: { value: "", touched: false },
    password: { value: "", touched: false },
    confirmPassword: { value: "", touched: false },
    bio: { value: "", touched: false },
    agreeToTerms: { value: "", touched: false }
};
Utility Type Purpose Example Use Case
Partial<T> All properties optional Update functions
Required<T> All properties required Ensure completeness
Readonly<T> All properties read-only Immutable data
Pick<T, K> Select specific properties Subset of type
Omit<T, K> Remove specific properties Exclude sensitive data
Record<K, T> Object with specific keys Maps, dictionaries
Exclude<T, U> Remove from union Filter types
Extract<T, U> Keep from union Select types
NonNullable<T> Remove null/undefined Ensure values exist
ReturnType<T> Function return type Type from function

✅ Pro Tip: Compose Utility Types

You can combine multiple utility types for powerful transformations! For example: Partial<Pick<User, "name" | "email">> creates a type with only name and email properties, both optional.

🎨 Interactive: Utility Type Transformer

See how utility types transform the original User interface. Click different utility types to see the result:

Original Type
interface User id: number name: string email: string age?: number role: string
Transformed Type
Select a utility type Click below...

🔀 Conditional Types

Conditional types allow you to create types based on conditions. They're like if-else statements for types! This is advanced TypeScript magic that lets you build incredibly flexible type systems. ✨

📖 Definition

Conditional Type: A type that depends on a condition, using the syntax T extends U ? X : Y. If T extends U, the type is X, otherwise it's Y.

Basic Conditional Types

Conditional types use the ternary operator syntax:

// Basic conditional type
type IsString<T> = T extends string ? true : false;

type A = IsString<string>;   // true
type B = IsString<number>;   // false
type C = IsString<"hello">;  // true

// Practical example: Make optional if nullable
type NullableOptional<T> = T extends null | undefined ? T | void : T;

type OptionalString = NullableOptional<string | null>;  // string | null | void
type RequiredString = NullableOptional<string>;         // string

Distributive Conditional Types

When conditional types are applied to union types, they "distribute" over each member:

type ToArray<T> = T extends any ? T[] : never;

// Distributes over union
type StrOrNumArray = ToArray<string | number>;
// Result: string[] | number[]
// NOT (string | number)[]

// Another example
type Filter<T, U> = T extends U ? T : never;

type Numbers = Filter<string | number | boolean, number>;
// Result: number

type StringsOrNumbers = Filter<string | number | boolean, string | number>;
// Result: string | number

The infer Keyword

The infer keyword lets you extract and store types within conditional types. This is incredibly powerful! 💪

// Extract return type from a Promise
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type A = UnwrapPromise<Promise<string>>;  // string
type B = UnwrapPromise<Promise<number>>;  // number
type C = UnwrapPromise<boolean>;          // boolean (not a Promise)

// Extract array element type
type Flatten<T> = T extends Array<infer U> ? U : T;

type D = Flatten<string[]>;    // string
type E = Flatten<number[][]>;  // number[]
type F = Flatten<boolean>;     // boolean

// Extract function parameters
type GetFirstParam<T> = T extends (first: infer U, ...args: any[]) => any ? U : never;

type Param = GetFirstParam<(name: string, age: number) => void>;  // string

🎪 Real-World Example: Event Handler Types

// Base event type
interface Event<T = any> {
    type: string;
    payload: T;
}

// Conditional type to extract payload type from event
type EventPayload<T> = T extends Event<infer P> ? P : never;

// Define specific events
interface UserLoggedIn extends Event<{ userId: string; timestamp: Date }> {
    type: "USER_LOGGED_IN";
}

interface ItemAdded extends Event<{ itemId: string; quantity: number }> {
    type: "ITEM_ADDED";
}

interface ErrorOccurred extends Event<{ message: string; code: number }> {
    type: "ERROR";
}

type AppEvent = UserLoggedIn | ItemAdded | ErrorOccurred;

// Extract payload types
type LoginPayload = EventPayload<UserLoggedIn>;
// Result: { userId: string; timestamp: Date }

type ItemPayload = EventPayload<ItemAdded>;
// Result: { itemId: string; quantity: number }

// Event handler with type safety
function handleEvent<T extends AppEvent>(
    event: T,
    handler: (payload: EventPayload<T>) => void
) {
    handler(event.payload);
}

// Usage with full type safety
const loginEvent: UserLoggedIn = {
    type: "USER_LOGGED_IN",
    payload: { userId: "123", timestamp: new Date() }
};

handleEvent(loginEvent, (payload) => {
    // TypeScript knows payload has userId and timestamp
    console.log(`User ${payload.userId} logged in at ${payload.timestamp}`);
});

Conditional Type Chains

You can chain conditional types to create complex type logic:

type TypeName<T> =
    T extends string ? "string" :
    T extends number ? "number" :
    T extends boolean ? "boolean" :
    T extends undefined ? "undefined" :
    T extends Function ? "function" :
    "object";

type T0 = TypeName<string>;     // "string"
type T1 = TypeName<number>;     // "number"
type T2 = TypeName<() => void>; // "function"
type T3 = TypeName<{ x: 1 }>;   // "object"

⚠️ Watch Out: Complexity

Conditional types can get complex quickly. Keep them simple and well-documented. If a conditional type becomes hard to understand, consider breaking it into smaller pieces or using a different approach.

graph TD A[Conditional Type] --> B{T extends U?} B -->|Yes| C[Type X] B -->|No| D[Type Y] C --> E[Result: X] D --> F[Result: Y] style A fill:#667eea,stroke:#333,stroke-width:2px,color:#fff style B fill:#ffc107,stroke:#333,stroke-width:2px style E fill:#4CAF50,stroke:#333,stroke-width:2px,color:#fff style F fill:#2196F3,stroke:#333,stroke-width:2px,color:#fff

🎨 Interactive: Conditional Type Simulator

See how T extends U ? X : Y evaluates. Select a type for T and watch the condition evaluate:

type IsString<T> = T extends string ? true : false

Select a type for T

🎨 Template Literal Types

Template literal types let you create new string literal types by combining existing string literal types. They're incredibly useful for creating type-safe APIs with dynamic string patterns. Think of them as template strings, but for types! 🎭

📖 Definition

Template Literal Type: A type that uses template literal syntax to create new string literal types. They use backticks and ${} interpolation, just like JavaScript template literals.

Basic Template Literals

Use backticks and interpolation syntax:

// Simple template literal type
type Greeting = `Hello, ${"World" | "TypeScript" | "Developer"}!`;
// Result: "Hello, World!" | "Hello, TypeScript!" | "Hello, Developer!"

// With type parameters
type EventName<T extends string> = `on${T}`;

type ButtonEvents = EventName<"Click" | "Hover" | "Focus">;
// Result: "onClick" | "onHover" | "onFocus"

// Multiple interpolations
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
type APIEndpoint = "users" | "posts" | "comments";

type APIRoute = `/${HTTPMethod}/${APIEndpoint}`;
// Result: "/GET/users" | "/GET/posts" | "/GET/comments" | 
//         "/POST/users" | "/POST/posts" | ... (12 combinations)

String Manipulation Types

TypeScript includes built-in types for common string transformations:

// Uppercase - converts to uppercase
type Loud = Uppercase<"hello">;  // "HELLO"

type ShoutCommands = Uppercase<"start" | "stop" | "pause">;
// Result: "START" | "STOP" | "PAUSE"

// Lowercase - converts to lowercase
type Quiet = Lowercase<"HELLO">;  // "hello"

// Capitalize - capitalizes first letter
type Proper = Capitalize<"typescript">;  // "Typescript"

// Uncapitalize - lowercases first letter
type Camel = Uncapitalize<"TypeScript">;  // "typeScript"

// Combining transformations
type ConstantCase<T extends string> = Uppercase<T>;
type PascalCase<T extends string> = Capitalize<T>;
type CamelCase<T extends string> = Uncapitalize<T>;

type MyConstant = ConstantCase<"max_value">;  // "MAX_VALUE"
type MyClass = PascalCase<"user">;            // "User"
type MyVariable = CamelCase<"UserName">;      // "userName"

Practical Applications

🎮 Real-World Example: Type-Safe Event System

// Define base event names
type EventBase = "click" | "hover" | "focus" | "blur" | "change";

// Create handler names
type EventHandler<T extends string> = `on${Capitalize<T>}`;

type Handlers = EventHandler<EventBase>;
// Result: "onClick" | "onHover" | "onFocus" | "onBlur" | "onChange"

// Create listener names
type EventListener<T extends string> = `add${Capitalize<T>}Listener`;

type Listeners = EventListener<EventBase>;
// Result: "addClickListener" | "addHoverListener" | ...

// Component with type-safe event props
type EventProps<T extends string> = {
    [K in EventHandler<T>]?: () => void;
};

type SafeButtonProps = EventProps<EventBase>;
// Automatically creates onClick, onHover, etc.

const button: SafeButtonProps = {
    onClick: () => console.log("Clicked!"),
    onHover: () => console.log("Hovering!")
};

🗄️ Real-World Example: Database Query Builder

// Define table columns
interface UserTable {
    id: number;
    name: string;
    email: string;
    age: number;
    isActive: boolean;
}

// Create getter method names
type Getter<T extends string> = `get${Capitalize<T>}`;
type Setter<T extends string> = `set${Capitalize<T>}`;

// Generate methods for each column
type UserGetters = {
    [K in keyof UserTable as Getter<string & K>]: () => UserTable[K];
};

type UserSetters = {
    [K in keyof UserTable as Setter<string & K>]: (value: UserTable[K]) => void;
};

// Result types:
// UserGetters: {
//     getId: () => number;
//     getName: () => string;
//     getEmail: () => string;
//     getAge: () => number;
//     getIsActive: () => boolean;
// }

// Create query builder
type QueryMethod = "find" | "create" | "update" | "delete";
type QueryBuilder = `${QueryMethod}User`;
// Result: "findUser" | "createUser" | "updateUser" | "deleteUser"

interface Database {
    findUser: (id: number) => UserTable | null;
    createUser: (data: Omit<UserTable, "id">) => UserTable;
    updateUser: (id: number, data: Partial<UserTable>) => UserTable;
    deleteUser: (id: number) => boolean;
}

Advanced Pattern: CSS-in-JS

// Define CSS property names (simplified)
type CSSProperty = "color" | "backgroundColor" | "fontSize" | "margin" | "padding";

// Create CSS variable names
type CSSVariable<T extends string> = `--${T}`;

type CSSVars = CSSVariable<CSSProperty>;
// Result: "--color" | "--backgroundColor" | "--fontSize" | "--margin" | "--padding"

// Create responsive breakpoint styles
type Breakpoint = "mobile" | "tablet" | "desktop";
type ResponsiveProperty<P extends string, B extends string> = `${P}-${B}`;

type ResponsiveColor = ResponsiveProperty<"color", Breakpoint>;
// Result: "color-mobile" | "color-tablet" | "color-desktop"

// Complete style system
type StyleSystem = {
    [K in CSSProperty]: string;
} & {
    [K in CSSVars]: string;
} & {
    [K in ResponsiveColor]: string;
};

const styles: Partial<StyleSystem> = {
    color: "blue",
    "--color": "var(--primary)",
    "color-mobile": "red",
    "color-tablet": "green",
    "color-desktop": "blue"
};

✅ Key Takeaways: Template Literal Types

  • Template literals create unions of all possible string combinations
  • Use built-in string manipulation types: Uppercase, Lowercase, Capitalize, Uncapitalize
  • Perfect for creating type-safe naming conventions
  • Combine with mapped types for powerful code generation
  • Extremely useful in React for prop types, event handlers, and CSS-in-JS
graph LR A[Template Literal Type] --> B[Base Type: 'click'] B --> C[Transform: Capitalize] C --> D[Prepend: 'on'] D --> E[Result: 'onClick'] style A fill:#667eea,stroke:#333,stroke-width:2px,color:#fff style E fill:#4CAF50,stroke:#333,stroke-width:2px,color:#fff

🎨 Interactive: Template Literal Type Builder

Build event handler types step by step. Select an event and see how template literals transform it:

Base Event "click" Capitalize<T> "Click" `on${T}` "onClick" Result onClick type Handler = `on${Capitalize<"click">}`

🏋️ Hands-on Practice

🏋️ Exercise 1: Build a Type-Safe API Client

Objective: Create a generic API client that handles different response types with full type safety.

Instructions:

  1. Create a generic ApiResponse<T> interface
  2. Write a fetchData<T> function that returns Promise<ApiResponse<T>>
  3. Create type guards to check if the response is successful
  4. Define specific data types (User, Product) and use them with your API client

Starter Code:

// TODO: Define ApiResponse interface

// TODO: Create fetchData function

// TODO: Create type guard

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

interface Product {
    id: number;
    title: string;
    price: number;
}

// TODO: Use your API client
💡 Hint

Remember that generic functions need the type parameter before the function parameters. Use response is Success syntax for type predicates.

✅ Solution
interface ApiResponse<T> {
    success: boolean;
    data?: T;
    error?: string;
}

async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
    try {
        const response = await fetch(url);
        const data = await response.json();
        return { success: true, data: data as T };
    } catch (error) {
        return { success: false, error: error.message };
    }
}

function isSuccess<T>(response: ApiResponse<T>): response is ApiResponse<T> & { data: T } {
    return response.success && response.data !== undefined;
}

// Usage
async function loadUser() {
    const response = await fetchData<User>("/api/users/1");
    if (isSuccess(response)) {
        console.log(response.data.name);
    }
}

🏋️ Exercise 2: Create a Type-Safe Event Emitter

Objective: Build an event emitter with discriminated unions and template literal types.

Instructions:

  1. Create event types using discriminated unions
  2. Use template literal types to generate event handler names
  3. Implement type-safe on and emit methods

Starter Code:

// TODO: Define your event types

// TODO: Create EventEmitter class

// Usage example:
// const emitter = new EventEmitter();
// emitter.on("userLogin", (payload) => console.log(payload.userId));
// emitter.emit({ type: "userLogin", userId: "123", timestamp: new Date() });
✅ Solution
type UserLoginEvent = {
    type: "userLogin";
    userId: string;
    timestamp: Date;
};

type ItemAddedEvent = {
    type: "itemAdded";
    itemId: string;
    quantity: number;
};

type AppEvent = UserLoginEvent | ItemAddedEvent;

class EventEmitter {
    private handlers: Partial<Record<AppEvent["type"], Function[]>> = {};

    on<T extends AppEvent>(
        eventType: T["type"],
        handler: (event: T) => void
    ) {
        if (!this.handlers[eventType]) {
            this.handlers[eventType] = [];
        }
        this.handlers[eventType]!.push(handler);
    }

    emit<T extends AppEvent>(event: T) {
        const handlers = this.handlers[event.type];
        if (handlers) {
            handlers.forEach(h => h(event));
        }
    }
}

🎯 Quick Quiz

Question 1: What's the difference between union and intersection types?

Question 2: What does a type guard do?

Question 3: When should you use generics?

🏆 Best Practices

✅ Do's

  • Use union types for values that can be multiple types - Perfect for status values, payment methods, or component variants
  • Use discriminated unions with a type property - Makes type narrowing automatic and reliable
  • Prefer intersection types when combining behaviors - Great for mixing in props or extending interfaces
  • Write custom type guards for complex type checks - Makes your code more readable and type-safe
  • Use generics for reusable, type-safe code - APIs, data structures, and utility functions benefit greatly
  • Leverage utility types to transform existing types - Saves time and reduces repetition
  • Document complex conditional types - Future you (and your team) will thank you

❌ Don'ts

  • Don't create deeply nested generics - Can slow down compilation and make errors hard to read
  • Don't make unions with thousands of members - Performance and readability suffer
  • Don't use type guards without runtime checks - They only work if the logic is correct
  • Don't overuse conditional types - They can make code hard to understand
  • Don't use generics when a simple type works - Not everything needs to be generic
  • Don't forget generic constraints when needed - Prevents misuse of your generic types

💡 Pro Tips for React Development

  • Use discriminated unions for component variants - Button components with different modes, for example
  • Make your component props generic when appropriate - List components, form fields, data tables
  • Use utility types for prop transformations - Omit HTML attributes, make props optional, etc.
  • Create type-safe event handlers with template literals - onClick, onChange, onSubmit patterns
  • Use Pick and Omit to extend HTML element props - Build on native elements with confidence
Use Case Best Type Feature Example
Value can be multiple types Union Types string | number
Combine multiple interfaces Intersection Types Person & Employee
Runtime type checking Type Guards typeof, instanceof, custom
Reusable for any type Generics Array<T>, Promise<T>
Transform existing types Utility Types Partial, Pick, Omit
Type logic and conditions Conditional Types T extends U ? X : Y
String pattern types Template Literals `on${Event}`

📝 Summary

🎉 Congratulations! You've Mastered Advanced Types!

You've completed the TypeScript Fundamentals module! Here's what you now know:

  • Union Types (|) - Multiple type possibilities with OR logic
  • Intersection Types (&) - Combining types with AND logic
  • Type Guards - Runtime checks for type safety (typeof, instanceof, in, custom guards)
  • Generics - Reusable, type-safe code with type parameters
  • Utility Types - Built-in type transformations (Partial, Pick, Omit, Record, etc.)
  • Conditional Types - Type logic with extends and infer
  • Template Literal Types - Type-safe string patterns

🎯 Key Takeaways

  • TypeScript's advanced type system is incredibly powerful and flexible
  • These features help you write more maintainable and bug-free code
  • Generics enable code reuse without sacrificing type safety
  • Utility types save you from writing repetitive type definitions
  • Type guards bridge the gap between compile-time and runtime safety
  • These patterns are essential for professional React development

📚 Additional Resources

🚀 What's Next?

You've completed Module 1: TypeScript Fundamentals! 🎊

Next up is the Mini-Project: Task Manager, where you'll apply everything you've learned to build a comprehensive, type-safe task management system. You'll use:

  • Interfaces and type aliases for data structures
  • Union types for task status and priority
  • Generics for reusable data handling
  • Type guards for validation
  • Utility types for partial updates
  • All your TypeScript skills combined!

After that, we'll dive into Module 2: React Basics, where you'll start building actual React components with TypeScript! 💪

graph LR A[TypeScript Fundamentals] --> B[Basic Types] A --> C[Interfaces] A --> D[Functions] A --> E[Advanced Types] E --> F[Union & Intersection] E --> G[Type Guards] E --> H[Generics] E --> I[Utility Types] E --> J[Conditional Types] E --> K[Template Literals] B --> L[Task Manager Project] C --> L D --> L E --> L L --> M[React Basics Module] style A fill:#667eea,stroke:#333,stroke-width:2px,color:#fff style E fill:#764ba2,stroke:#333,stroke-width:2px,color:#fff style L fill:#4CAF50,stroke:#333,stroke-width:2px,color:#fff style M fill:#2196F3,stroke:#333,stroke-width:2px,color:#fff

🎉 You Did It!

You've completed all 5 lessons of TypeScript Fundamentals! You now have a solid foundation in TypeScript's type system - from basic types to advanced generic programming. You're ready to build type-safe, maintainable applications!

Time to put it all together in the Task Manager mini-project! 🚀