如何从Typescript中的构造函数动态声明类的实例属性?

时间:2019-04-24 09:08:23

标签: typescript class

我试图用打字稿写一个简单的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具有正确的类型,以便我可以正确地访问构造函数中声明的属性。

3 个答案:

答案 0 :(得分:2)

问题的第一部分是正确设置initProxy的返回类型。由于您要将data返回的所有属性添加到代理,因此返回类型应包含它们。为此,我们将需要T类的类型参数(Vue)。此类型参数将捕获data返回类型的实际类型。有了这个类型参数,我们可以让打字稿知道initProxy实际上返回T & Vue<T>,即它返回一个既是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;
    

答案 1 :(得分:0)

Typescript不知道代理及其可能接受的属性名称。例如,考虑一个设置器,例如:

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

  data[key] = value;
  return true;
}

Typescript必须运行代码以确定此处哪些属性名称合法。

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

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

答案 2 :(得分:0)

您的Vue类正在返回Proxy对象。

您的代理具有获取和设置功能,这意味着您可以使用索引运算符,即[]

来设置和获取包装的对象(在您的情况下为Record<string, any>)。

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

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

您可以看到此功能here