如何在 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具有正确的类型,以便我可以访问构造函数中声明的属性而不会出错。

问题的第一部分是正确获取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;
    

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

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

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

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

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

你的 Vue 类返回了一个 Proxy 对象。

您的代理具有 get 和 set 函数,这意味着您可以使用索引运算符(即 [] 来设置和获取包装的对象(在您的情况下Record<string, any>

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

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

您可以在此处看到此功能。

最新更新