这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战
泛型的作用域
下面定义的 T 这个类型,只有在函数调用之后,才知道具体调用是什么类型。
function createArray<T>(value: T, length: number): T[] {
let arr = [];
for (let i = 0; i < length; i++) {
arr[i] = value;
}
return arr;
}
let fooArray = createArray<string>('foo', 3);
console.log(fooArray);
// ['foo', 'foo', 'foo']
复制代码
所以 T 只能在函数内部使用,比如上面定义的临时变量 arr,其实它也应该是每一项都为 T 的数组,但是由于定义时,没有加上类型,所以它就被推论成每一项都为 any 的数组,我们可以给它加上类型,这样它就有自己正确的类型了。
function createArray<T>(value: T, length: number): T[] {
// 修改的地方
let arr: T[] = [];
for (let i = 0; i < length; i++) {
arr[i] = value;
}
return arr;
}
let fooArray = createArray<string>('foo', 3);
console.log(fooArray);
// ['foo', 'foo', 'foo']
复制代码
再次说明,T 只能在函数内部使用,如果在函数外部使用的话,会报错。
习题:关于泛型的作用域,说法错误的是?
A. 泛型具有全局作用域
B. 泛型具有局部作用域
C. 泛型函数中泛型具有块级作用域
答案:
A
解析:
泛型的作用域是局部的而不是全局的。比如我们在泛型函数或泛型类中使用泛型,那么我们就只能在内部使用,而不能在外部使用。泛型函数和泛型类中泛型的(局部)作用域是块级作用域。内部闭环,离开函数或者类则无法访问。
泛型类将在后面会讲到。
泛型中的类型推论
TS 中的类型推论,也可以应用到泛型中,我们来看这个 createArray,它是手动传入 string 类型,但其实在调用它的时候,就知道传入的是一个 字符串的 'foo',那我们把<string>
给删掉的话,可以看看,它还是可以正确的推论出 string 类型。
function createArray<T>(value: T, length: number): T[] {
// 修改的地方
let arr: T[] = [];
for (let i = 0; i < length; i++) {
arr[i] = value;
}
return arr;
}
let fooArray = createArray('foo', 3);
console.log(fooArray);
// ['foo', 'foo', 'foo']
复制代码
习题:根据代码块,求变量 animal
的类型
const animalInfo = <T>(arg: T) : {
age: T; name: string;
} => {
return {
age: arg,
name: 'panda'
};
};
const animal = animalInfo(10);
// A
{
age: string;
name: string;
}
// B
{
age: number;
name: string;
}
// C
{
age: T;
name: string;
}
// D
{
age: any;
name: string;
}
复制代码
答案:
B
解析:
函数 animalInfo
的返回值类型为
{
age: T;
name: string;
}
复制代码
泛型 T
的类型为函数 animalInfo
的参数类型。
const animal = animalInfo(10);
复制代码
所以变量 animal
的类型为 number
。
多个类型参数
下面我们来看看这样一个例子,首先定义了一个函数 swap,也就是一个交换的意思,它能接收一个元组,然后把它的第 0 项和第 1 项,交换一下,再返回,那么输出的结果,就是 [7, 'foo']
。
function swap(tuple) {
return [tuple[1], tuple[0]];
}
let swapped = swap(['foo', 7]);
// [7, 'foo']
复制代码
由于我们没有定义任何类型,所以这里的 swap 自然被推论为输入为 any,输出为 any 的数组,在使用它的第 0 项的时候,就无法预知它的类型了。
这个时候,需要添加泛型的话,就需要多个泛型参数。
function swap<T, U>(tuple: [T, U]): [U, T] {
return [tuple[1], tuple[0]];
}
let swapped = swap<string, number>(['foo', 7]);
复制代码
这样的话,就有正确的类型,它的返回值也可以被正确推论出类型。
当然我们可以省略掉,这里定义的类型,有类型推论根据函数的输入,自动推论出函数的输出。
习题:在空白处填上合适的类型
const animalInfo = <T, R>(arg1: T, arg2: R) : {
age: T; name: R;
} => {
return {
age: arg1,
name: arg2
};
};
animalInfo<______, ______>(10, 'panda');
复制代码
答案:
number
string
解析:
本题目比较简单,多泛型参数显示指定参数类型。函数 animalInfo
的两个参数类型分别是 number
和 string
。所以本题的答案为 number
和 string
。
默认类型
下面我们来学习泛型的默认类型,还是以创建数组的函数为例子,这个函数是需要传入两个参数的,第一个是数组的每一项的值,第二个是数组的长度,如果我们希望这个函数在调用的时候,可以是零个参数,就可以那么这两个参数都可以设为可选的,然后再改造下它。
function createArray<T>(value?: T, length?: number): T[] {
if (arguments.length === 0) {
return [];
}
let arr: T[] = [];
for (let i = 0; i < length; i++) {
arr[i] = value;
}
return arr;
}
let emptyArray = createArray();
复制代码
调用函数,我们在调用的时候就可以不传参数了。
但是这个时候,我们发现,这个泛型被推论成下面这样。
我们当然可以使用 string 类型给它指定上泛型的类型。
let emptyArray = createArray<string>();
复制代码
另外一种方式,就是利用到泛型的默认类型。
// 修改下面这行
function createArray<T = string>(value?: T, length?: number): T[] {
if (arguments.length === 0) {
return [];
}
let arr: T[] = [];
for (let i = 0; i < length; i++) {
arr[i] = value;
}
return arr;
}
let emptyArray = createArray();
复制代码
这样的话,即使这里不指定 string 类型,它也会被推论成每一项都是 string 的数组。
习题:根据代码块,填写泛型 T
的类型
const animalInfo = <T = number, R = string>(arg1?: T, arg2?: R) : {
age: T | undefined; name: R | undefined;
} => {
return {
age: arg1,
name: arg2
};
};
// 本次调用,泛型 T 的类型为?
animalInfo();
// 本次调用,泛型 T 的类型为?
animalInfo('10', 'panda');
复制代码
答案:
number
string
解析:
在函数 animalInfo
中已经指定了泛型的默认值 T
为 number
,R
为 string
.
在第一次调用中并没有传参数,因此泛型 T
是默认类型 number
。
animalInfo()
复制代码
在第二次调用中,第一个参数指定了类型为 string
,因此泛型 T
是的类型为 string
。
animalInfo('10', 'panda');
复制代码
资料:泛型约束
我们在函数中使用泛型的时候,由于预先并不知道泛型的类型,所以不能随意访问相应类型的属性或方法。
function loggingIdentity<T>(arg: T): T {
console.log(arg.length); // Error: T doesn't have .length
return arg;
}
复制代码
上述例子中,由于 T 不一定包含 length 属性(例如:number 类型就没有 length 属性),所以会报错。
这时候我们可以利用 interface 来对泛型进行约束。
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
复制代码
我们使用了 extends 约束了泛型 T 必须符合接口 Lengthwise 的形状,也就是必须包含 length 属性。 此时如果调用 loggingIdentity 的时候,传入的 arg 不包含 length,那么在编译阶段就会报错了:
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
loggingIdentity(7);
// index.ts(10,17): error TS2345: Argument of type '7' is not assignable to parameter of type 'Lengthwise'.
复制代码
多个类型参数之间也可以互相约束:
function copyFields<T extends U, U>(target: T, source: U): T {
for (let id in source) {
target[id] = (<T>source)[id];
}
return target;
}
let x = { a: 1, b: 2, c: 3, d: 4 };
copyFields(x, { b: 10, d: 20 });
复制代码