10. Advanced Types

Advanced types in TypeScript provide powerful tools to create flexible and precise type definitions for your code. In this article, we’ll explore union and intersection types, as well as type aliases, and see how they enhance type safety and expressiveness in your TypeScript projects.

Union Types

Union types allow a variable or parameter to accept multiple types of values. You define a union type by using the | operator between the types you want to allow.

// Union type
let age: number | string;

age = 30;    // Valid
age = "30";  // Valid
age = true;  // Error: Type 'true' is not assignable to type 'number | string'.

In this example, the age variable can accept both numbers and strings as values. If you attempt to assign a value of an incompatible type, TypeScript raises a type error.

Union types are valuable when you need to work with variables that can hold different data types based on specific conditions or inputs.

Intersection Types

Intersection types combine multiple types into a single type that includes all their properties. You define an intersection type using the & operator.

// Intersection type
type Person = {
    name: string;
};

type Address = {
    address: string;
};

type ContactInfo = Person & Address;

const contact: ContactInfo = {
    name: "John",
    address: "123 Main St",
};

In this example, the ContactInfo type combines the properties of both Person and Address, creating a type that includes name and address properties.

Intersection types are useful when you want to create new types by merging existing ones, allowing you to define more precise type definitions.

Type Aliases

Type aliases allow you to create custom names for complex or reusable types, improving code readability and maintainability. They provide a way to create a shorthand for complex type definitions.

// Type alias
type Point = {
    x: number;
    y: number;
};

const origin: Point = { x: 0, y: 0 };

In this example, we create a type alias Point for an object with x and y properties. This makes it easier to declare variables with the same type in your code.

Type aliases are particularly useful when dealing with complex object structures, function signatures, and union types.

// Type alias for a function signature
type Operation = (a: number, b: number) => number;

const add: Operation = (a, b) => a + b;
const subtract: Operation = (a, b) => a - b;

In this example, we create a type alias Operation for a function that takes two numbers and returns a number. This simplifies the declaration of functions with the same signature.

Discriminated Unions

Discriminated unions, also known as tagged unions or algebraic data types, allow you to create a type that can represent multiple possible shapes. They are often used with a common property (a discriminant) to determine which specific shape a value has.

// Discriminated union
type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; sideLength: number }
  | { kind: "rectangle"; width: number; height: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.sideLength ** 2;
    case "rectangle":
      return shape.width * shape.height;
  }
}

In this example, the Shape type represents different shapes with a common kind property. The area function takes a Shape and uses a switch statement to calculate the area based on the kind.

Discriminated unions are valuable when you want to work with values that can have multiple, distinct shapes and need to narrow down the type based on a specific property.

Indexed Access Types

Indexed access types allow you to create new types by looking up properties from existing types. You use the indexed access operator [ ] to access properties by their names.

type Person = {
  name: string;
  age: number;
};

type PersonProperty = Person["name" | "age"]; // "string" | "number"

In this example, PersonProperty represents the types of the name and age properties in the Person type. This is useful when you want to create new types by extracting properties from existing ones.

Indexed access types are also commonly used with mapped types, which allow you to transform or modify properties in bulk.

Conditional Types

Conditional types provide conditional type checking based on a condition. They enable you to create types that depend on the values of other types. You use the extends keyword to specify the condition.

type NonNullable<T> = T extends null | undefined ? never : T;

type ValueOrError<T> = T extends Error ? "Error" : T;

type Result = ValueOrError<number | Error>; // "number" | "Error"

In this example, ValueOrError is a conditional type that returns "Error" if the input type is an Error or the original type T otherwise.

Conditional types are useful for creating more flexible and dynamic type definitions, especially when dealing with generic types or complex type transformations.

Conclusion

Union and intersection types, along with type aliases, are powerful features in TypeScript that enhance type safety and code expressiveness. Union types enable variables to accept multiple types of values, intersection types allow you to create precise type combinations, and type aliases improve code readability by providing custom names for types. Understanding and using these advanced types effectively can lead to cleaner, more maintainable, and safer TypeScript code.

Discriminated unions, indexed access types, and conditional types are advanced features in TypeScript that can significantly enhance your ability to create precise and flexible type definitions. Discriminated unions help manage complex data structures with distinct shapes, indexed access types enable property extraction and manipulation, and conditional types provide conditional type checking and transformation. Understanding and using these advanced features can lead to more expressive and type-safe TypeScript code.

Previous
Next