本文将会通过一道实际问题带你掌握TS如下知识:枚举、范型、联合类型、implements、keyof、extends等知识
问题:如何将一个枚举转化为一个 elementUI的Select选择器的数据结构
也就是
//枚举形式
enum fruit {
apple = '苹果',
pear = '梨'
}
//转化为
[
{label:'apple',value:'苹果'},
{label:'pera',value:'梨'}
]
下面给出anyscirpt的代码
可以看到,上面这样的没编写任何类型的方法其实是没有代码提示可言的。而我们今天的目的就是将这个enum2Form方法的类型慢慢补上
enum Fruit {
apple = '苹果',
pear = '梨'
}
function enum2FormItems(enumType) {
const result = []
//遍历枚举
for (const i in enumType) {
if (isNaN(+i)) {
result.push({ label: i, value: enumType[i] })
}
}
return result
}
enum2FormItems(Fruit);//[ { label: 'apple', value: '苹果' }, { label: 'pear', value: '梨' } ]
做isNaN判断的原因是因为当枚举值为数字的时候会把枚举值也给遍历出来,如下。感兴趣可以看一下枚举的值为number和string类型的时候,ts转js的结果有什么区别,这里不做过多赘述
我们将通过以下两部分完善函数类型推断并学习ts
一、 实现数组项类型
- 抽象出数组项
interface - 实现数组项
implements - 修改enum2FormItems方法
二、 优化数组项类型
- 将数组项类型优化为范型
范型 - 优化范型
联合类型、extends、keyof
一、实现数组项类型
1.抽象出数组项 interface
由于我们上面的枚举项和枚举值的类型均为string,所以我们可以给我们上面的结果数组项创建一个interface如下
interface IArrayValue{
label: string,
value: string
}
2.实现数组项 implements
基于上面的interface,我们可以对这个interface做实现类(implements),在我们使用到这个数组项的时候创建出来
class ArrayValue implements IArrayValue{
label: string;
value: string
constructor(label: string, value: string) {
this.label = label;
this.value = value
}
}
3. 修改enum2FormItems方法
在我们实现了抽象类型IArrayValue后
1.我们用这个interface为方法的结果定义了类型
2.以此抽象实现类implements并用在类数组项的创建
3.我们的enum2FormItems的类型提示有了初步的丰富,起码能通过类型提示到找接口,知道这个函数的返回值的类型是IArrayValue[]
二、优化数组项类型
1. 将数组项类型优化为范型范型
考虑到我们到枚举值可能不光为 stirng类型,可能是什么别的类型。所以我们可以将类型的选择权交给使用者。由使用者在调用该方法的时候,手动告诉我们我们的枚举值是什么类型。
如何让使用者在调用的时候告诉我们类型呢?
这里就要引入泛型的概念 按照我个人的理解,这个范型其实就是类型的变量
// 我们可以在调用的时候传入任意类型赋值给这个类型变量T(范型)
function fun<T>(arg: T): T { return arg; }
fun<boolean>(arg)//arg为boolean类型
fun<number>(arg)//arg为number类型
fun<string>(arg)//arg为string类型
那么对于我们当前数组项的interface如何利用范型达到我们的目的呢? 我们可以给IArrayValue一个范型T,让用户使用的时候自己传入类型.当枚举项为number时,就给范型传number。如果枚举项为string,就给范型传string
如下(当我们传入了number类型但是value是'0'时,就会报错)
那么我们通过IArrayValue实现的类ArrayValue就也需要传入一个范型T,毕竟你实现的是一个使用了范型的接口。如果这个类不提供一个范型参数,那么就无法获得value的类型
如下(当我们传入了number类型但是value是'0'时,就会报错)
同理我们的enum2FormItems方法也应该给一个范型参数,毕竟你需要在使用这个方法的时候才能知道枚举项的类型,那么最后就修改为如下
2. 优化范型联合类型、extends、keyof
我们终于给范型给安进去了,但是我们枚举值又是可能不光仅仅只有string类型或者仅仅只有number类型。可能会有如下情况
enum Fruit {
apple = '苹果',//string
pear = 1 //number
}
这个时候我们就可以使用联合类型
联合类型(该类型的取值可以为多种类型中的一种,可以通过 | 去定义)
let data: string | number;
data = '';//OK
or
data = 1 ;//OK
而在我们的例子中我们可以给范型T传入我们的联合类型,让我们的value是number或者string都合法
这里我再介绍两个概念extends和keyof后,我们就开始小做类型体操(type-challenges)
1.extends(也叫范型约束,也就是当属性过于自由时,可以用来约束属性。比如我们上面的T就是过于自由的表现,你可以传任意类型),直接以上面的IArrayValue举例,我们就可以给这个interface的范型加以约束。
可以看到由于我们给T extends number | string 所以我们给范型传入Object类型的时候就会报错。
2.keyof(可以理解为对类型的遍历,取出类型的所有键,返回联合类型)
interface Fruit {
apple: string;
pear: string;
}
or
enum Fruit {
apple = 'a',
pear = '1'
}
type fruit = keyof Fruit // apple | pear
至此我们需要思考一个问题,我们虽然使用了范型。但是我们使用的时候是去帮ts做了一层理解的。比如说,我们的枚举已经写好了,那么类型不论是number,string还是number | string都是确定的了。我们是否可以通过定义范型为我们传入的枚举类型,再通过其他方式给我们的label和value类型呢?
interface IArrayValue<T> { //这个T为枚举
label: any,//如何通过T去定义
value: any//如何通过T去定义
}
当范型T为枚举时候,我们可以通过定义第二个范型K,通过keyof T得到枚举的key的联合类型。再将该第二个范型K通过extends约束在得到的联合类型中。(有点绕,看代码)
enum Fruit {
apple = 'a',
pear = '1'
}
//如果将枚举Fruit传入则 T:Fruit K:apple | pear
interface IArrayValue<T, K extends keyof T> {
label: K,
value: T[K]
}
同理我们按照前面的添加范型的方法,依次添加
最后我们在使用的时候就会得到相对最开始的anyScript丰富多了的代码提示
这样
这样
这样
感觉类型推断其实还是没有足够的好,例如我在arr[0].label的时候他应该要能够推断出是apple。
ps:最终代码
interface IArrayValue<T, K extends keyof T> {
label: K,
value: T[K]
}
class ArrayValue<T, K extends keyof T> implements IArrayValue<T, keyof T> {
label: K;
value: T[K]
constructor(label: K, value: T[K]) {
this.label = label;
this.value = value
}
}
function enum2FormItems<T>(enumType: T): IArrayValue<T, keyof T>[] {
const result: IArrayValue<T, keyof T>[] = []
for (const i in enumType) {
if (isNaN(+i)) {
result.push(new ArrayValue<T, keyof T>(i, enumType[i]))
}
}
return result
}