【问题标题】:How to dynamically declare an instance property of a class from a constructor in Typescript?如何从 Typescript 中的构造函数动态声明类的实例属性?
【发布时间】:2019-04-24 09:08:22
【问题描述】:

我尝试使用 typescript 编写一个简单的 VUE,但第一步失败了。我找到了很多答案,但没有找到可以解决我的问题的解决方案。

我想动态声明类的一些属性,它们是通过构造函数得到的,但我不知道如何编写这样的声明。

环境

打字稿 3.4.5

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'.

这是地址https://stackblitz.com/edit/typescript-kh4zmn的在线预览

打开它,你可以看到控制台输出了预期的输出,但是编辑器给出了一个打字稿错误Property 'a' does not exist on type 'Vue'.。

我希望vm 具有正确的类型,以便我可以访问构造函数中声明的属性而不会出错。

【问题讨论】:

    标签: typescript class constructor


    【解决方案1】:

    问题的第一部分是正确获取initProxy 的返回类型。由于您要将data 返回的所有属性添加到代理,因此返回类型应该包含它们。为了实现这一点,我们需要Vue 类的类型参数(T)。此类型参数将捕获data 返回类型的实际类型。有了这个类型参数,我们可以让 typescript 知道initProxy 实际上返回了T &amp; Vue&lt;T&gt;,也就是说它返回了一个既是T 又是原始类的对象

    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
        }
        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
    

    问题的第二部分是,虽然 typescript 会让你从构造函数返回一个对象,但这不会以任何方式改变构造函数调用的返回类型(你也不能注释构造函数的返回类型)。这就是为什么虽然vm.initProxy().a 有效,但vm.a 仍然无效。

    为了解决这个限制,我们有两种选择:

    1. 使用私有构造函数和正确类型的静态方法:

      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;
      
    2. 为类使用单独的签名

      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
      
      const vm = new Vue({
          data() {
              return {
                  a: 1
              }
          }
      })
      vm.a = 2;
      

    【讨论】:

    • 太棒了! !急需这样一个答案,对我学习打字稿很有帮助,非常感谢你的详细解答。
    【解决方案2】:

    Typescript 不知道代理和它可能接受的属性名称。例如,考虑一个 setter,例如:

    set(_, key: string, value: any) {
      if (!key.startsWith('foo')) {
        return false;
      }
    
      data[key] = value;
      return true;
    }
    

    Typescript 必须运行代码来确定哪些属性名称在这里是合法的。

    快速解决您的问题的方法是在 Vue 类中添加一个类似 [key: string]: unknown; 的属性,它会告诉 typescript 接受任何内容,只要键是 string,无论其类型如何。这将使您的示例编译。

    您可能应该考虑正确声明 Vue 类将使用的属性,但如果可能的话,以利用 Typescript 的静态类型检查。

    【讨论】:

    • [key: string]: unknown; 会起作用,但基本上会使整个类非常不安全(即vm.blaBla 会很好)
    • 没错,但这正是问题所要求的:在调用构造函数之前,类属性是完全未知的。如果它们不是,那么它们可能应该被声明为如上所述的类型安全。无论您是通过 Titian 的回答中的 mixin、通过从基 Vue 类继承还是直接在该类上执行此操作,都取决于上下文。
    【解决方案3】:

    您的 Vue 类正在返回一个代理对象。

    您的代理有一个 get 和 set 函数,这意味着您可以使用索引运算符,即 []

    因此,要正确使用您的 Vue 对象添加属性“a”,然后检索您将使用的值:

    vm["a"] = 2
    console.log(vm["a"])
    

    你可以看到这个工作here。

    【讨论】:

    • 感谢您的及时回复。这似乎解决了我的问题,但它让我失去了静态类型检查,这样我在使用不存在的变量时就不会报错了。
    • @TitianCernicova-Dragomir 啊,是的。你给出的答案很好。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-04-23
    • 2020-05-01
    • 2021-02-26
    • 2016-04-25
    • 1970-01-01
    • 2020-09-13
    相关资源
    最近更新 更多