通过对一个实际问题思考学习TypeScript(小做类型体操)

204 阅读6分钟

本文将会通过一道实际问题带你掌握TS如下知识:枚举、范型、联合类型、implements、keyof、extends等知识

问题:如何将一个枚举转化为一个 elementUI的Select选择器的数据结构

image.png 也就是

//枚举形式
enum fruit {
    apple = '苹果',
    pear = '梨'
}

//转化为

[
    {label:'apple',value:'苹果'},
    {label:'pera',value:'梨'}
]

下面给出anyscirpt的代码 image.png

可以看到,上面这样的没编写任何类型的方法其实是没有代码提示可言的。而我们今天的目的就是将这个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的结果有什么区别,这里不做过多赘述

image.png

我们将通过以下两部分完善函数类型推断并学习ts

一、 实现数组项类型

  1. 抽象出数组项 interface
  2. 实现数组项 implements
  3. 修改enum2FormItems方法

二、 优化数组项类型

  1. 将数组项类型优化为范型范型
  2. 优化范型联合类型、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[] image.png

二、优化数组项类型

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'时,就会报错)

image.png

那么我们通过IArrayValue实现的类ArrayValue就也需要传入一个范型T,毕竟你实现的是一个使用了范型的接口。如果这个类不提供一个范型参数,那么就无法获得value的类型

如下(当我们传入了number类型但是value是'0'时,就会报错)

image.png

同理我们的enum2FormItems方法也应该给一个范型参数,毕竟你需要在使用这个方法的时候才能知道枚举项的类型,那么最后就修改为如下 image.png

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都合法

image.png

这里我再介绍两个概念extends和keyof后,我们就开始小做类型体操(type-challenges)

1.extends(也叫范型约束,也就是当属性过于自由时,可以用来约束属性。比如我们上面的T就是过于自由的表现,你可以传任意类型),直接以上面的IArrayValue举例,我们就可以给这个interface的范型加以约束。

image.png

可以看到由于我们给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]
}

同理我们按照前面的添加范型的方法,依次添加

image.png 最后我们在使用的时候就会得到相对最开始的anyScript丰富多了的代码提示

这样 image.png 这样

image.png 这样

image.png

感觉类型推断其实还是没有足够的好,例如我在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
}