typescript generics

typescript generics

  • 1 Introduction
  • 2. Custom generic type
  • 3. Generic constraints
    • 1. extends
    • 2, keyof
    • 3, infer
    • 4. Partial
    • 5. Required
    • 6. Exclude
    • 7. Pick
    • 8. Omit

1, Introduction

Generics is a powerful language feature of TypeScript that allows us to write reusable code, thereby improving code reusability, maintainability, and flexibility.

Simply put, generics are the ability to define a generic type or function with a type as a parameter. Through generics, we can define a function, class or interface whose concrete type is determined when it is used rather than when it is defined. This allows us to write more flexible and generic code without having to write duplicate code for each concrete type.

Simple generic function example:

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

The function identity receives a generic parameter T, indicating the type of the input parameter and the type of the return value.
When calling this function, we can use different type parameters to call this function to get different output results. For example:

// different return value types
let output1 = identity<string>("Hello, world!");
let output2 = identity<number>(42);

In addition to functions, you can also use generics to define classes and interfaces. For example, here’s an example of a simple class that uses generics:

class Box<T> {<!-- -->
  constructor(value: T) {<!-- -->
    this.contents = value;
  }
  contents: T;
}

The Box class receives a generic type parameter T, which is used within the class to define the type of the contents property. In the constructor of the class, we can pass the generic type parameter T to the constructor and assign it to the contents property.

let box1 = new Box<string>("Hello, world!");
let box2 = new Box<number>(42);

2, custom generic type

In TypeScript, use <> to define generic type parameters and use them in the definition of a function, class, interface, or type alias.

For example, the following Pair type represents a key-value pair consisting of two elements, where the key and value can be of any type:

type Pair<K, V> = {<!-- -->
  key: K;
  value: V;
};

In the above code, 2 generic type parameters K and V are used to represent the type of key and value.

By defining this generic type, a type-safe key-value pair is obtained, where the key and value can be of any type.

const person: Pair<string, number> = {<!-- -->
  key: "age",
  value: 30,
};

Example 2

type Result<T> = {<!-- -->
  success: boolean;
  data: T;
};

function getResult<T>(data: T): Result<T> {<!-- -->
  return {<!-- --> success: true, data };
}

In the above code, we use a generic type parameter T to represent the type of the data attribute in the Result type.

The getResult function can accept any type of parameter and return an object containing success and data properties.

3, generic constraints

In TypeScript, generic constraints are a very useful feature that can restrict generic type parameters, thereby enhancing the type safety and readability of the code.

Generic constraints can be implemented using the following keywords. (extends, keyof, infer are most common)

1, extends

By using the extends keyword, more precise restrictions can be placed on generic type parameters, enabling more accurate type inference and type checking when using generics.

When using the extends keyword to specify a generic type parameter, the generic type parameter has 2 characteristics

  1. Must be a subtype of a type
  2. Or the constraints of an interface must be met.

For example, must be a subtype of a type:

function printLength<T extends Array<any>>(arr: T): void {<!-- -->
  console.log(arr.length);
}

printLength([1, 2, 3]); // prints 3
printLength(["a", "b", "c"]); // prints 3
printLength(123); // wrong type, must be an array type

In the above example, a generic function printLength is defined, which accepts an array type parameter arr.
Whereas with the extends Array generic constraint, we restrict the generic type parameter T to be a subtype of Array. This means that the arr parameter must be an array type, otherwise a TypeError will be generated.

For example, the constraints of an interface must be satisfied:

interface HasLength {<!-- -->
  length: number;
}

function printLength<T extends HasLength>(obj: T): void {<!-- -->
  console.log(obj.length);
}

printLength([1, 2, 3]); // prints 3
printLength("hello"); // prints 5
printLength({<!-- --> length: 10 }); // output 10
printLength(123); // Type error, must meet the constraints of the HasLength interface

In the above example, an interface HasLength is defined, which contains a length property.

Then a generic function printLength is defined, which accepts a parameter obj that satisfies the HasLength interface constraint.

Using the extends HasLength generic constraint, we restrict the generic type parameter T to meet the constraints of the HasLength interface, that is, it must contain the length attribute. Only parameters that satisfy the constraints pass type checking.

2,keyof

keyof is an index type query operator in TypeScript, which is used to obtain the union type of all property names of a type. It can be used to restrict generic type parameters to only be the property names of an object.

That is to say, keyof T means to obtain the union type composed of all attribute names of type T.

type Person = {<!-- -->
  name: string;
  age: number;
};

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {<!-- -->
  return obj[key];
}

const person: Person = {<!-- -->
  name: "John",
  age: 30,
};

const name = getProperty(person, "name"); // type is string
const age = getProperty(person, "age"); // type is number

3, infer

In TypeScript, infer is a keyword used in Conditional Types.

It is used to infer a new type from a type expression as the result of a conditional type. For example, infer U means to infer a new type U from a certain type.

When infer is used in conjunction with conditional types, can capture specific type information from a generic type parameter and assign it to a new type variable. This allows us to infer other types of values based on input types when defining generic types, enabling more flexible and complex type operations.

Example 1: Extract function return type:

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

function greet(): string {<!-- -->
  return "Hello, world!";
}

type Greeting = ReturnType<typeof greet>; // type is string

In this example, we define a ReturnType type that takes a function type T as an argument. Using the conditional type T extends (...args: any[]) => infer R ? R : never, we check whether the generic type parameter T is a function type, and if so, Assign the return value type R of the function to infer R, otherwise return the never type.

In other words, assuming that the return value type of the original function is x, infer R means: use the infer keyword to capture and assign x to the generic variable R.

By using typeof greet to get the type of the greet function, and passing this type as a generic parameter to the ReturnType type, we can get the return value type of the greet function, which is string .

Example 2: Extract array element type:

type ElementType<T> = T extends Array<infer E> ? E : never;

type Numbers = [1, 2, 3, 4, 5];
type Element = ElementType<Numbers>; // type is 1 | 2 | 3 | 4 | 5

In this example, we define an ElementType type that takes an array type T as a parameter.

Using the conditional type T extends Array ? E : never, we check whether the generic type parameter T is an array type, and if so, assign the array element type E to infer E, otherwise return never type.

By passing the array type [1, 2, 3, 4, 5] as a generic parameter to the ElementType type, we can deduce that the array element is of type 1 | 2 | 3 | 4 | 5.

4, Partial

Used to make all properties of a type optional. For example, Partial means to make all properties of type T optional.

For example,

type Person = {<!-- -->
  name: string;
  age: number;
};

function updatePerson(person: Person, update: Partial<Person>): void {<!-- -->
  Object. assign(person, update);
}

const person: Person = {<!-- -->
  name: "John",
  age: 30,
};

updatePerson(person, {<!-- --> name: "Alice" }); // The second parameter can only pass the name attribute

5, Required

Used to make all properties of a type mandatory. For example, Required means to make all attributes of type T into required attributes.

example

type Person = {<!-- -->
  name?: string;
  age?: number;
};

function validatePerson(person: Required<Person>): boolean {<!-- -->
  return person.name !== undefined & amp; & amp; person.age !== undefined;
}

const person: Person = {<!-- -->
  name: "John",
};

validatePerson(person); // wrong type, missing required attribute age

6, Exclude

Used to exclude the specified type from a union type and generate a new union type.

The Exclude type accepts two parameters: the original type and the type to exclude. It returns a new union type that contains all other types of the original type except the specified type.

For example, Exclude means to exclude properties of type U from type T.

definition:

type Exclude<T, U> = T extends U ? never : T;

It can be seen that the Exclude type uses a conditional type to determine whether to include T in the result type by judging whether the original type T can be assigned to the type U to be excluded. Returns never if T is assignable to U, otherwise returns T itself.

Because T is a joint type, it will be judged one by one whether it can be assigned to type U

example

type Colors = 'red' | 'green' | 'blue'; // string literal type
type ExcludedColors = Exclude<Colors, 'green'>; // 'red' | 'blue';

const color: ExcludedColors = 'red';

console.log(color); // Output: 'red'

Compared with the Omit type, the Exclude type is more general, it can be used to exclude any type, not limited to object properties.
The Omit type is specifically used to exclude properties of objects.

Example 2: Exclude properties of objects

In fact, the object property is converted to a union type through keyof.

type Person = {<!-- -->
  name: string;
  age: number;
  gender: string;
};

type ExcludeGender = Exclude<keyof Person, "gender">; // type is "name" | "age"

function getPersonInfo(person: Person, key: ExcludeGender): string {<!-- -->
  return person[key].toString();
}

const person: Person = {<!-- -->
  name: "John",
  age: 30,
  gender: "male",
};

const name = getPersonInfo(person, "name"); // type is string
const age = getPersonInfo(person, "age"); // type is number

7, Pick

Pick: Used to select specified attributes from a type. For example, Pick means pick an attribute of type K from types T.

example

type Person = {<!-- -->
  name: string;
  age: number;
  gender: string;
};

type PersonInfo = Pick<Person, "name" | "age">; // type is { name: string; age: number; }

function getPersonInfo(person: Person, info: keyof PersonInfo): string {<!-- -->
  return person[info].toString();
}

const person: Person = {<!-- -->
  name: "John",
  age: 30,
  gender: "male",
};

const name = getPersonInfo(person, "name"); // type is string
const age = getPersonInfo(person, "age"); // type is number

8, Omit

Used to exclude specified attributes from a type and generate a new type.

The Omit type accepts two parameters: the original type and the property to exclude. It returns a new type that includes all the properties of the original type, excluding the specified properties.

definition:

Copy code
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

It can be seen that the Pick and Exclude types are used internally by the Omit type.

Compared with the Exclude type, the role of the Omit type is more specific, it is specially used to exclude specified attributes from a type (interface or object).

example

interface Person {<!-- -->
  name: string;
  age: number;
  email: string;
}

type PersonWithoutEmail = Omit<Person, 'email'>; // { name: string, age: number }

const person: PersonWithoutEmail = {<!-- -->
  name: 'John',
  age: 25,
};

console.log(person); // Output: { name: 'John', age: 25 }

These generic constraint types can be flexibly combined to build more complex type constraints. It should be noted that when using generic constraints, avoid excessive constraints, otherwise it may affect the scalability and maintainability of the code.