# 泛型
# 什么是泛型,为什么需要泛型
在了解泛型之前我们先看一个需求,假如我们需要创建一个函数,输入什么值,就输出这个值。我们的实现方式可能如下:
let echo = (arg:number):number => {
return arg;
};
echo(2)
上面的代码中,我们给函数参数和返回值都固定成了number类型,这样输入一个数字就能够返回这个数字。但是假如我们需要输入一个字符串,使用上面那个函数就会报错了。也就是说,我们不能固定输入参数的类型,那么新的实现方式如下:
let echo = (arg:any):any => {
return arg;
};
echo(2);
echo('helllo');
通过使用any
类型,我们可以实现输入什么输出什么,但是这样的话我们就容易丢失一些信息,我们要求返回的参数类型和传入的参数类型应该是相同的,然而使用any时,返回的参数类型可以与输入类型不同。也就是说我们现在的需求需要满足两个条件:
- 输入和返回的参数类型不能固定
- 输入和返回的参数类型必须相同
这时候,我们就可以使类型变量,它是一种特殊的变量,只用于表示类型而不是值。
let echo = <T>(arg:T):T => {
return arg;
};
echo(111);
echo('222');
在定义函数前,我们给函数添加一个类型变量T
,这个类型变量T会捕捉用户传入的类型。之后我们就可以在我们想要使用的地方使用这个T
来代替具体的类型。这样的话,用户不需要自己去定义具体类型,也能够确保返回类型和输入类型相同。
总结:
- 什么是泛型? 所谓泛型就是使用了参数化的类型,更通俗地说就是数据类型被指定为一个参数(T)。
- 什么时候使用泛型? 当我们对返回值和输入值的类型要求相同时使用。
# 泛型的定义和使用
泛型的定义 所有泛型方法声明都有一个类型参数声明部分(由尖括号分割),该类型参数声明必须在方法参数之前(也就是括号前面)。
// ES6写法
let echo = <T>(arg:T):T => {
return arg;
}
// ES5写法
function echo1 <T>(arg:T):T{
return arg;
}
泛型的使用 泛型的使用有两种方法:第一种是传入所有的参数,包括类型参数:
let result = echo<string>('helloworld');
第二种方法更加普遍,利用类型推断——即编译器会根据传入的参数自动地帮助我们确定T的类型:
let result = echo(111);
注意我们没必要使用尖括号(<>)来明确地传入类型;编译器可以查看myString的值,然后把T设置为它的类型。 类型推论帮助我们保持代码精简和高可读性。如果编译器不能够自动地推断出类型的话,只能像上面那样明确的传入T的类型,在一些复杂的情况下,这是可能出现的。
# 泛型变量
使用泛型创建了echo
这样的泛型函数时,编译器要求你在函数体内必须正确地使用这个通用的类型。换句话说,由于泛型可以是任意类型,你必须考虑传入任意类型时,函数是否会报错。示例:
let echo = <T>(arg:T):T => {
console.log(arg.length);
return arg;
}
echo泛型函数,我们不仅希望返回与输入值相同的类型,同时我们还希望能够打印输入值的长度。但是由于输入值可以是number
类型,而number
类型的数据是没有length
属性的,这时候就会报错。编译器也会进行如下提示:
但是,现在假如我们想要操作的是T类型的数组,而不直接是T
let echo4 = <T>(arg:T[]):T[] => {
console.log(arg.length);
return arg;
};
console.log(echo4([1,2]));
由于我们操作的是数组,所以.length
属性是应该存在的,不会出现报错。也就是说,我们不仅可以直接使用泛型变量T,也可以把泛型变量T当做类型的一部分使用,而不是这个类型,从而增加了类型的灵活性。
# 定义泛型函数类型
所有泛型函数都有一个类型参数声明部分(由尖括号分割),该类型参数声明必须在方法参数之前(也就是括号前面)。 使用ES6来定义泛型函数类型
// 使用泛型定义函数类型
let echo5 : <T>(arg:T[]) => T[];
// 定义实际的函数
console.log('泛型定义函数类型');
echo5 = (arg:any) => {
return arg;
};
echo5([1,2,3]).map((item) => item.length); // error
从上面的代码中我们可以看到,使用泛型定义的函数,哪怕传入的参数是any类型。在函数调用时还是会自动获取参数类型,返回相对应的参数类型。
使用类型别名来定义泛型函数类型
我们可以通过type
关键字来定义泛型函数类型
// 使用类型别名来定义泛型函数类型
type Echo = <T>(arg:T) => T;
let echo:Echo = (arg:any) => {
return arg;
};
使用接口来定义泛型函数类型
interface Echos{
<T>(arg:T):T
};
let echo7:Echo = (arg:any) => {
return arg;
};
console.log( echo6(['1','22','333']).map((item) => item.length));
# 泛型约束
泛型约束的使用
泛型约束顾名思义主要是对泛型类型进行约束或者说限制。我们在使用泛型时,由于泛型可以是任意类型,我们有时候无法保证在函数体中正确地使用类型。比如,之前的示例中,我们无法对传入的number
类型数据获取.length
属性。因此,我们需要对泛型类型进行约束,我们不允许传入任何类型(any),而是只允许传入带有.length
属性的类型。
- 我们定义一个接口来描述约束条件:
- 使用这个接口和extends关键字来实现约束
// 定义接口来描述约束条件
interface valueWithLength {
length:number
};
// 使用extends来实现约束
let echo8 = <T extends valueWithLength>(arg:T) => {
console.log(arg.length);
return arg;
};
在泛型约束中使用类型参数 我们可以声明一个类型参数,且它被另外的类型参数所约束。 我们首先看一下为什么要在泛型约束中使用类型参数。
console.log('在类型约束中使用类型参数')
const getProp = (obj:any,prop:any) => {
return obj[prop];
};
const obj = {
a:'a',
b:'b'
};
console.log(getProp(obj,'a')); // a
console.log(getProp(obj,'c')); // undefined
从上面的代码中,我们可以看到。我们想要实现一个函数从对象中通过属性名获取对应得值。但是如果我们获取对象中不存在的属性时不会报错,而是返回undefined
。事实上,我们希望编译器能够直接进行提示,也就是说限定属性值只能从对象中已有的属性获取。
const getProp2 = <T,U extends keyof T>(obj:T,prop:U) => {
return obj[prop];
};
getProp2(obj,'a'); // a
getProp2(obj,'c'); //error Argument of type '"c"' is not assignable to parameter of type '"a" | "b"'
我们定义了两个泛型变量T
和U
,其中U
收到T
的约束。