2021前端必须掌握的TS | 8月更文挑战

1,605 阅读6分钟

TypeScript之泛型篇

前端的小伙伴每每看到这样的代码是不是想吐,究其原因是还不特别了解泛型,快来跟着铁蛋儿一起学习吧。

D9ED7721-C921-4FE8-80CC-9CCBCF6A3154

学完以后就可以开开心心恰饭了!

nice.gif

首先来看下官网给我们的泛型介绍

软件工程中,我们不仅要创建一致的定义良好的API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。

像C#和Java这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。

下面是来谈一谈我理解的泛型:

泛型可以理解为宽泛的类型,通常用于类和函数, 完了!!!

20171006297207_VrDhAT.jpg

泛型像是提取了一类事物的共性特性的抽象, 比如说松树、柳树都是树。

但是树那不一定用泛型表达,树的表达方式在程序里有3种:

  1. 接口
  2. 继承
  3. 泛型

image-20210803174838782

先从大家都了解的继承开始说松树继承于树,松树同时也是木材。如果用继承表达就是上图, 大家思考一个问题? 这样的话如果再出现一个物质的类这么办?

我们是不是还要继承,无论谁继承谁,无疑都增加了程序设计复杂度,也增加了继承关系的维护成本或者说耦合, 所以关系太强并不是特别好。很多时候没有必要非常考虑用关系表达, 这时候就可以用接口了。

接口官网是这样描述的:

接口是对行为的抽象,而具体如何行动需要由类(classes)去实现(implement)。

TypeScript中,使用接口(Interfaces)来定义对象的类型。除了可用于对类的一部分行为进行抽象以外,也常用于对「对象的形状(Shape)」进行描述。

人话: 描述某一个东西的某一个特性。

比如: 用护在支付场景支付, 用户在登陆的时候登陆, 那这两个就没必要放在用户里面写。

再比如: 松树可以生长,动植物也可以生长,但是没有必要放到一起写。

再来看下泛型:

泛型不仅仅是描述,它是对共性的提取

class TaBle<T>{
  make() {
  }
}

const A = new TaBle<红木>()
const B = new TaBle<桃木>()

泛型是可以定义函数可以进行计算,相比于继承关系也不是太强很弱很合理

  • 木头可以制造桌子,但不是所有的木头可以制造桌子。
  • 制造桌子()这个方法, 放到木头类中会很奇怪,因为木头不仅仅可以制造桌子。
  • 同理,让木头继承于 “可以制造桌子” 这个接口也很奇怪。

奇怪的代码展示:

class 红木 implements IMakeTable{
  makeBed(){....}
}

设计IMakeTable的目标是为了拆分描述事物不同的方面(Aspect), 其实还有一个更专业的词汇叫 “关注点”

拆分关注点的技巧,叫做关注点的分离。比如从架构层来说 Vue3的CompositionAPI和React hooks 都是要做关注点的分离。

如果仅仅用接口,不用泛型,那么关注点就没有做到完全解耦。

e331220d4440436f8b7eabddc6e4a841.jpeg

泛型是一种抽象共性(本质)的变成手段,它允许将类型作为其它类型的参数(表现形式),从而分离不同关注点的实现(作用)。

泛型基础:

// test函数是一个返回数字的函数
// 传入的number 返回的也是number
function test(arg: number): number {
  return arg
}

// 为了让test函数支持更多类型可以声明参数为any
function test(arg: any): any {
  return arg
}

// 就算传进去Array返回的testVal值也不是any(implicit any)
let testVal = test(Array)

// any会丢失后续所有的检查,因此可以考虑用泛型
// <>叫做钻石操作符,代表传入的参数
function test<Type>(arg: Type): Type {
  // Type声明、传入、返回必须都是string类型
  return arg
}

// let output = test<string>('string')
// 或者直接调用 也会推导出来
let output = test('string')

// 所有给output赋其它类型的值会报错
output = 100 // error

泛型类:

class AddNumber<NumType>{
  initValue: NumType;
  add: (x: NumType, y: NumType) => NumType
}
let myAddNumber = new AddNumber<number>()
myAddNumber.initValue = 1
// (number,number)=>number
myAddNumber.add = (x, y) => {
  return x + y
}

let myAddString = new AddNumber<string>()
myAddString.initValue = '1'
// (string,string)=>string
myAddString.add = (x, y) => {
  return x + y
}

官网这么说:

类有两部分:静态部分和实例部分。 泛型类指的是实例部分的类型,所以类的静态属性不能使用这个泛型类型。

注意: 不能这么干

src=http___5b0988e595225.cdn.sohucs.com_images_20190116_c0e43c51325e434ba4a972b08dbe4571.jpeg&refer=http___5b0988e595225.cdn.sohucs.jpeg

class AddNumber<T>{
  private initValue: T
  constructor(v: T) {
    this.initValue = v
  };
  public add(x: T, y: T) {
    return x + y
  }
}

因为是泛型变量问题:

// test例子
function test<T>(arg: T): T {
  return arg;
}

// 获取参数数组lengt
function test<T>(arg: T): T {
  console.log(arg.length);  // 报错
  return arg;
}

function testArray<T>(arg: T[]): T[] {
  console.log(arg.length);  // 这样可以
  return arg;
}

// 或者

function testArray<T>(arg: Array<T>): Array<T> {
  console.log(arg.length);  // 这样也行
  return arg;
}

​ 使用泛型创建像test这样的泛型函数时,编译器要求你在函数体必须正确的使用这个通用的类型。 换句话说,你必须把这些参数当做是任意或所有类型。

泛型约束

​ 有时候想操作某类型的一组值,并且我们知道这组值具有什么样的属性。就像上例,想访问arg的length属性,但是不能保证每种类型都有length属性。 所以我们最好对T类型做出约束, 约束的话我们就用到了接口。

interface LengthTest {
  length: number;
}

function test<T extends LengthTest>(arg: T): T {
  console.log(arg.length);
  return arg;
}

// 但是 T 被约束为有length属性就不再适合任意类型
test(1) // no

// 需要传进去符合约束的类型
test({ length: 2, value: 1 }) // yes

操作小技巧 Keyof

keyof是索引类型查询操作符f与Object.keys略有相似,只是 keyof 是取 interface 的键,而且 keyof 取到键后会保存为联合类型。

假如我们有这样一个需求:

实现一个getValue函数获取对像的value值?

function getValue(o: object, key: string) {
  return o[key];
}
const obj = { name: '铁蛋儿', age: 18 };
const val = getValue(obj, 'name');

这样写有两点不好:

  1. 无法确定返回值的类型
  2. 无法对key进行约束, 可能拼写错误

使用keyof来增强getValue函数:

// 此时key是name|age
function getValue<T extends Object, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
 
const obj = { name: '张三', age: 18 };
const val = getValue(obj, 'name');

最后来一个官网的高级操作(类型参数实例化):

class Name {
  name: string;
}
class Job {
  job: boolean;

}
class User {
  age: number;
}
class ZhangSan extends User {
  zname: Name;
}
class LiSi extends User {
  ljob: Job;
}

function createInstance<U extends User>(o: new () => U): U {
  return new o();
}

createInstance(ZhangSan).zname.name;
createInstance(LiSi).ljob.job;

欢迎关注B站: 前端铁蛋儿

最后给还不是很熟练的小伙伴留两个问题:

  1. 什么时候用接口?什么时候用泛型?
  2. 将类型作为参数传递, 并实例化有哪些应用场景?

公众号.jpg