TypeScript 中的泛型作用域、类型推论、多个类型参数

2,403 阅读5分钟

这是我参与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 只能在函数内部使用,如果在函数外部使用的话,会报错。

image.png

习题:关于泛型的作用域,说法错误的是?

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]);

这样的话,就有正确的类型,它的返回值也可以被正确推论出类型。

image.png

当然我们可以省略掉,这里定义的类型,有类型推论根据函数的输入,自动推论出函数的输出。

习题:在空白处填上合适的类型

const animalInfo = <T, R>(arg1: T, arg2: R) : {
  age: T; name: R;
} => {
  return {
    age: arg1,
    name: arg2
  };
};

animalInfo<______, ______>(10, 'panda');

答案:

number

string

解析:

本题目比较简单,多泛型参数显示指定参数类型。函数 animalInfo 的两个参数类型分别是 numberstring。所以本题的答案为 numberstring

默认类型

下面我们来学习泛型的默认类型,还是以创建数组的函数为例子,这个函数是需要传入两个参数的,第一个是数组的每一项的值,第二个是数组的长度,如果我们希望这个函数在调用的时候,可以是零个参数,就可以那么这两个参数都可以设为可选的,然后再改造下它。

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();

调用函数,我们在调用的时候就可以不传参数了。

但是这个时候,我们发现,这个泛型被推论成下面这样。

image.png

我们当然可以使用 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 中已经指定了泛型的默认值 TnumberRstring.

在第一次调用中并没有传参数,因此泛型 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 });