TypeScript之泛型篇
前端的小伙伴每每看到这样的代码是不是想吐,究其原因是还不特别了解泛型,快来跟着铁蛋儿一起学习吧。
学完以后就可以开开心心恰饭了!
首先来看下官网给我们的泛型介绍
软件工程中,我们不仅要创建一致的定义良好的API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。
像C#和Java这样的语言中,可以使用
泛型
来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。
下面是来谈一谈我理解的泛型:
泛型可以理解为宽泛的类型,通常用于类和函数, 完了!!!
泛型像是提取了一类事物的共性特性的抽象, 比如说松树、柳树都是树。
但是树那不一定用泛型表达,树的表达方式在程序里有3种:
- 接口
- 继承
- 泛型
先从大家都了解的继承开始说松树继承于树,松树同时也是木材。如果用继承表达就是上图, 大家思考一个问题? 这样的话如果再出现一个物质的类这么办?
我们是不是还要继承,无论谁继承谁,无疑都增加了程序设计复杂度,也增加了继承关系的维护成本或者说耦合, 所以关系太强并不是特别好。很多时候没有必要非常考虑用关系表达, 这时候就可以用接口了。
接口官网是这样描述的:
接口是对行为的抽象,而具体如何行动需要由类(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 都是要做关注点的分离。
如果仅仅用接口,不用泛型,那么关注点就没有做到完全解耦。
泛型是一种抽象共性(本质)的变成手段,它允许将类型作为其它类型的参数(表现形式),从而分离不同关注点的实现(作用)。
泛型基础:
// 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
}
官网这么说:
类有两部分:静态部分和实例部分。 泛型类指的是实例部分的类型,所以类的静态属性不能使用这个泛型类型。
注意: 不能这么干
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');
这样写有两点不好:
- 无法确定返回值的类型
- 无法对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站: 前端铁蛋儿
最后给还不是很熟练的小伙伴留两个问题:
- 什么时候用接口?什么时候用泛型?
- 将类型作为参数传递, 并实例化有哪些应用场景?