我试图用打字稿写一个简单的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
仍然不起作用。
为了绕过这个限制,我们有两个选择:
使用私有构造函数和正确类型的静态方法:
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;
对类使用单独的签名
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"])
您可以在此处看到此功能。