阿里云主机折上折
  • 微信号
Current Site:Index > Generic constraints

Generic constraints

Author:Chuan Chen 阅读数:27026人阅读 分类: TypeScript

Basic Concepts of Generic Constraints

Generic constraints in TypeScript allow us to impose restrictions on generic type parameters, ensuring they meet specific conditions. Using the extends keyword, we can specify that a generic type must conform to a certain type structure. This mechanism is particularly useful when we need to ensure that generic parameters possess certain properties or methods.

interface Lengthwise {
  length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}

Using Type Parameter Constraints

Generic constraints can be based not only on concrete types but also on other type parameters. This technique is often used to create generic functions with dependencies, where one type parameter must satisfy the requirements of another.

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

let x = { a: 1, b: 2, c: 3 };
getProperty(x, "a"); // Correct
getProperty(x, "m"); // Error: 'm' is not in 'a'|'b'|'c'

Application of Multiple Constraints

TypeScript supports applying multiple constraints to generic parameters through intersection types. This approach requires the type parameter to satisfy multiple interfaces or type conditions simultaneously.

interface Serializable {
  serialize(): string;
}

interface Loggable {
  log(): void;
}

function process<T extends Serializable & Loggable>(item: T) {
  item.log();
  const serialized = item.serialize();
  // ...
}

Constructor Constraints

Using the new constraint, we can restrict a generic type to have a constructor. This is particularly useful in factory functions that need to create instances of types.

interface Constructor<T> {
  new(...args: any[]): T;
}

function createInstance<T>(ctor: Constructor<T>, ...args: any[]): T {
  return new ctor(...args);
}

class Person {
  constructor(public name: string) {}
}

const person = createInstance(Person, "Alice");

Combining Constraints with Conditional Types

When generic constraints are combined with conditional types, more flexible type operations can be created. This combination allows selecting different type branches based on constraint conditions.

type Flatten<T> = T extends Array<infer U> ? U : T;

function flatten<T>(array: T): Flatten<T> {
  return Array.isArray(array) ? array[0] : array;
}

const str = flatten("hello"); // string
const num = flatten([42]); // number

Advanced Constraint Patterns

In complex scenarios, we can use mapped types and the keyof operator to create advanced constraint patterns. These techniques are often used to build type-safe utility types.

type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

interface User {
  id: number;
  name: string;
  age: number;
}

type PartialUser = PartialBy<User, 'id' | 'age'>;
// Equivalent to { name: string; id?: number; age?: number }

Constraints in React Components

In React development, generic constraints are often used to create reusable components while ensuring type safety for props. This pattern is particularly common in higher-order components and render prop components.

interface WithLoadingProps {
  loading: boolean;
}

function withLoading<P extends object>(
  Component: React.ComponentType<P>
): React.FC<P & WithLoadingProps> {
  return ({ loading, ...props }: P & WithLoadingProps) => 
    loading ? <div>Loading...</div> : <Component {...props as P} />;
}

Type Predicates and Constraints

Combined with type predicates, generic constraints can be used to create user-defined type guard functions that can check types at runtime and narrow the type scope.

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

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

function isBird<T extends Bird | Fish>(pet: T): pet is T & Bird {
  return (pet as Bird).fly !== undefined;
}

function move<T extends Bird | Fish>(pet: T) {
  if (isBird(pet)) {
    pet.fly();
  } else {
    pet.swim();
  }
}

Constraints with Default Type Parameters

Generic constraints can be combined with default type parameters to provide more flexible API design. When type parameters are not explicitly specified, default types are used, but they must still satisfy the constraint conditions.

interface PaginationOptions<T = any> {
  pageSize: number;
  currentPage: number;
  data: T[];
}

function paginate<T extends { id: string } = { id: string }>(
  options: PaginationOptions<T>
) {
  // Implement pagination logic
}

Recursive Type Constraints

TypeScript supports recursive type constraints, allowing type parameter constraints to reference themselves. This technique is often used to define tree structures or recursive data structures.

interface TreeNode<T extends TreeNode<T>> {
  value: number;
  children: T[];
}

class BinaryNode implements TreeNode<BinaryNode> {
  constructor(
    public value: number,
    public children: [BinaryNode?, BinaryNode?] = []
  ) {}
}

Constraints and Variadic Tuple Types

In variadic tuple types introduced in TypeScript 4.0, generic constraints can be used to restrict type relationships between tuple elements, creating more precise tuple manipulation functions.

type Shift<T extends any[]> = ((...args: T) => any) extends 
  ((arg: any, ...rest: infer R) => any) ? R : never;

function tail<T extends any[]>(args: [...T]): Shift<T> {
  const [, ...rest] = args;
  return rest as Shift<T>;
}

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

上一篇:泛型类

下一篇:泛型参数默认值

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.