8-1. 在函数中使用泛型
前言
任务列表
- 掌握在函数中如何使用泛型
notes
有时,书写某个函数时,会丢失一些类型信息(多个位置的类型应该保持一致或有关联的信息)
泛型的概念:附属于函数、类、接口、类型别名之上的类型。
泛型好比类型变量:泛型相当于是一个类型变量,在定义时,无法预先知道具体的类型,可以用该变量占位,只有到调用时,才能确定它的类型。
智能推导:很多时候,TS 会智能地根据传递的参数,推导出泛型的具体类型。
默认值:泛型可以手动设置默认值
若没有手动设置默认值,并且无法完成推导,调用函数时也没有传递具体的类型,泛型默认为空对象~~{}~~
。
:::danger
这句话在最新版的 ts 中是有误的,默认应该为 unknown
类型,而非一个空对象。
:::
在函数中使用泛型:函数名 <泛型名称>{ 函数体 }
codes
编写一个 take 函数,要求:
- 参数1:arr 数组
- 参数2:n 数组的前 n 项
功能:从数组 arr 中,取前 n 项返回。
function take(arr, n) {
if (n >= arr.length) return arr;
const newArr = [];
for (let i = 0; i < arr.length; i++) {
const item = arr[i];
newArr.push(item);
}
return newArr;
}
const newArr = []
这种写法,TS 会认为我们声明了一个 newArr 数组,该数组始终为一个空数组 never[]
。
对于 never[]
类型的数组,我们没法往里边添加任何内容。
“any”类型的参数不能赋给“never”类型的参数。
function take(arr, n) {
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;
}
从错误提示中可得知 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;
}
虽然我们已经尽可能地添加上了合适的类型约束,但是还存在一个问题:
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);
我们调用的时候传入的是一个 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);
识别步骤应该是这样的:
- 当我们调用 take 时,传入的第一个参数,它识别为
number[]
- 就相当于告诉
arr: T[]
中的T
是number[]
- 然后将 take 中所有泛型(类型变量)T 都识别为
number[]
我们也可以将泛型视作一个类型变量,在调用 take 函数的时候,将对应类型传递进去。
泛型默认值
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;
}
此时无法完成类型推导,无法得知泛型 T 应该是什么类型,T 将取默认值 unknown
类型。
如果我们指定了 T 的默认值,那么在这种情况下,默认值将为我们指定的类型。
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
中的泛型,不存在关联。
解决办法非常简单,就是将泛型信息给提升一下。
试问:提升到什么位置呢?
可以理解为提升到 take
、shuffle
的共同父级(类 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));
细节 - 泛型作用域问题
此时我们给类添加了泛型,并且它和函数 take、shuffle 的泛型重名了,发生了冲突。
就近原则,函数的泛型会覆盖类的泛型,所以我们要将原先定义在函数身上的泛型给去掉。
仔细观察 vscode 给我们提供的智能提示,会发现此时 ArrayHelper<T>
中的泛型没有高亮显示。
接口<泛型名称>
这种写法,在介绍如何写数组的约束时就接触到了,只不过那时不知道已经用到了泛型。
当我们想要约束一个数字类型的数组时,通常可以采用下面两种写法:
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
上述程序存在的一些隐患:
- 函数
nameToUpperCase
没法确认传入的参数user
是否含有name
属性 - 在编写
nameToUpperCase
函数体的时候,由于无法识别user.name
的类型,没有智能提示,容易写错api
,比如split
、substr
、toUpperCase
、join
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
依据推断出来的 user
字段,它是满足条件的,可以作为函数 nameToUpperCase
的参数传入。
如果传入的值不满足条件,那么立刻就会有错误提示:
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' ]
自动完成类型推导:
(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);
结合打印结果,了解一下我们需要实现的这几个 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);
}
}
核心数据结构:数组
需要准备两个数组:
_keys
:存放 key_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);
});
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);
}
}
}