Working with the DOM

Learn how to use TypeScript to interact with the DOM and create dynamic web pages.


Advanced TypeScript Concepts & Utility/Conditional Types for Beginners

Advanced TypeScript Concepts

Beyond the basics, TypeScript offers powerful features that help write more robust, maintainable, and expressive code. Here are a few key concepts:

1. Intersection Types

An intersection type combines multiple types into one. The resulting type has all the properties and methods of all the constituent types.

 // Define some interfaces
interface Colorful {
    color: string;
}

interface Circle {
    radius: number;
}

// Create an intersection type
type ColorfulCircle = Colorful & Circle;

// Usage
const colorfulCircle: ColorfulCircle = {
    color: "red",
    radius: 10,
};

console.log(colorfulCircle.color); // Output: red
console.log(colorfulCircle.radius); // Output: 10 

2. Union Types

A union type allows a variable to hold values of different types. It's denoted using the pipe symbol |.

 function printId(id: string | number) {
    console.log("Your ID is: " + id);

    // Narrowing is often required
    if (typeof id === "string") {
        console.log(id.toUpperCase()); // Access string-specific methods
    } else {
        console.log(id * 2);         // Access number-specific methods
    }
}

printId(123);
printId("abc"); 

3. Type Guards

Type guards are functions that narrow down the type of a variable within a specific block of code. They help TypeScript understand the possible types at runtime.

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

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

function isBird(animal: Bird | Fish): animal is Bird {
    return (animal as Bird).fly !== undefined;
}

function move(animal: Bird | Fish) {
    if (isBird(animal)) {
        animal.fly();
    } else {
        animal.swim();
    }
}

const myBird: Bird = { fly: () => console.log("Flying!"), layEggs: () => console.log("Bird laying eggs") };
const myFish: Fish = { swim: () => console.log("Swimming!"), layEggs: () => console.log("Fish laying eggs") };

move(myBird);
move(myFish); 

4. Discriminated Unions

Also known as tagged unions or algebraic data types. They are union types where each type in the union has a common, distinct property (the "discriminant"). This makes it easier to handle different cases within the union using type guards.

 interface Circle {
    kind: "circle"; // Discriminant property
    radius: number;
}

interface Square {
    kind: "square"; // Discriminant property
    sideLength: number;
}

type Shape = Circle | Square;

function getArea(shape: Shape): number {
    switch (shape.kind) {
        case "circle":
            return Math.PI * shape.radius ** 2;
        case "square":
            return shape.sideLength ** 2;
        default:
            // TypeScript knows this should never happen if all cases are handled
            const _exhaustiveCheck: never = shape;
            return _exhaustiveCheck; // for exhaustiveness checking
    }
}

const myCircle: Circle = { kind: "circle", radius: 5 };
const mySquare: Square = { kind: "square", sideLength: 4 };

console.log("Circle area:", getArea(myCircle));
console.log("Square area:", getArea(mySquare)); 

Introduction to TypeScript Utility and Conditional Types

TypeScript provides built-in utility types and conditional types that allow you to perform complex type transformations and create more flexible and reusable types.

1. Utility Types

Utility types are globally available type transformers. They are pre-defined generic types that operate on other types to produce new types. Here are some common examples:

Partial

Makes all properties in T optional.

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

// `PartialPerson` has `name?: string; age?: number;`
type PartialPerson = Partial<Person>;

const partialPerson: PartialPerson = { name: "Alice" }; // Valid 

Required

Makes all properties in T required.

 interface OptionalPerson {
    name?: string;
    age?: number;
}

// `RequiredPerson` has `name: string; age: number;`
type RequiredPerson = Required<OptionalPerson>;

const requiredPerson: RequiredPerson = { name: "Bob", age: 30 }; // Valid 

Readonly

Makes all properties in T readonly. The values cannot be reassigned after initial assignment.

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

// `ReadonlyPoint` has `readonly x: number; readonly y: number;`
type ReadonlyPoint = Readonly<ImmutablePoint>;

const readonlyPoint: ReadonlyPoint = { x: 10, y: 20 };
// readonlyPoint.x = 30; // Error: Cannot assign to 'x' because it is a read-only property. 

Pick

Selects a set of properties K from T.

 interface Product {
    id: number;
    name: string;
    price: number;
    description: string;
}

// `ProductSummary` has `name: string; price: number;`
type ProductSummary = Pick<Product, "name" | "price">;

const productSummary: ProductSummary = { name: "Laptop", price: 1200 }; 

Omit

Removes a set of properties K from T. The opposite of Pick.

 interface User {
    id: number;
    username: string;
    email: string;
    isAdmin: boolean;
}

// `PublicUser` has `username: string; email: string;`
type PublicUser = Omit<User, "id" | "isAdmin">;

const publicUser: PublicUser = { username: "john.doe", email: "john.doe@example.com" }; 

Record

Constructs a type with a set of properties K of type T.

 // Creates a type where the keys are strings (ids) and values are User objects
type UserMap = Record<string, User>;

const users: UserMap = {
    "123": { id: 123, username: "john.doe", email: "john.doe@example.com", isAdmin: false },
    "456": { id: 456, username: "jane.doe", email: "jane.doe@example.com", isAdmin: true }
}; 

2. Conditional Types

Conditional types allow you to define types based on a condition. They use a syntax similar to a ternary operator.

 // Type = T if T extends U, otherwise type = V
// T extends U ? T : V

// Example: Get the return type of a function
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

function myFunction(x: number): string {
    return "Result: " + x;
}

// `MyFunctionReturnType` is `string`
type MyFunctionReturnType = ReturnType<typeof myFunction>;

const myVar: MyFunctionReturnType = "Hello"; // Valid 

Explanation: The ReturnType utility type takes a function type T. It uses conditional typing to check if T is a function that accepts any arguments (...args: any) and returns any value (=> any). If it is, it infers the return type R and returns it. Otherwise, it defaults to any. The infer keyword is crucial here; it allows TypeScript to automatically "infer" the type from the provided function type.

Conditional types are powerful for creating flexible and dynamic type definitions based on runtime conditions.

Example combining Utility and Conditional Types

Suppose we want to create a type that can be used to selectively make properties of an interface required based on some condition.

 interface EventData {
  eventName: string;
  payload?: any;  // Optional payload.
  timestamp?: number; // Optional timestamp
}


type RequireTimestamp<T extends EventData, K extends boolean> =
    K extends true ? Required<T> : T;

// Example usage:
type EventWithOptionalTimestamp = RequireTimestamp<EventData, false>; // EventData with optional timestamp.
type EventWithRequiredTimestamp = RequireTimestamp<EventData, true>;  // EventData with REQUIRED timestamp.

const eventOptional: EventWithOptionalTimestamp = { eventName: "UserLogin", payload: { userID: 123 } }; // Valid.  timestamp is missing and that's ok.
const eventRequired: EventWithRequiredTimestamp = { eventName: "UserLogin", payload: { userID: 123 }, timestamp: Date.now() };  // Valid, Timestamp is present

// The next line would result in an error, as timestamp is required:
// const eventRequiredError: EventWithRequiredTimestamp = { eventName: "UserLogin", payload: { userID: 123 } }; 

This is a basic introduction. Mastering these features takes practice, but understanding them is essential for writing advanced and type-safe TypeScript code.