概念
先看看我从别处嫖过来的概念解释:
泛型指的是类型参数化,即将原来某种具体的类型进行参数化。和定义函数参数一样,我们可以给泛型定义若干个类型参数,并在调用时给泛型传入明确的类型参数。设计泛型的目的在于有效约束类型成员之间的关系,比如函数参数和返回值、类或者接口成员和方法之间的关系。
这概念真的长,而且第一次看还比较难理解,等通篇文章看完后再回过头来看,应该就会好理解一些了。(PS:面试应该也喜欢问这个概念)
先举个例子,假如要实现一个函数,输入一个数字A和数值B,能够输出一个以A为长度,每个子项为B的数组。一般可以这么实现:
function createArray(length: number, value: any): Array<any> {
let result = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
createArray(3, 'x'); // 返回['x', 'x', 'x']
点评:这个方法缺点是即没有准确的定义输入的子项是什么类型,也没有定义返回的数组中子项是什么类型,还有就是any
的使用放弃了类型检查。如果把any类型改为具体的类型,那么这个方法又只能局限于这个固定类型,没有很好的复用性。
方案:使用泛型<X>
,TS会根据传入的值自动推导类型来说明输入的子项是什么类型,返回的数组子项是什么类型,同时<X>
内不必要细写类型:
function createArray<T>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
createArray(3, 'x'); // ['x', 'x', 'x']
通过泛型的定义,可以解决类、接口、方法的复用性,以及对未来新数据类型的支持!
再来个简单的例子,来更清晰了解泛型的运作:
function getVal<T>(val: T): T{
return val;
}
getVal<string>('wow') // 泛型输入了字符串,那么与泛型有关的就被固定了字符串类型
getVal<string>(123) // 这样就会报错,入参不能为数字类型
多个泛型定义
function createTuple<T, U>(tuple: [T, U]): [U, T] {
return [tuple[1], tuple[0]];
}
createTuple([7, 'seven']); // ['seven', 7]
默认泛型
可以给泛型写默认的类型。
interface FN<State = { id: number; name: string }> {
state: State
}
type FN1= FN // 相当于FN<{ id: number; name: string; }>
注意:当有多个入参时,每个定义了一个默认泛型其他的也要一起定义。
泛型约束
就是通过一些方法去把泛型的定义范围再进一步缩小。例如实现一个函数,在用泛型的情况下,只能给泛型输入字符串或数组,打印出入参变量的长度并返回自身:
function fn<T> (val: T) : T {
console.log(val.length) // 问题:当写到这里时会报错,因为只有数组和字符串有这个属性,其他类型没有
return val
}
方案:通过继承extends
来约束泛型:
interface Num{
length: number;
}
function fn<T extends Num>(x:T): T { // 此时的泛型就被约束了
console.log(x.length);
return x;
}
// 当入参变量没有length属性,才会报错。
多写几个例子:
function FN<P extends number | string | boolean>(param: P):P {
return param;
} // 限定了泛型入参只能是 number | string | boolean 的子集
interface FN1<State extends { id: number; name: string }> {
state: State
} // 泛型仅接收 { id: number; name: string } 接口类型的子类型作为入参。
interface FN2<State extends {} = { id: number; name: string }> {
state: State
} // 入参 State 必须是 {} 类型的子类型,同时也指定了入参缺省时的默认类型是接口类型 { id: number; name: string; }
给接口设置泛型
interface dynamicObj<T, U> {
key: T
value: U
}
let obj1: dynamicObj<number, string> = { key: 1, value: '1' }
let obj2: dynamicObj<string, number> = { key: '1', value: 1 }
提高了接口定义的灵活度。
注意:如果设置类的接口泛型,那么在按照该接口定义类的时候,这个类也需要设置泛型:
interface InterFn<T>{}
class Fn<T> implements InterFn<T>{}
给类设置泛型(泛型类)
class Person<T> {
num: T; // 存入一个数字
constructor(num: T) {
this.num = num;
}
showX(x: T): T {
// 输入一个值,返回一个值
return x;
}
}
let p = new Person<number>(1);
p.showX(1);
给类的定义提高了灵活度,也叫泛型类。
作为变量类型的泛型类型
例如,定义一个函数的类型:
let FN: <P>(param: P) => P; // 用泛型定义函数类型
function fn<P>(param: P): P { // 写个符合变量FN类型的函数
return param;
}
FN = fn; // 可以正常赋予
当然,把这些泛型类型用类型别名或者接口去定义会更好一些:
type FN1 = <P>(param: P) => P;
interface FN2 {
<P>(param: P): P
}
泛型拓展
可以给泛型加入一些表达式进行拓展:
type StringOrNumberArray<E> = E extends string | number ? E[] : E;
type StringArray = StringOrNumberArray<string>; // 类型是 string[]
type NumberArray = StringOrNumberArray<number>; // 类型是 number[]
type NeverGot = StringOrNumberArray<boolean>; // 类型是 boolean
通过extends
继承和三元表达式,我们作出了一个这样的效果,当输入为字符串或数字类型时,被定为数组类型,当输入为其他类型时被定义为输入的类型。
尾巴
感觉泛型的灵活运用需要大量的实践才能灵活运用,掌握精髓。