likes
comments
collection
share

学习TS泛型

作者站长头像
站长
· 阅读数 16

泛型

🎈为什么使用泛型

TypeScript 中的泛型是一种将类型作为参数传递给函数或类的方式。它允许您编写可重用的代码,可以用于不同类型的数据,而不必为每种类型编写新的代码。泛型是在编译时确定的,因此可以在编译期间进行类型检查。

如果没有泛型,我们要么必须给予恒等函数一个特定的类型:

function identity(arg: number): number {  
    return arg;  
}  

或者,我们可以使用 any 类型来描述恒等函数:

function identity(arg: any): any {  
    return arg;  
}  

虽然使用 any 肯定是通用的,因为它会导致函数接受任何和所有类型的类型 arg ,但我们实际上正在丢失有关函数返回时该类型的信息。如果我们传入一个数字,我们所拥有的唯一信息是可以返回任何类型。

相反,我们需要一种方法来捕获参数的类型,以便我们也可以使用它来表示返回的内容。在这里,我们将使用类型变量,这是一种特殊类型的变量,作用于类型而不是值。

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

我们现在已经在identity函数中添加了一个类型变量 Type 。这个 Type 允许我们捕获用户提供的类型(例如#20102;,以便我们以后可以使用这些信息。在这里,我们再次使用 Type 作为返回类型。在检查时,我们现在可以看到参数和返回类型使用了相同的类型。这允许我们在函数的一端传输该类型信息,并从另一端输出。

我们说这个版本的 identity 函数是泛型的,因为它可以在一系列类型上工作。与使用 any 不同,它也同样精确(即,它不会丢失任何信息)作为第一个使用数字作为参数和返回类型的 identity 函数。

一旦我们编写了通用标识函数,我们就可以用两种方式之一调用它。第一种方法是将所有参数(包括类型参数)传递给函数:

let output = identity<string>("myString"); // let output: string  

在这里,我们显式地将 Type 设置为 string 作为函数调用的参数之一,使用参数周围的 <> 而不是 () 表示。

第二种方式可能也是最常见的。这里我们使用类型参数推断-也就是说,我们希望编译器根据我们传入的参数类型自动为我们设置 Type 的值:

let output = identity("myString"); // let output: string  

请注意,我们不必显式地在尖括号( <> )中传递类型;编译器只是查看了值 "myString" ,并将 Type 设置为其类型。虽然类型参数推断可以是一个有用的工具,以保持代码更短,更可读,但当编译器无法推断类型时,您可能需要显式传递类型参数,就像我们在前面的示例中所做的那样,这可能发生在更复杂的示例中。

🎆使用泛型类型变量

当你开始使用泛型时,你会注意到当你创建像 identity 这样的泛型函数时,编译器会强制你在函数体中正确使用任何泛型类型的参数。也就是说,您实际上将这些参数视为可以是任何类型。

让我们从前面的 identity 函数开始:

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

如果我们还想在每次调用时将参数 arg 的长度记录到控制台,该怎么办?我们可能会忍不住这样写:

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

当我们这样做时,编译器会给予我们一个错误,我们正在使用 arg 的 .length 成员,但我们没有说 arg 有这个成员。记住,我们之前说过这些类型变量代表任何类型,所以使用这个函数的人可以传入一个没有 .length 成员的 number 。

假设我们实际上打算让这个函数直接作用于 Type 而不是 Type 的数组。因为我们使用的是数组,所以 .length 成员应该是可用的。我们可以像创建其他类型的数组一样描述它:

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

你可以把 loggingIdentity 的类型读作“泛型函数 loggingIdentity 接受一个类型参数 Type 和一个参数 arg ,它是一个 Type 的数组,并返回一个 Type 的数组。”如果我们传入一个数字数组,我们会得到一个数字数组,因为 Type 会绑定到 number 。这允许我们使用泛型类型变量 Type 作为我们正在处理的类型的一部分,而不是整个类型,这给了我们更大的灵活性。

我们也可以这样写示例:

function loggingIdentity<Type>(arg: Array<Type>): Array<Type> {  
    console.log(arg.length); // Array has a .length, so no more error  
    return arg;  
}  

🎇泛型类型

在前面的部分中,我们创建了适用于一系列类型的通用恒等函数。在本节中,我们将探索函数本身的类型以及如何创建泛型接口。

泛型函数的类型就像非泛型函数的类型一样,类型参数列在第一位,类似于函数声明:

function identity<Type>(arg: Type): Type {  
    return arg;  
}  
  
let myIdentity: <Type>(arg: Type) => Type = identity;  

我们还可以为类型中的泛型类型参数使用不同的名称,只要类型变量的数量和类型变量的使用方式一致即可。

function identity<Input>(arg: Input): Input {  
    return arg;  
}  
  
let myIdentity: <Input>(arg: Input) => Input = identity;  

我们也可以将泛型类型写成对象文字类型的调用签名:

function identity<Type>(arg: Type): Type {  
    return arg;  
}  
  
let myIdentity: { <Type>(arg: Type): Type } = identity;  

这引导我们编写第一个泛型接口。让我们从前面的例子中获取对象文字,并将其移动到接口中:

interface GenericIdentityFn {  
    <Type>(arg: Type): Type;  
}  
  
function identity<Type>(arg: Type): Type {  
    return arg;  
}  
  
let myIdentity: GenericIdentityFn = identity;  

在一个类似的例子中,我们可能希望将泛型参数移动为整个接口的参数。这可以让我们看到我们泛型的类型(例如 Dictionary<string> 而不是 Dictionary )。这使类型参数对接口的所有其他成员可见。

interface GenericIdentityFn<Type> {  
    (arg: Type): Type;  
}  
  
function identity<Type>(arg: Type): Type {  
    return arg;  
}  
  
let myIdentity: GenericIdentityFn<number> = identity;  

请注意,我们的示例已经发生了轻微的变化。我们现在有一个非泛型函数签名,它是泛型类型的一部分,而不是描述泛型函数。当我们使用 GenericIdentityFn 时,我们现在还需要指定相应的类型参数(这里: number ),有效地锁定了底层调用签名将使用的内容。了解何时将类型参数直接放在调用签名上以及何时将其放在接口本身上将有助于描述类型的哪些方面是泛型的。

除了泛型接口,我们还可以创建泛型类。

::: danger 请注意,不可能创建通用枚举和命名空间。 :::

🧨泛型类

泛型类具有与泛型接口相似的形状。泛型类在类名后面的尖括号( <> )中具有泛型类型参数列表。

class GenericNumber<NumType> {  
    zeroValue: NumType;  
    add: (x: NumType, y: NumType) => NumType;  
}  
  
let myGenericNumber = new GenericNumber<number>();  
myGenericNumber.zeroValue = 0;  
myGenericNumber.add = function (x, y) {  
    return x + y;  
};  

这是对 GenericNumber 类的一种非常直接的使用,但您可能已经注意到,没有任何东西限制它只能使用 number 类型。我们可以使用 string 或者更复杂的对象。

let stringNumeric = new GenericNumber<string>();  
stringNumeric.zeroValue = "";  
stringNumeric.add = function (x, y) {  
    return x + y;  
};  
  
console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));  

就像接口一样,将类型参数放在类本身上可以确保类的所有属性都使用相同的类型。

正如我们在关于类的章节中所述,类有两个方面:静态端和实例端。泛型类仅在其实例端而不是其静态端是泛型的,因此在使用类时,静态成员不能使用类的类型参数。

✨通用约束

如果你还记得前面的例子,你可能有时想写一个泛型函数,它可以在一组类型上工作,你对这组类型将具有什么功能有一些了解。在我们的 loggingIdentity 示例中,我们希望能够访问 arg 的 .length 属性,但编译器无法证明每个类型都有 .length 属性,因此它警告我们不能做这个假设。

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

我们不想处理任何类型,而是想限制这个函数处理任何和所有也有 .length 属性的类型。只要该类型具有此成员,我们就允许它,但它必须至少具有此成员。要做到这一点,我们必须列出我们的需求,作为对 Type 可以是什么的约束。

为此,我们将创建一个描述约束的接口。在这里,我们将创建一个接口,它有一个 .length 属性,然后我们将使用这个接口和 extends 关键字来表示我们的约束:

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

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

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

相反,我们需要传入其类型具有所有必需属性的值:

loggingIdentity({ length: 10, value: 3 });  

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

可以声明受其他类型参数约束的类型参数。例如,这里我们想从一个给定名称的对象中获取一个属性。我们希望确保我们不会意外地获取 obj 上不存在的属性,因此我们将在两个类型之间放置一个约束:

function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {  
    return obj[key];  
}  
  
let x = { a: 1, b: 2, c: 3, d: 4 };  
  
getProperty(x, "a");  
getProperty(x, "m");  
// Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.  

🎃在泛型中使用类类型

当使用泛型在TypeScript中创建工厂时,有必要通过其构造函数引用类类型。比如说

function create<Type>(c: { new (): Type }): Type {  
    return new c();  
}  

一个更高级的示例使用prototype属性来推断和约束构造函数与类类型的实例端之间的关系。

class BeeKeeper {  
    hasMask: boolean = true;  
}  
  
class ZooKeeper {  
    nametag: string = "Mikle";  
}  
  
class Animal {  
    numLegs: number = 4;  
}  
  
class Bee extends Animal {  
    numLegs = 6;  
    keeper: BeeKeeper = new BeeKeeper();  
}  
  
class Lion extends Animal {  
    keeper: ZooKeeper = new ZooKeeper();  
}  
  
function createInstance<A extends Animal>(c: new () => A): A {  
    return new c();  
}  
  
createInstance(Lion).keeper.nametag;  
createInstance(Bee).keeper.hasMask;  

::: info 此模式用于增强mixins设计模式。 :::

🎄通用参数默认值

考虑一个创建新的 HTMLElement 的函数。不带参数调用函数会生成一个 Div ;

使用元素作为第一个参数调用它会生成该参数类型的元素。

您也可以选择传递子级列表。

以前,你必须将其定义为:

declare function create(): Container<HTMLDivElement, HTMLDivElement[]>;  
// Cannot find name 'Container'.  
declare function create<T extends HTMLElement>(element: T): Container<T, T[]>;  
// Cannot find name 'Container'.  
declare function create<T extends HTMLElement, U extends HTMLElement>(  
    element: T,  
    children: U[]  
): Container<T, U[]>;  
// Cannot find name 'Container'.  

使用泛型参数默认值,我们可以将其减少为:

declare function create<T extends HTMLElement = HTMLDivElement, U = T[]>(  
    element?: T,  
    children?: U  
): Container<T, U>;  
// Cannot find name 'Container'.  

::: tip 泛型参数默认值遵循以下规则 :::

function identity<T = string>(arg: T): T {  
    return arg;  
}  
const result1 = identity('hello'); // result1: string  
const result2 = identity<number>(); // result2: number  

必需类型参数不能跟在可选类型参数后面。

function foo<T = string, U>(a: T, b?: U, c: T): void {}  
// 正确示例  
function bar<T, U = string>(a: T, c: U, b?: U): void {}  

类型参数的默认类型必须满足类型参数的约束(如果存在)。

interface HasLength {  
    length: number;  
}  
function foo<T extends HasLength = string>(arg: T): T {  
    return arg;  
}  
const result1 = foo('hello'); // result1: string  
const result2 = foo({ length: 5 }); // result2: { length: number }  
const result3 = foo(42); // Error: number does not extend HasLength  

指定类型参数时,只需要为所需的类型参数指定类型参数。未指定的类型参数将解析为其默认类型。

function foo<T, U = string>(a: T, b: U): void {}  
foo<number>('hello', 'world'); // Error: 'hello' is not a number  
foo<number, boolean>('hello', true); // Error: 'hello' is not a number  
foo<number, boolean>(42, true); // OK  
foo<number>('hello', 42); // OK, U is string by default  

如果指定了默认类型,而推断无法选择候选类型,则推断默认类型。

function foo<T = string>(arg: T): T {  
    return arg;  
}  
const result1 = foo('hello'); // result1: string  
const result2 = foo(); // result2: string, T is string by default  

与现有类或接口声明合并的类或接口声明可能会引入现有类型参数的默认值。

interface Foo<T = string> {  
    bar: T;  
}  
interface Foo<T = number> {  
    baz: T;  
}  
const result: Foo = { bar: 'hello', baz: 42 };  

与现有类或接口声明合并的类或接口声明可以引入新的类型参数,只要它指定默认值即可。

interface Foo<T = string> {  
    bar: T;  
}  
interface Foo<T = number, U = boolean> {  
    baz: T;  
    qux: U;  
}  
const result: Foo<boolean> = { bar: 'hello', baz: 42, qux: true };  
转载自:https://juejin.cn/post/7249010956935807036
评论
请登录