DEV Community

Tianya School
Tianya School

Posted on

TypeScript Advanced Type Techniques Generics, Union, and Intersection Types

Generics

In TypeScript, generics are a powerful tool that allow us to write reusable components capable of adapting to multiple types.

1. Generic Constraints

Generics can be constrained to a specific type or interface, ensuring that the types passed to the generic meet certain conditions. For example, if we want a function to only accept types with a length property, we can do this:

interface Lengthwise {
  length: number;
}

function printLength<T extends Lengthwise>(item: T): void {
  console.log(item.length);
}

const stringLength = "hello";
printLength(stringLength); // OK, strings have a length property
const objectWithoutLength = { name: "World" };
printLength(objectWithoutLength); // Error, no length property
Enter fullscreen mode Exit fullscreen mode

<T extends Lengthwise> ensures that T must have a length property.

2. Type Inference with Generics

TypeScript allows automatic inference of generic types in certain cases, particularly during function calls. For example, using the infer keyword, we can extract the return type from a function type:

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function identity<T>(arg: T): T {
  return arg;
}

type IdentityReturnType = ReturnType<typeof identity>; // IdentityReturnType is inferred as 'string'
Enter fullscreen mode Exit fullscreen mode

Here, ReturnType extracts the return type from the function type.

3. Multi-Parameter Generics

You can define types or functions that accept multiple generic parameters:

interface Pair<T, U> {
  first: T;
  second: U;
}

function createPair<T, U>(first: T, second: U): Pair<T, U> {
  return { first, second };
}

const pair = createPair("Hello", 42); // pair has type Pair<string, number>
Enter fullscreen mode Exit fullscreen mode

The createPair function accepts two generic parameters T and U and returns an object of type Pair<T, U>.

4. Generic Interfaces

Interfaces can also use generics:

interface GenericIdentityFn<T> {
  (arg: T): T;
}

function identity<T>(arg: T): T {
  return arg;
}

let myIdentity: GenericIdentityFn<number> = identity; // myIdentity is a number-specific version of the identity function
Enter fullscreen mode Exit fullscreen mode

Here, GenericIdentityFn is a generic interface, and the identity function is assigned to an instance of it, restricting it to handle only number type parameters.

5. Generic Classes

Classes can also be defined as generics, allowing methods and properties to use different types:

class Box<T> {
  value: T;

  constructor(value: T) {
    this.value = value;
  }

  setValue(newValue: T): void {
    this.value = newValue;
  }
}

const boxOfStrings = new Box<string>("Hello");
boxOfStrings.setValue("World"); // OK
boxOfStrings.setValue(123); // Error, incompatible types
Enter fullscreen mode Exit fullscreen mode

The Box class accepts a generic type T, and the specific type is specified during instantiation.

Union Types

Union types in TypeScript allow combining multiple types into a single type, meaning a variable can be one of several types.

1. Type Guards and Type Assertions

When dealing with union types, you may need to determine the specific type of a variable. This can be achieved with type guards, which are functions or expressions that check a variable's properties to narrow its possible type range. For example:

type Shape = { kind: 'circle'; radius: number } | { kind: 'square'; side: number };

function getArea(shape: Shape): number {
  if ('radius' in shape) {
    // Type guard: shape is now { kind: 'circle'; radius: number }
    return Math.PI * shape.radius ** 2;
  } else {
    // Type guard: shape is now { kind: 'square'; side: number }
    return shape.side ** 2;
  }
}
Enter fullscreen mode Exit fullscreen mode

In this example, checking if shape has a radius property determines whether it’s a circle or a square.

2. Non-null Assertion Operator (!)

The non-null assertion operator ! informs the compiler that, even if a union type includes null or undefined, you are certain the value is neither. However, incorrect usage may lead to runtime errors:

function logValue(value: string | null | undefined): void {
  if (value) {
    console.log(value!.toUpperCase()); // Use ! for non-null assertion
  } else {
    console.log('Value is null or undefined');
  }
}
Enter fullscreen mode Exit fullscreen mode

Here, if value is not null or undefined, we use ! to suppress potential null or undefined type warnings.

3. Distributive Type Operator (& and |)

When applying union or intersection types to a generic type, they are distributed across each instance of the generic. For example, with an array whose elements are a union type:

type NumbersOrStrings = number | string;
type ArrayWithMixedElements<T> = T[];

const mixedArray: ArrayWithMixedElements<NumbersOrStrings> = [1, "two", 3];
Enter fullscreen mode Exit fullscreen mode

mixedArray’s element type is NumbersOrStrings, so it can contain number or string.

4. Pattern Matching

In destructuring assignments, function parameters, or type aliases, pattern matching can be used to handle values of union types:

type Shape = { kind: 'circle'; radius: number } | { kind: 'square'; side: number };

function handleShape(shape: Shape) {
  switch (shape.kind) {
    case 'circle':
      const { radius } = shape;
      // Now we know shape is { kind: 'circle'; radius: number }
      break;
    case 'square':
      const { side } = shape;
      // Now we know shape is { kind: 'square'; side: number }
      break;
  }
}
Enter fullscreen mode Exit fullscreen mode

In this example, the switch statement acts as a type guard, handling different shape types based on the kind property.

Intersection Types

Intersection types in TypeScript allow combining multiple types into a new type that includes all properties and methods of the original types.

1. Combining Types

Intersection types use the & operator to merge two or more types. For example, suppose we have two interfaces, Person and Employee. We can create a PersonAndEmployee intersection type:

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

interface Employee {
  id: number;
  department: string;
}

type PersonAndEmployee = Person & Employee;

const person: PersonAndEmployee = {
  name: 'Alice',
  age: 30,
  id: 123,
  department: 'HR',
};
Enter fullscreen mode Exit fullscreen mode

The person variable must satisfy the requirements of both Person and Employee interfaces.

2. Intersection of Classes and Interfaces

Intersection types can also be applied between classes and interfaces, combining a class instance with the properties and methods of an interface:

class Animal {
  name: string;
  makeSound(): void {
    console.log('Making sound...');
  }
}

interface HasColor {
  color: string;
}

class ColoredAnimal extends Animal implements HasColor {
  color: string;
}

type ColoredAnimalIntersection = Animal & HasColor;

function describeAnimal(animal: ColoredAnimalIntersection) {
  console.log(`The ${animal.name} is ${animal.color} and makes a sound.`);
  animal.makeSound();
}

const coloredCat = new ColoredAnimal();
coloredCat.name = 'Kitty';
coloredCat.color = 'Gray';
describeAnimal(coloredCat);
Enter fullscreen mode Exit fullscreen mode

The ColoredAnimalIntersection type is both an instance of the Animal class and possesses the color property from the HasColor interface.

3. Type Guards

Intersection types are useful in type guards, especially when determining a specific type within a union type. For example, you might have an object that could be one of two types, and you want to determine which it is at a specific moment:

interface Movable {
  move(): void;
}

interface Static {
  stay(): void;
}

type ObjectState = Movable & Static;

function isMovable(obj: ObjectState): obj is Movable {
  return typeof obj.move === 'function';
}

const object: ObjectState = {
  move: () => console.log('Moving...'),
  stay: () => console.log('Staying...')
};

if (isMovable(object)) {
  object.move(); // Type guard ensures the move method exists
} else {
  object.stay();
}
Enter fullscreen mode Exit fullscreen mode

The isMovable function is a type guard that checks if the move method exists, confirming that object is of type Movable.

📘 *Want to get more practical programming tutorials? *

👨‍💻 If you want to systematically learn front-end, back-end, algorithms, and architecture design, I continue to update content packages on Patreon

🎁 I have compiled a complete series of advanced programming collections on Patreon:

  • Weekly updated technical tutorials and project practice
  • High-quality programming course PDF downloads
  • Front-end / Back-end / Full Stack / Architecture Learning Collection
  • Subscriber Exclusive Communication Group

👉 Click to join and systematically improve development capabilities: Advanced Development Tutorial

🚀 Join my technical exchange group to get daily useful information:

Thank you for your support and attention ❤️

Top comments (0)