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.