8-1. 在函数中使用泛型

前言

任务列表

  • 掌握在函数中如何使用泛型

notes

有时,书写某个函数时,会丢失一些类型信息(多个位置的类型应该保持一致或有关联的信息)

泛型的概念:附属于函数、类、接口、类型别名之上的类型。

泛型好比类型变量:泛型相当于是一个类型变量,在定义时,无法预先知道具体的类型,可以用该变量占位,只有到调用时,才能确定它的类型。

智能推导:很多时候,TS 会智能地根据传递的参数,推导出泛型的具体类型。

默认值:泛型可以手动设置默认值

若没有手动设置默认值,并且无法完成推导,调用函数时也没有传递具体的类型,泛型默认为空对象~~{}~~

:::danger 这句话在最新版的 ts 中是有误的,默认应该为 unknown 类型,而非一个空对象。 :::

在函数中使用泛型:函数名 <泛型名称>{ 函数体 }

codes

编写一个 take 函数,要求:

  1. 参数1:arr 数组
  2. 参数2:n 数组的前 n 项

功能:从数组 arr 中,取前 n 项返回。

  1. function take(arr, n) {
  2. if (n >= arr.length) return arr;
  3. const newArr = [];
  4. for (let i = 0; i < arr.length; i++) {
  5. const item = arr[i];
  6. newArr.push(item);
  7. }
  8. return newArr;
  9. }

image.png

const newArr = []

这种写法,TS 会认为我们声明了一个 newArr 数组,该数组始终为一个空数组 never[]

对于 never[]类型的数组,我们没法往里边添加任何内容。

image.png

“any”类型的参数不能赋给“never”类型的参数。

  1. function take(arr, n) {
  2. if (n >= arr.length) return arr;
  3. const newArr: any[] = [];
  4. for (let i = 0; i < arr.length; i++) {
  5. const item = arr[i];
  6. newArr.push(item);
  7. }
  8. return newArr;
  9. }

从错误提示中可得知 item 被识别为一个 any 类型的数据,我们想要往 newArr 中添加成员,只要将其设置为一个 any[]即可。

未使用泛型

function take(arr: any[], n: number): any[] {
  if (n >= arr.length) return arr;
  const newArr: any[] = [];
  for (let i = 0; i < arr.length; i++) {
    const item = arr[i];
    newArr.push(item);
  }
  return newArr;
}

虽然我们已经尽可能地添加上了合适的类型约束,但是还存在一个问题:

image.png

3个 any[],无法确保它们都是相同的类型。这句话不易理解,请看下边的代码:

function take(arr: any[], n: number): any[] {
  if (n >= arr.length) return arr;
  const newArr: any[] = [];
  for (let i = 0; i < arr.length; i++) {
    const item = arr[i];
    newArr.push(item);
  }
  return newArr;
}

const newArr = take([1, 2, 3], 2);
newArr.forEach(item => {
  // item.
  // 此时调用 item. 不会罗列出 Number.prototype 上可以用的所有 api
})
console.log(newArr);

image.png
我们调用的时候传入的是一个 number[],我们希望 take 中的 3 个 any[]都识别为 number[]

但是由于按照上述写法, take 中的 3 个 any[] 是相互独立的,它们之间并没有关联。

使用泛型

function take<T>(arr: T[], n: number): T[] {
  if (n >= arr.length) return arr;
  const newArr: T[] = [];
  for (let i = 0; i < arr.length; i++) {
    const item = arr[i];
    newArr.push(item);
  }
  return newArr;
}

const newArr = take([1, 2, 3], 2);
newArr.forEach(item => {
  // item.
  // 此时调用 item. 会罗列出 Number.prototype 上可以用的所有 api
})
console.log(newArr);

image.png

识别步骤应该是这样的:

  1. 当我们调用 take 时,传入的第一个参数,它识别为 number[]
  2. 就相当于告诉 arr: T[] 中的 Tnumber[]
  3. 然后将 take 中所有泛型(类型变量)T 都识别为 number[]

image.png

我们也可以将泛型视作一个类型变量,在调用 take 函数的时候,将对应类型传递进去。

image.png

泛型默认值

function take<T = string>(arr: T[], n: number): T[] {
  if (n >= arr.length) return arr;
  const newArr: T[] = [];
  for (let i = 0; i < arr.length; i++) {
    const item = arr[i];
    newArr.push(item);
  }
  return newArr;
}

image.png

此时无法完成类型推导,无法得知泛型 T 应该是什么类型,T 将取默认值 unknown 类型。

如果我们指定了 T 的默认值,那么在这种情况下,默认值将为我们指定的类型。

image.png

8-2. 在类、接口、类型别名中使用泛型

前言

任务列表

  • 掌握在类、接口、类型别名中使用泛型
  • 理解泛型作用域

notes

在类、接口、类型别名中使用泛型:直接在名称后边加上<泛型名称>即可。

codes

// 回调函数:判断数组中的某一项是否满足条件
type callback<T> = (n: T, i: number) => boolean;

function filter<T>(
                    arr: T[], 
                    callback: callback<T>
                    ): T[] {
  const newArr: T[] = [];
  arr.forEach((n, i) => {
    if (callback(n, i)) newArr.push(n);
  });
  return newArr;
}

type callback<T>filter<T>callback: callback<T>

此时,类型别名中定义的泛型,与 filter 函数的泛型相同,它们之间是相互关联的。

类型变量 T 的值,只有在 filter 函数被调用的时候才能确定。

export class ArrayHelper {
  take<T>(arr: T[], n: number): T[] {
    if (n >= arr.length) return arr;
    const newArr: T[] = [];
    for (let i = 0; i < arr.length; i++) {
      newArr.push(arr[i]);
    }
    return newArr;
  }

  shuffle<T>(arr: T[]) {
    const len = arr.length;
    for (let i = 0; i < len; i++) {
      const t = this.getRandom(0, len);
      [arr[i], arr[t]] = [arr[t], arr[i]];
    }
  }

  private getRandom(min: number, max: number) {
    return Math.floor((max - min) * Math.random())+ min;
  }
}

缺陷:take 中的泛型和 shuffle 中的泛型,不存在关联。

解决办法非常简单,就是将泛型信息给提升一下。

试问:提升到什么位置呢?
可以理解为提升到 takeshuffle 的共同父级(类 ArrayHelper)身上。

export class ArrayHelper<T> {
  private arr: T[];

  constructor(arr: T[]) {
    this.arr = arr;
  }

  take(n: number): T[] {
    if (n >= this.arr.length) return this.arr;
    const newArr: T[] = [];
    for (let i = 0; i < n; i++) {
      newArr.push(this.arr[i]);
    }
    return newArr;
  }

  shuffle() {
    const len = this.arr.length;
    for (let i = 0; i < len; i++) {
      const t = this.getRandom(0, len);
      [this.arr[i], this.arr[t]] = [this.arr[t], this.arr[i]];
    }
  }

  print() {
    this.arr.forEach((it) => console.log(it + "\t"));
  }

  private getRandom(min: number, max: number) {
    return Math.floor((max - min) * Math.random()) + min;
  }
}

arr 改写为 this.arr
将原来的 arr 参数全部去掉,直接使用私有属性 arr 来替代。

const helper = new ArrayHelper([1, 2, 3]);
console.log("====完成初始化====");
helper.print();

console.log("====获取前2张====");
console.log(helper.take(2));

console.log("====完成洗牌====");
helper.shuffle();
helper.print();

console.log("====获取前2张====");
console.log(helper.take(2));

image.png

细节 - 泛型作用域问题 image.png
此时我们给类添加了泛型,并且它和函数 take、shuffle 的泛型重名了,发生了冲突。

就近原则,函数的泛型会覆盖类的泛型,所以我们要将原先定义在函数身上的泛型给去掉。

仔细观察 vscode 给我们提供的智能提示,会发现此时 ArrayHelper<T>中的泛型没有高亮显示。

接口<泛型名称>

这种写法,在介绍如何写数组的约束时就接触到了,只不过那时不知道已经用到了泛型。

当我们想要约束一个数字类型的数组时,通常可以采用下面两种写法:

  1. number[]
  2. Array<number>

后者其实就是 Array 接口,加 number 泛型。

8-3. 泛型约束

前言

任务列表

  • 掌握泛型约束的作用及用法

notes

泛型约束:

  • 作用:用于限制泛型的取值
  • 语法:泛型 extends xxx,要求泛型必要要满足xxx

codes

// 将传入的对象的 name 属性的首字母大写
function nameToUpperCase(user) {
  const newName = user.name
    .split(" ")
    .map((it) => it.substr(0, 1).toUpperCase() + it.substr(1))
    .join(" ");
  user.name = newName;
  return user;
}

const user = {
  name: "da hu you",
  age: 23,
  gender: "男",
};

const newUser = nameToUpperCase(user);

console.log(newUser.name); // Da Hu You

上述程序存在的一些隐患:

  1. 函数 nameToUpperCase 没法确认传入的参数 user 是否含有 name 属性
  2. 在编写 nameToUpperCase 函数体的时候,由于无法识别 user.name 的类型,没有智能提示,容易写错 api,比如 splitsubstrtoUpperCasejoin
interface hasNameObj {
  name: string
}

// 将传入的对象的 name 属性的首字母大写
function nameToUpperCase<T extends hasNameObj>(user: T): T {
  const newName = user.name
    .split(" ")
    .map(it => it.substring(0, 1).toUpperCase() + it.substring(1))
    .join(" ");
  user.name = newName;
  return user;
}

const user = {
  name: "da hu you",
  age: 23,
  gender: "男",
};

const newUser = nameToUpperCase(user);

console.log(newUser.name); // Da Hu You

<T extends hasNameObj>

要求传入的 user 必须得满足 hasNameObj

image.png

依据推断出来的 user 字段,它是满足条件的,可以作为函数 nameToUpperCase的参数传入。

如果传入的值不满足条件,那么立刻就会有错误提示:
image.png

8-4. 多泛型

前言

任务列表

  • 掌握多泛型的语法

notes

可以同时定义多个泛型,泛型和泛型之间用逗号分隔。

codes

下面用一个 demo 来介绍多泛型,demo 需求:

输入:

  • [1, 2, 3]
  • ["a", "b", "c"]

输出:[1, "a", 2, "b", 3, "c"]

按照上述要求,对两个数组进行混合操作。如果数组的长度不同,则抛出错误。

function mixinArray<T, K>(arr1: T[], arr2: K[]): (T | K)[] {
  const newArr: (T | K)[] = [];
  if (arr1.length !== arr2.length) throw new Error("两个数组长度不等");
  for (let i = 0; i < arr1.length; i++) {
    newArr.push(arr1[i]);
    newArr.push(arr2[i]);
  }
  return newArr;
}

const result = mixinArray([1, 2, 3], ["a", "b", "c"]);
console.log(result); // => [ 1, 'a', 2, 'b', 3, 'c' ]

自动完成类型推导:
image.png

(T | K)[] 数组中可以是 T 类型或者 K 类型。

8-5. 练习:自定义字典类

前言

任务列表

  • 独立完成本节练习

开发一个字典类(Dictionary),字典中会保存键值对的数据

键值对数据的特点:

  • 键(key)可以是任何类型,但不允许重复
  • 值(value)可以是任何类型
  • 每个键对应一个值
  • 所有的键类型相同,所有的值类型相同

字典类中对键值对数据的操作:

  • delete按照键,删除对应的键值对
  • forEach循环每一个键值对
  • size得到当前键值对的数量
  • has判断某个键是否存在
  • set重新设置某个键对应的值,如果不存在,则添加

袁老版本

import { Dictionary } from "./dictionary";

const dic = new Dictionary<string, number>();

dic.set("a", 1);
dic.set("b", 2);
dic.set("a", 11);
dic.set("c", 33);

dic.forEach((k, v) => {
    console.log(`${k}:${v}`);
})
console.log("当前键值对数量:" + dic.size);

console.log("b 是否存在:" + dic.has("b"));
console.log("删除键b")
dic.delete("b");
console.log("b 是否存在:" + dic.has("b"));

dic.forEach((k, v) => {
    console.log(`${k}:${v}`);
})
console.log("当前键值对数量:" + dic.size);

image.png

结合打印结果,了解一下我们需要实现的这几个 api 的功能

  • set
  • forEach
  • delete
  • has
  • size
export type CallBack<T, U> = (key: T, val: U) => void;

export class Dictionary<K, V> {
  private keys: K[] = [];
  private vals: V[] = [];

  get size() {
    return this.keys.length;
  }

  set(key: K, val: V) {
    const i = this.keys.indexOf(key);
    if (i < 0) {
      this.keys.push(key);
      this.vals.push(val);
    } else {
      this.vals[i] = val;
    }
  }

  forEach(callback: CallBack<K, V>) {
    this.keys.forEach((k, i) => {
      const v = this.vals[i];
      callback(k, v);
    });
  }

  has(key: K) {
    return this.keys.includes(key);
  }

  delete(key: K) {
    const i = this.keys.indexOf(key);
    if (i === -1) {
      return;
    }
    this.keys.splice(i, 1);
    this.vals.splice(i, 1);
  }
}

核心数据结构:数组

需要准备两个数组:

  1. _keys:存放 key
  2. _vals:存放 val

⚠️ 数组中的顺序要对应 key-val

我的版本

import { Dictionary } from "./dictionary";

const dic = new Dictionary<string, number>();

// 测试 set、forEach
dic.set("a", 1);
dic.set("b", 2);
dic.set("c", 3);
dic.set("d", 4);

dic.forEach((k, v) => {
  console.log(k, v);
});

// 测试 delete、has、size
console.log("是否含有 d:", dic.has("d"));
console.log("还有多少个键值对:", dic.size);
dic.delete("d");
console.log("是否含有 d:", dic.has("d"));
console.log("还有多少个键值对:", dic.size);

dic.forEach((k, v) => {
  console.log(k, v);
});

// 测试 get
console.log("给 a 重新赋值之前:", dic.get("a"));
dic.set("a", 10);
console.log("给 a 重新赋值之前:", dic.get("a"));

dic.forEach((k, v) => {
  console.log(k, v);
});

image.png

type callback<T, U> = (key: T, val: U) => void;

export class Dictionary<K, V> {
  private _keys: K[] = [];
  private _vals: V[] = [];

  // 判断是否存在指定 key
  has(key: K) {
    return this._keys.includes(key);
  }

  // 删除指定 key-val
  delete(key: K) {
    const targetIndex = this._keys.indexOf(key);
    if (targetIndex === -1) return;
    this._keys.splice(targetIndex, 1);
    this._vals.splice(targetIndex, 1);
  }

  // 设置 key-val
  set(key: K, val: V) {
    const targetIndex = this._keys.indexOf(key);
    if (targetIndex !== -1) {
      this._vals[targetIndex] = val;
    } else {
      this._keys.push(key);
      this._vals.push(val);
    }
  }

  // 依据指定 key 获取 val
  get(key: K) {
    const targetIndex = this._keys.indexOf(key);
    if (targetIndex === -1) return;
    else return this._vals[targetIndex];
  }

  // 获取键值对的数量
  get size() {
    return this._keys.length;
  }

  forEach(callback: callback<K, V>) {
    for (let i = 0; i < this._keys.length; i++) {
      const k = this._keys[i];
      const v = this._vals[i];
      callback(k, v);
    }
  }
}