加更 泛型加深理解 下饭下饭
大家好,这里是皮哥饭店
,帮助大家下饭,再不吃饱就要被赶回家啦。
泛型的定义和作用
泛型是 TypeScript 中的一种高级类型,它允许你在定义函数、类、接口时不指定具体类型,而是将类型作为参数传入,从而实现通用性更强的代码。
在学习泛型的时候,你需要了解以下概念:
- 泛型类型参数(type parameter):用于指定泛型类型的占位符。
- 泛型函数(generic function):使用泛型类型参数的函数。
- 泛型类(generic class):使用泛型类型参数的类。
- 泛型约束(generic constraint):用于对泛型类型参数进行约束,以保证泛型类型参数的特定属性或方法存在。
下面是一个简单的泛型函数示例:
function identity<T>(arg: T): T {
return arg;
}
在这个函数中,<T>
表示这是一个泛型函数,并且 T
是一个泛型类型参数。这个函数接收一个参数 arg
,类型为 T
,并将其原封不动地返回。
泛型函数的调用方式与普通函数类似,不同之处在于调用时需要指定具体类型参数:
const result1 = identity<string>("hello");
const result2 = identity<number>(123);
在这个示例中,我们分别调用了 identity
函数,并指定了具体的类型参数。第一个调用中,类型参数为 string
,返回值类型也为 string
;第二个调用中,类型参数为 number
,返回值类型也为 number
。
通过这个例子,你可以初步了解泛型的基本用法,包括泛型函数的定义和调用。接下来,你需要更深入地了解泛型的使用和约束。
泛型函数
使用泛型函数
使用泛型函数时,需要在函数名后面用尖括号(<>
)指定类型参数。例如:
function identity<T>(arg: T): T {
return arg;
}
let output = identity<string>("hello world");
在这个例子中,identity
是一个泛型函数,类型参数为<T>
,传入的参数和返回值都是类型T
。在调用identity
函数时,将类型参数指定为string
,函数返回值为"hello world"
。
泛型接口
定义泛型接口
泛型接口是一种可以指定参数类型的接口。定义泛型接口的语法如下:
interface interfaceName<T> {
propertyName: T;
}
这里的<T>
表示一个类型参数,可以根据需要自定义类型参数的名称。泛型函数可以包含多个类型参数,每个参数之间用逗号分隔。
使用泛型接口
使用泛型接口时,需要在接口名后面用尖括号(<>
)指定类型参数。例如:
interface GenericIdentityFn<T> {
(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
在这个例子中,GenericIdentityFn
是一个泛型接口,类型参数为<T>
,接口中定义了一个接受类型为T
的参数并返回类型为T
的函数。在调用myIdentity
函数时,将类型参数指定为number
,函数返回值为一个number
类型的值。
泛型类
定义泛型类
泛型类是具有一个或多个类型参数的类。定义泛型类的语法如下:
class ClassName<T> {
propertyName: T;
}
这里的<T>
表示一个类型参数,可以根据需要自定义类型参数的名称。
使用泛型类
使用泛型类与使用泛型函数和泛型接口的方式类似。首先,我们需要在实例化泛型类时指定类型参数。
例如,如果我们有一个泛型类MyClass<T>
,我们可以这样使用它:
// 定义泛型类
class MyClass<T> {
value: T;
constructor(value: T) {
this.value = value;
}
}
// 实例化泛型类
const myInstance = new MyClass<string>("Hello, world!");
// 访问泛型类的属性
console.log(myInstance.value); // 输出:Hello, world!
在这个例子中,我们定义了一个泛型类MyClass
,并使用T
作为类型参数。类有一个属性value
,它的类型为T
。我们可以在构造函数中使用value
初始化该属性。
在实例化泛型类时,我们必须为T
指定一个类型参数。在本例中,我们实例化了一个MyClass
的实例,并传入了一个字符串类型的参数"Hello, world!"
。
最后,我们通过访问实例的value
属性,输出了"Hello, world!"
。
如果我们要使用一个非常通用的类型参数,比如any
,那么我们可以省略类型参数的指定,让 TypeScript 推断类型。例如
// 实例化泛型类并省略类型参数
const myInstance = new MyClass("Hello, world!");
// 访问泛型类的属性
console.log(myInstance.value); // 输出:Hello, world!
在这个例子中,我们实例化了一个MyClass
的实例,并传入了一个字符串类型的参数"Hello, world!"
。由于我们没有指定类型参数,TypeScript 推断出T
的类型为string
。
泛型约束
泛型约束用于限制传入的类型参数必须符合某些条件,以便在函数或类中使用。
泛型约束可以通过以下两种方式实现:
- 通过继承接口的方式实现泛型约束
- 使用
keyof
关键字对泛型类型参数进行约束
下面分别介绍这两种方式。
通过继承接口的方式实现泛型约束
通过继承接口的方式实现泛型约束,可以保证传入的类型参数符合接口定义的要求。
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // Now we know it has a .length property, so no more error
return arg;
}
loggingIdentity("hello"); // Output: 5
在上面的例子中,loggingIdentity
函数接受一个泛型参数T
,并对T
进行约束,使其必须符合Lengthwise
接口定义的要求,即具有length
属性。
通过这种方式,我们可以在函数中直接使用传入参数的length
属性,而不需要进行类型判断。
使用keyof
关键字对泛型类型参数进行约束
使用keyof
关键字对泛型类型参数进行约束,可以限制传入的类型参数必须是指定类型的属性之一。
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a"); // OK
getProperty(x, "e"); // Error
在上面的例子中,getProperty
函数接受两个参数,一个是对象obj
,一个是属性名key
。通过使用keyof T
,可以限制传入的类型参数K
必须是对象T
的属性之一。这样可以保证obj[key]
的类型是有效的。
需要注意的是,在使用keyof
关键字时,属性名必须是一个已知的类型。因此,如果要使用一个变量作为属性名,可以使用索引类型查询操作符[]
。
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
let key = "a";
getProperty(x, key); // Error
getProperty(x, key as keyof typeof x); // OK
在上面的例子中,变量key
的类型是string
,无法直接作为getProperty
函数的第二个参数传入。可以使用keyof typeof x
将key
转换为"a" | "b" | "c" | "d"
类型,即对象x
的属性之一。
能看到这里,恭喜你大兄弟 你已经超越了70%的人
泛型默认值
在 TypeScript 中,我们可以为泛型类型参数指定默认值,当在使用泛型时未显式传入该参数的值时,将使用指定的默认值。语法如下:
function functionName<T = defaultValue>(args: T): void {
// 函数体
}
其中 <T = defaultValue>
表示泛型类型参数 T 有一个默认值 defaultValue。
举个例子,我们定义一个返回数组最后一个元素的函数,如果数组为空,则返回默认值 -1
。
function last<T = number>(arr: T[]): T | number {
return arr.length > 0 ? arr[arr.length - 1] : -1;
}
console.log(last([1, 2, 3])); // 输出 3
console.log(last([])); // 输出 -1
console.log(last<string>([])); // 输出 -1
在上面的代码中,我们使用泛型类型参数 T 来表示数组的元素类型,如果没有传递泛型类型参数,则默认为 number
。在函数实现中,我们使用了默认值 -1
,当数组为空时返回该默认值。
注意,在这里我们传递了一个空数组,由于我们没有传递泛型类型参数,因此默认为 number
类型。所以即使我们传递的是一个空的 string
数组,最终的返回值依然是 -1
。
当然,如果我们显式指定泛型类型参数,比如传递了一个 string
类型的数组,那么最终的返回值将还是 -1
,因为我们使用了默认的泛型类型参数 number
。
泛型默认值的定义
泛型默认值指的是当泛型没有明确传入具体类型时,可以为泛型设置默认值,这样就可以使用默认值代替具体类型。
在定义泛型时,使用 =
号为泛型设置默认值,如下所示:
function identity<T = string>(arg: T): T {
return arg;
}
上述代码中,泛型 T
设置默认值为 string
,当没有明确传入具体类型时,T
会默认为 string
类型。
泛型默认值的使用
使用泛型默认值时,可以在不传入具体类型的情况下使用泛型。
function identity<T = string>(arg: T): T {
return arg;
}
const result1 = identity("hello"); // result1 类型为 string
const result2 = identity(123); // result2 类型为 number
const result3 = identity(); // result3 类型为 string,因为 T 默认为 string 类型
上述代码中,调用 identity
函数时,第一个参数传入字符串,第二个参数传入数字,第三个参数不传入具体类型。因为泛型 T
设置了默认值为 string
,所以第三个参数的类型为 string
。
能看到这里,恭喜你大兄弟 你已经超越了90%的人
泛型类型推断
类型推断的概念
类型推断指的是 TypeScript 在没有明确指定类型的情况下,根据变量的使用上下文自动推断变量的类型。这个过程是在编译期间进行的。
泛型类型推断的规则
在函数调用时,如果没有明确指定泛型类型参数,TypeScript 会根据传入的参数自动推断泛型类型参数的类型。
function identity<T>(arg: T): T {
return arg;
}
const result = identity("hello"); // result 类型为 string
上述代码中,调用 identity
函数时,传入参数 "hello"
,TypeScript 会自动推断出 T
类型为 string
。
此外,TypeScript 还支持从函数返回值推断泛型类型参数的类型。
function createArray<T>(length: number, value: T): T[] {
return Array.from({ length }, () => value);
}
const result = createArray(3, "hello"); // result 类型为 string[]
上述代码中,调用 createArray
函数时,传入参数 3
和 "hello"
,TypeScript 会自动推断出 T
类型为 string
,从而使 result
的类型为 string[]
。
泛型在函数中的应用
在 TypeScript 中,我们可以使用泛型来创建可重用的函数。通过将类型参数传递给函数,我们可以确保该函数适用于多种不同的类型,而不是只适用于一种类型。
泛型函数的定义如下:
function identity<T>(arg: T): T {
return arg;
}
在这个例子中,<T>
表示类型参数。这意味着 identity
函数接受一个类型为 T
的参数,并返回一个类型为 T
的值。
调用泛型函数时,我们可以传递不同的类型参数:
let output1 = identity<string>("hello");
let output2 = identity<number>(42);
在这个例子中,我们将 string
和 number
作为类型参数传递给 identity
函数,并且它们都工作得很好。
泛型在类中的应用
与函数一样,我们可以在 TypeScript 中使用泛型来创建可重用的类。通过将类型参数传递给类,我们可以确保该类适用于多种不同的类型,而不是只适用于一种类型。
泛型类的定义如下:
class GenericClass<T> {
private _value: T;
constructor(value: T) {
this._value = value;
}
getValue(): T {
return this._value;
}
}
在这个例子中,<T>
表示类型参数。这意味着 GenericClass
类接受一个类型为 T
的参数,并且具有一个名为 _value
的私有属性和一个名为 getValue
的公共方法,它返回一个类型为 T
的值。
调用泛型类时,我们可以传递不同的类型参数:
let stringClass = new GenericClass<string>("hello");
let numberClass = new GenericClass<number>(42);
在这个例子中,我们将 string
和 number
作为类型参数传递给 GenericClass
类,并且它们都工作得很好。
泛型在接口中的应用
与函数和类一样,我们可以在 TypeScript 中使用泛型来创建可重用的接口。通过将类型参数传递给接口,我们可以确保该接口适用于多种不同的类型,而不是只适用于一种类型。
泛型接口的定义如下:
interface GenericInterface<T> {
value: T;
getValue(): T;
}
在这个例子中,<T>
表示类型参数。这意味着 GenericInterface
接口有一个名为 value
的类型为 T
的属性和一个名为 getValue
的方法,它返回一个类型为 T
的值。
调用泛型接口时,我们可以传递不同的类型参数:
let stringInterface: GenericInterface<string> = {
value: "hello",
getValue() {
return this.value;
},
};
let numberInterface
能看到这里,恭喜你大兄弟 你已经超越了98%的人
泛型的局限性
尽管泛型非常灵活,但是仍然存在一些局限性。以下是一些常见的泛型局限性:
- 不能使用原始类型作为类型参数。例如,无法声明一个
Array<number>
的数组类型,该数组中的元素是number
或string
类型。如果尝试这样做,则会出现编译器错误。 - 无法访问类型参数的属性或方法。由于类型参数可能是任何类型,编译器无法推断类型参数具有哪些属性或方法。因此,在使用泛型时,只能访问类型参数的公共成员。
- 不能使用运算符。由于泛型是在运行时确定的,因此无法在泛型类型上使用运算符,例如
+
或-
。
如何提高泛型的使用
以下是一些可以提高泛型使用的技巧:
- 使用泛型约束。泛型约束可以限制类型参数的类型范围,从而可以在类型参数上使用属性和方法。
- 使用默认类型参数。默认类型参数允许在调用泛型时省略类型参数,并使用默认值代替。
- 使用泛型类型推断。泛型类型推断可以让编译器自动推断类型参数的类型。
- 编写通用的代码。尽量编写可复用的通用代码,以便在需要时可以轻松地重复使用。