TypeScript 进阶:Class 中的动态类型参数

栏目: 编程语言 · 发布时间: 5年前

内容简介:前段时间听说 Vue3.x 要使用 TypeScript 重构了, 本来一直都想研究一下 Vue 的源码, 这次带着夙愿来从头编写一个简单的、现代化的 Vue.搭建好 TypeScript 开发环境后, 开始了一段 TypeScript 与 Vue 源码的探索之旅.我使用 ES6

前段时间听说 Vue3.x 要使用 TypeScript 重构了, 本来一直都想研究一下 Vue 的源码, 这次带着夙愿来从头编写一个简单的、现代化的 Vue.

搭建好 TypeScript 开发环境后, 开始了一段 TypeScript 与 Vue 源码的探索之旅.

我使用 ES6 class 创建了一个 Vue 类, 为了实现数据监听, 我使用了 ES2015 中的 proxy 方法来对数据进行封装, 并且将这个 proxy 返回给类的构造方法, 以便于获取 vm 实例.

代码如下

查看源代码
interface IOptions {
  data: () => Record<string, any>
}

class Vue {
  private $options: IOptions = {
    data: () => ({})
  }

  constructor(options: IOptions) {
    this.$options = options
    const proxy = this.initProxy()
    return proxy
  }

  initProxy() {
    const data = this.$options.data ? this.$options.data() : {}

    return new Proxy(this, {
      set(_, key: string, value) {
        data[key] = value
        return true
      },
      get(_, key: string) {
        return data[key]
      }
    })
  }
}

const vm = new Vue({
  data() {
    return {
      a: 1
    }
  }
})

vm.a = 2
// ^ Property 'a' does not exist on type 'Vue'.

console.log(vm.a) // => 2
//             ^ Property 'a' does not exist on type 'Vue'.
在线版本

如果你查看了在线版本, 可以看到, 我们在使用实例属性 vm.a 时报了一个 TS 错误 Property 'a' does not exist on type 'Vue' , 意思是说 vm 实例上不存在属性 a .

虽然这段代码可以正常执行, 但是静态类型检查没有通过呀.

没关系, 万物皆 Any, 我们这样

const vm: any = new Vue({
  data() {
    return {
      a: 1
    }
  }
})

vm.a = 2

console.log(vm.a)

Everything is any!

看起来还不错, 代码正常运行了, 静态类型检查也通过了. But …

但是我们使用 typescript 的初衷呢? 我们不就是为了类型安全吗, 现在编辑器也不提示 vm 上有哪些变量了, 就算用了 vm.b 也不报错了 [掀桌.jpg]

不行, 我们不能这样.

经过一番思考与探索, 最终我选择求助 overstackflow 上的网友, 在提出我的 问题 后的短短 1 个小时内, 竟然获得了三个回答

其中有个网友提到, typescript 并不能知道 Proxy 上可能出现的属性, 必须在运行时才能知道.

但是我的观点是, 我并没有等到运行时才声明才会出现的属性, 而是在类声明之后的调用中声明属性, 我认为这仍然处于编译时的阶段.

不久之后看到了一个新的回答, 他说道这个问题需要两个步骤,

第一个步骤是正确处理 initProxy 的返回值, 它将包含实例中动态声明的属性.

第二步是让类的返回值中也包含这个动态类型和类本身.

在 Proxy 的初始化方法 initProxy() 中, 我们将 target 绑定到了类的 this 中, 以便与我们使用 vm.a 来访问这个 proxy, 它的返回值就是类本身

在构造函数 constractor 中我们将这个 proxy 返回给类, 试图让类的实例也获得 proxy 中声明的类型, 但是上面我们知道了, 这样行不通,

虽然这个实例确实获得了 proxy 的指向, 但是类型并没有被一并获得.

根据 @Titian Cernicova-Dragomir 的回答 我们需要对这种动态的类型使用泛型变量

首先我们需要正确的获取 initProxy() 的返回值类型

查看源代码
interface IOptions<T> {
  data: () => T
}

class Vue<T = {}> {
  private $options: IOptions<T> = {
    data: () => ({})
  } as IOptions<T>

  constructor(options: IOptions<T>) {
    this.$options = options
    const proxy = this.initProxy()
    return proxy
  }
  public initProxy(): T & Vue<T> {
    const data = this.$options.data ? this.$options.data() : {}

    return new Proxy(this as unknown as T & Vue<T>, {
      set(_, key: string, value) {
        data[key] = value
        return true
      },
      get(_, key: string) {
        return data[key]
      }
    })
  }
}

const vm = new Vue({
  data() {
    return {
      a: 1
    }
  }
})

vm.initProxy().a // ok now
在线版本

我们使用泛型变量 T 来告诉 typescript 我们即将要声明的 data 的类型, 这个类型变量将会用于声明:

  • 接口 IOptionsdata 属性的类型

    interface IOptions<T> {
      data: () => T
    }
    
  • proxy 的返回值类型

    return new Proxy(this as unknown as T & Vue<T>, {/* ... */}) 
    // Proxy 会返回第一个参数的类型
    

IOptions<T> 这个接口在构造函数中进行了声明, 实例化类时就会获得泛型变量 T 的实际类型

proxy 的返回值类型中我们使用到了 TS 中的交叉类型 (Intersection Types), 它会返回这两个类型的所有属性.

@Titian Cernicova-Dragomir 还指出, 即使 TypeScript 允许我们指定 constractor 的返回值, 我们也无力改变这个类的返回值的 类型 , 所以

vm.initProxy().a // It's ok

vm.a // Error: Property 'a' does not exist on type 'Vue'

我们有两个办法来克服这个限制:

1. 不使用构造函数实例化类

我们将构造函数私有化, 并且声明一个静态方法来实例话这个类, 在这个静态方法中返回类的类型和泛型 T

查看源代码
interface IOptions<T> {
  data: () => T
}

class Vue<T = {}> {
  private $options: IOptions<T> = {
    data: () => ({})
  } as IOptions<T>

  private constructor(options: IOptions<T>) {
    this.$options = options
    const proxy = this.initProxy()
    return proxy
  }
  static create<T>(data: IOptions<T>): Vue<T> & T {
    return new Vue<T>(data) as unknown as Vue<T> & T
  }
  initProxy(): T & Vue<T> {
    const data = this.$options.data ? this.$options.data() : {}

    return new Proxy(this as unknown as T & Vue<T>, {
      set(_, key: string, value) {
        data[key] = value
        return true
      },
      get(_, key: string) {
        return data[key]
      }
    })
  }
}

const vm = Vue.create({
  data() {
    return {
      a: 1
    }
  }
})

vm.a = 2
在线版本

这种办法解决了我们的问题, 但是不完美. 我们实例话 Vue 类的时候, 需要这样写

const vm = Vue.create({/* ... */})

这一点都不 OOP ~ [掀桌.jpg]x2

2. 对类使用单独的签名

查看源代码
interface IOptions<T> {
  data: () => T
}

class _Vue<T = {}> {
  private $options: IOptions<T> = {
    data: () => ({})
  } as IOptions<T>

  private constructor(options: IOptions<T>) {
    this.$options = options
    const proxy = this.initProxy()
    return proxy
  }
  initProxy(): Vue<T> {
    const data = this.$options.data ? this.$options.data() : {}

    return new Proxy(this as unknown as Vue<T>, {
      set(_, key: string, value) {
        data[key] = value
        return true
      },
      get(_, key: string) {
        return data[key]
      }
    })
  }
}

type Vue<T> = _Vue<T> & T
const Vue: new <T>(data: IOptions<T>) => Vue<T> = _Vue as any // 这很 any

const vm = new Vue({
  data() {
    return {
      a: 1
    }
  }
})

vm.a = 2
在线版本

完美的解决了我们的问题, 虽然这个办法需要声明一个额外的类型, 但是由于我们使用的是封装的高阶方法, 只要不影响使用就行啦!

再次感谢 @Titian Cernicova-Dragomir !


以上所述就是小编给大家介绍的《TypeScript 进阶:Class 中的动态类型参数》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

图解物联网

图解物联网

[ 日] NTT DATA集团、河村雅人、大塚纮史、小林佑辅、小山武士、宫崎智也、石黑佑树、小岛康平 / 丁 灵 / 人民邮电出版社 / 2017-4 / 59.00元

本书图例丰富,从设备、传感器及传输协议等构成IoT的技术要素讲起,逐步深入讲解如何灵活运用IoT。内容包括用于实现IoT的架构、传感器的种类及能从传感器获取的信息等,并介绍了传感设备原型设计必需的Arduino等平台及这些平台的选择方法,连接传感器的电路,传感器的数据分析,乃至IoT跟智能手机/可穿戴设备的联动等。此外,本书以作者们开发的IoT系统为例,讲述了硬件设置、无线通信及网络安全等运用Io......一起来看看 《图解物联网》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具