Skip to main content

Generics

软件工程的主要部分是构建不仅具有明确定义和一致的 API,而且还可以重复使用的组件。能够处理今天和未来数据的组件将为您构建大型软件系统提供最灵活的能力。在像C#和Java这样的语言中,创建可重复使用组件的主要工具之一是泛型,即能够创建可以适用于多种类型而不是单个类型的组件。这使得用户可以消费这些组件并使用自己的类型。

TypeScript中的泛型是一种编写能够适用于多个数据类型而非单个数据类型的代码的方式。泛型允许编写带有一个或多个类型参数的函数、类和接口,这些参数充当实际数据类型的占位符,将在使用函数、类或接口时使用。

所谓泛型,就是泛指的类型,在数据类型不明确时的替代类型。在定义时,接收泛指的数据类型,在使用时,指定明确的数据类型的一种类型给定方式。

考虑如下函数,如果没有泛型,那么必须为每个函数指定特定类型:

function identity(arg: number[], sep: string): number {
return arg.join(sep);
}

function identity(arg: string[], sep: string): string {
return arg.join(sep);
}

如上,两个函数内部的逻辑完全一样,但由于传递参数的类型不同而导致了不得不定义两个参数去处理两个不同类型的数组。

此时可以使用any来解决:

function identity(arg: any[], sep: string): any {
return arg.join(sep);
}

虽然使用any对于arg类型来说是泛型的,它将使函数接受任何类型的arg,但是如果想在使用函数的时候明确数组元素的类型话,any就不适合了,并且any会使函数的传参约束变小。

此时需要解决的问题是:逻辑相同类型不同的的指定类型的函数。

泛型初识

在函数定义时,不明确类型的前提下,给出一个泛指的类型占位,在调用函数时,明确泛型所对应的实际类型。

function identity<Type>(arg: Type[], sep: string): Type {
return arg.join(seq);
}

如上,在函数上使用了一个类型变量Type,这个Type允许捕获用户提供的类型,以便后续使用其信息,其次还使用了Type作为了返回类型。可以看到,参数和返回类型使用了相同的类型,这使得我们能够在函数的一侧传递类型信息,并在另一侧传递出来,我们可以说这个函数是泛型的。

泛型标别符

泛型标识符用来指明泛型的类型参数,可以由任意字符串或字符来标识,常见的有以下几种:

  • T -> Type
  • E -> Element
  • K -> Key
  • V -> Value
  • R -> Result
  • N -> Number
  • S -> String

泛型书写

函数声明:写在函数名后面

function identity<Type>(arg: Type[], sep: string): Type {
return arg.join(seq);
}

函数表示式:写在表达式最前面

const identity = <Type>(arg: Type[], sep: string): Type {
return arg.join(seq);
}

泛型调用

在函数调用时,函数名后面跟泛型,并传入具体的数据类型:

const output = identity<string>(['a', 'b', 'c'], '-');

这里,明确地将类型参数Type设置为string,并将其作为函数调用的参数之一,使用尖括号<>而不是圆括号()来表示。

当然,也可以让编译器根据传递的参数的类型进行类型参数推断。如下,编译器会自动推断Typenumber类型

const output = identity([1, 2, 3], ',');

虽然类型参数推断可以帮助我们保持代码更短、更易读,但在更复杂的例子中,当编译器无法推断出类型时,可能需要显式地传递类型参数。

泛型的好处

  1. 在不明确类型的情况下,使用泛型进行占位。
  2. 在调用函数时,对函数的参数进行类型的约束。
  3. 类型参数化时泛型的特征。

结合泛型类型变量

当使用泛型时,会注意到,当创建泛型函数时,编译器会强制你在函数体中正确使用任何泛型类型的参数。也就是说,要将这些参数视为可以是任何类型的参数,并相应地处理它们。

考虑如下示例:

function logLength<Type>(arg: Type): Type {
// Property 'length' does not exist on type 'Type'.
console.log(arg.length);
return arg;
}

如上,当这样做时,编译器会给出一个错误,指出在使用arg.length成员,但是我们并没有声明arg具有这个成员。假设我们实际上打算使这个函数适用于Type的数组,而不是直接使用Type。由于我们正在处理数组,.length 成员是可用的。我们可以像创建其他类型的数组一样描述这一点:

function logLength<Type>(arg: Type[]): Type[] {
console.log(arg.length);
return arg;
}

// or
function logLength<Type>(arg: Array<Type>): Array<Type> {
console.log(arg.length);
return arg;
}

将泛型类型变量Type作为类型的一部分,而不是整个类型,从而使其具有更大的灵活性。

接口泛型

接口中也可以是泛型声明。

interface ITest<T> {
a: T;
b: T;
}

const test: ITest<number> = {
a: 1,
b: 2
}

const test2: ITest<string> = {
a: '1',
b: '2'
}

泛型类

类就像像接口一样,也可以是泛型的。当使用new实例化一个泛型类时,其类型参数的推断方式与函数调用中的推断方式相同,当然也可以像接口一样使用泛型约束和默认值:

interface ILength {
length: number;
}

class Box<T extends ILength> {
value: T;

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

const box1 = new Box({
length: 10,
name: 'box1'
});

// { value: { length: 10, name: 'box1' } }
console.log(box1);

泛型约束

考虑如下示例:

function logLength<Type>(arg: Type): Type {
// Property 'length' does not exist on type 'Type'.
console.log(arg.length);
return arg;
}

与其处理任何类型,我们希望将此函数限制为仅适用于具有length属性的任何类型。只要类型具有此成员,就允许使用它,但是至少需要具有此成员。为了实现这一点,需要对Type进行约束。

先创建一个描述约束的接口,然后使用extends关键字表示约束,如下示例:

interface Lengthwise {
length: number;
}

function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
// Now we know it has a .length property, so no more error
console.log(arg.length);
return arg;
}

由于泛型函数现在受到约束,它将不再适用于任何和所有类型:

// ok
loggingIdentity({ length: 10, value: 3 });
loggingIdentity("hello world");

// Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.
loggingIdentity(3)

泛型约束是为了让泛型的可代表类型的范围缩小,而不是给泛型指定类型。

在泛型约束中使用类型参数

可以声明一个类型参数,并将其约束为另一个类型参数。例如,我们可能需要根据属性名称从对象中获取属性值,但是首先得确保该对象存在此属性,因此可做如下泛型限制:

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

const obj = {
a: 1,
b: 2,
}

// Key is "a" | "b"
getProperty(obj, 'a');
// Argument of type '"m"' is not assignable to parameter of type '"a" | "b"'.
getProperty(obj, 'm');

泛型的联合类型

类型参数可以指定为联合类型,以满足多种类型:

function mergeArr<E>(arr1: E[], arr2: E[]): E[] {
return [...arr1, ...arr2]
}

const result = mergeArr<number | string>([1, 2, 3], ['a', 'b', 'c'])

泛型参数默认值

在TypeScrip 中,可以为泛型参数提供默认值,这使得在使用泛型时更加灵活,同时允许您在必要时不指定具体的类型参数。

function processArray<T = number>(arg: T[]): T[] {
return arg;
}

const numbers: number[] = [1, 2, 3, 4];
const strings: string[] = ['a', 'b', 'c'];

processArray(numbers);
processArray(strings);

// -------------------------------------------------------
interface Container<T, U> {
element: T;
children: U;
}

declare function create<T extends HTMLElement = HTMLDivElement, U = T[]>(
element?: T,
children?: U[]
): Container<T, U[]>;

通过提供泛型参数的默认值,可以减少在使用泛型函数时需要显式指定类型参数的情况,同时仍然具有灵活性来处理不同类型的数据。

泛型参数默认值编写规则

  • 如果类型参数具有默认值,则被视为可选。
function foo<T = string>(arg: T): void {
console.log(arg);
}

foo("Hello");
foo<number>(42);
foo(32); // 推断为number
  • 必需的类型参数必须在可选类型参数之前声明。
function foo<T, U = string>(arg1: T, arg2?: U): void {
console.log(arg1, arg2);
}

foo("Hello");
// Argument of type 'string' is not assignable to parameter of type 'number'.
foo<number>("Hello");
foo<string, number>("Hello", 42);
  • 如果类型参数存在约束条件,则为其设置默认类型时必须满足该约束条件。
interface Lengthwise {
length: number;
}

function foo<T extends Lengthwise = string>(arg: T): void {
console.log(arg.length);
}

foo("Hello");
// Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.
foo(42);
foo({ length: 5 });
  • 在指定类型参数时,只需要为必需类型参数指定类型参数。未指定的类型参数将解析为其默认类型。
function foo<T, U = string>(arg1: T, arg2: U): void {
console.log(arg1, arg2);
}

foo<number>(42, 'hello');
foo("Hello", 42);
  • 如果指定了默认类型且无法推断出候选类型,则会推断默认类型。
  • 与现有类或接口声明合并的类或接口声明可以为现有类型参数引入默认类型。
  • 与现有类或接口声明合并的类或接口声明可以引入新的类型参数,只要它指定了默认类型。