如何让动态生成的 getter 和 setter 为 TypeScript 的编译器所知?
How can I make dynamically generated getter and setters known to TypeScript's compiler?
以下代码在 JavaScript 中没有任何错误:
class Test {
constructor() {
Object.defineProperty(this, "hello", {
get() {return "world!"}
})
}
}
let greet = new Test()
console.log(greet.hello)
但是 TypeScript 抛出这个错误:
Property 'hello' does not exist on type 'Test'.
更新
这与我需要的和我正在尝试的类似。 Playground link。
我想在编译时检查属性,因为它们将来可能会改变。
class ColorPalette {
#colors = [
["foregroundColor", "#cccccc"],
["backgroundColor", "#333333"],
["borderColor", "#aaaaaa"]
];
constructor() {
this.#colors.forEach((e, i) => {
Object.defineProperty(this, this.#colors[i][0], {
enumerable: true,
get() { return this.#colors[i][1]; },
set(hex: string) {
if (/^#[0-9a-f]{6}$/i.test(hex)) {
this.#colors[i][1] = hex;
}
}
})
});
}
toString(): string {
return Object.values(this).map(c => c.toString().toUpperCase()).join(", ");
}
}
let redPalette = new ColorPalette();
// redPalette.foregroundColor = "#ff0000"; <----- error: "Property 'foregroundColor' does not exist on type 'ColorPalette'"
console.log(redPalette.toString());
有两件事阻碍了您的代码按原样工作。
首先是您不能在 TypeScript 代码的 constructor
方法体中隐式声明 class 字段。如果你想让 class 有一个 属性,你需要在构造函数外显式声明这个 属性:
class ColorPalette {
foregroundColor: string; // <-- must be here
backgroundColor: string; // <-- must be here
borderColor: string; // <-- must be here
// ...
microsoft/TypeScript#766 asking for such in-constructor declarations, and an open-but-inactive suggestion at microsoft/TypeScript#12613 上有一个被拒绝的建议要求相同,但在可预见的未来,它不是语言的一部分。
第二个问题是,在 TypeScript 代码中,在构造函数中调用 Object.defineProperty()
并不能使编译器相信所讨论的 属性 是 definitely assigned, so when you use the --strict
compiler option, you'd need something like a definite assignment assertion 以消除警告:
class ColorPalette {
foregroundColor!: string; // declared and asserted
backgroundColor!: string; // ditto
borderColor!: string; // ditto
// ...
microsoft/TypeScript#42919 上有一个建议,让编译器将 Object.defineProperty()
识别为初始化属性,但目前,它还不是语言的一部分。
如果您愿意将每个 属性 的名称和类型写两次,那么您可以让您的代码按照最初的方式工作。如果你不是,那么你需要做点别的事情。
一种可能的前进方式是制作一个 class 工厂函数 ,它根据一些输入生成 class 构造函数。您将一些 属性 描述符(嗯,return 此类描述符的函数)放入函数中,然后出现一个 class 构造函数来设置这些属性。它可能看起来像这样:
function ClassFromDescriptorFactories<T extends object>(descriptors: ThisType<T> &
{ [K in keyof T]: (this: T) => TypedPropertyDescriptor<T[K]> }): new () => T {
return class {
constructor() {
let k: keyof T;
for (k in descriptors) {
Object.defineProperty(this, k, (descriptors as any)[k].call(this))
}
}
} as any;
}
调用签名的意思是:对于任何泛型T
对象类型,可以传入一个descriptors
对象,其键来自T
,其属性为零- arg 为来自 T
的每个 属性 生成 属性 描述符的函数;并且工厂函数的结果有一个 construct signature 生成类型 T
.
的实例
ThisType<T>
不是必需的,但在您根据其他 class 属性定义描述符的情况下有助于推理。更多内容如下:
当调用构造函数时,实现遍历 descriptors
的所有键,并且对于每个这样的键 k
,它在 this
上定义一个 属性密钥 k
,以及调用 descriptors[k]
.
时出现的 属性 描述符
请注意,编译器无法验证实现是否与调用签名匹配,原因与它无法验证您的原始示例相同;我们还没有声明属性,也没有看到 Object.defineProperty()
初始化它们。这就是 returned class
已经 asserted as any
的原因。这会抑制有关 class 实现的任何警告,因此我们必须注意实现和调用签名匹配。
但是无论如何,一旦我们有了ClassFromDescriptorFactories()
,我们就可以多次使用它。
对于您的调色板示例,您可以制作一个通用的 colorDescriptor()
函数,该函数采用 initValue
字符串输入,并生成一个无参数函数,该函数生成一个 属性 具有您想要的验证的描述符:
function colorDescriptor(initValue: string) {
return () => {
let value = initValue;
return {
enumerable: true,
get() { return value },
set(hex: string) {
if (/^#[0-9a-f]{6}$/i.test(hex)) {
value = hex;
}
}
}
}
}
value
变量用于存储颜色的实际字符串值。间接 () => {...}
的全部要点是最终 class 的每个实例都有其自己的 value
变量;否则,你最终会让 value
成为 class 的静态 属性,这是你不想要的。
现在我们可以将它与 ClassFromDescriptorFactories()
一起使用来定义 ColorPalette
:
class ColorPalette extends ClassFromDescriptorFactories({
foregroundColor: colorDescriptor("#cccccc"),
backgroundColor: colorDescriptor("#333333"),
borderColor: colorDescriptor("#aaaaaa"),
}) {
toString(): string {
return Object.values(this).map(c => c.toString().toUpperCase()).join(", ");
}
}
编译没有错误,编译器将 ColorPalette
的实例识别为在键 foregroundColor
、backgroundColor
和 [=50= 处具有 string
值的属性],并且在运行时这些属性具有适当的验证:
let redPalette = new ColorPalette();
redPalette.foregroundColor = "#ff0000";
console.log(redPalette.toString()); // "#FF0000, #333333, #AAAAAA"
redPalette.backgroundColor = "oopsie";
console.log(redPalette.backgroundColor) // still #333333
为了确保每个实例都有自己的属性,让我们创建一个新实例:
let bluePalette = new ColorPalette();
bluePalette.foregroundColor = "#0000ff";
console.log(redPalette.foregroundColor) // #ff0000
console.log(bluePalette.foregroundColor) // #0000ff
是的,bluePalette
和 redPalette
不共享 foregroundColor
属性。看起来不错!
请注意,在这种情况下,ThisType<T>
代码会派上用场,我们添加了一个新的描述符,该描述符引用了 class 的其他属性:
class ColorPalette extends ClassFromDescriptorFactories({
foregroundColor: colorDescriptor("#cccccc"),
backgroundColor: colorDescriptor("#333333"),
borderColor: colorDescriptor("#aaaaaa"),
foregroundColorNumber() {
const that = this;
const descriptor = {
get() {
return Number.parseInt(that.foregroundColor.slice(1), 16);
}
}
return descriptor;
}
}) {
toString(): string {
return Object.values(this).map(c => c.toString().toUpperCase()).join(", ");
}
}
这里编译器理解为foregroundColorNumber()
是在T
上定义了一个number
属性,实现里面的this
对应的是T
,因此调用如下工作没有错误:
console.log("0x" + redPalette.foregroundColorNumber.toString(16)) // 0xff0000
console.log(redPalette.foregroundColor = "#000000")
console.log("0x" + redPalette.foregroundColorNumber.toString(16)) // 0x0
如果删除 ThisType<T>
,您会看到一些错误。
以下代码在 JavaScript 中没有任何错误:
class Test {
constructor() {
Object.defineProperty(this, "hello", {
get() {return "world!"}
})
}
}
let greet = new Test()
console.log(greet.hello)
但是 TypeScript 抛出这个错误:
Property 'hello' does not exist on type 'Test'.
更新
这与我需要的和我正在尝试的类似。 Playground link。 我想在编译时检查属性,因为它们将来可能会改变。
class ColorPalette {
#colors = [
["foregroundColor", "#cccccc"],
["backgroundColor", "#333333"],
["borderColor", "#aaaaaa"]
];
constructor() {
this.#colors.forEach((e, i) => {
Object.defineProperty(this, this.#colors[i][0], {
enumerable: true,
get() { return this.#colors[i][1]; },
set(hex: string) {
if (/^#[0-9a-f]{6}$/i.test(hex)) {
this.#colors[i][1] = hex;
}
}
})
});
}
toString(): string {
return Object.values(this).map(c => c.toString().toUpperCase()).join(", ");
}
}
let redPalette = new ColorPalette();
// redPalette.foregroundColor = "#ff0000"; <----- error: "Property 'foregroundColor' does not exist on type 'ColorPalette'"
console.log(redPalette.toString());
有两件事阻碍了您的代码按原样工作。
首先是您不能在 TypeScript 代码的 constructor
方法体中隐式声明 class 字段。如果你想让 class 有一个 属性,你需要在构造函数外显式声明这个 属性:
class ColorPalette {
foregroundColor: string; // <-- must be here
backgroundColor: string; // <-- must be here
borderColor: string; // <-- must be here
// ...
microsoft/TypeScript#766 asking for such in-constructor declarations, and an open-but-inactive suggestion at microsoft/TypeScript#12613 上有一个被拒绝的建议要求相同,但在可预见的未来,它不是语言的一部分。
第二个问题是,在 TypeScript 代码中,在构造函数中调用 Object.defineProperty()
并不能使编译器相信所讨论的 属性 是 definitely assigned, so when you use the --strict
compiler option, you'd need something like a definite assignment assertion 以消除警告:
class ColorPalette {
foregroundColor!: string; // declared and asserted
backgroundColor!: string; // ditto
borderColor!: string; // ditto
// ...
microsoft/TypeScript#42919 上有一个建议,让编译器将 Object.defineProperty()
识别为初始化属性,但目前,它还不是语言的一部分。
如果您愿意将每个 属性 的名称和类型写两次,那么您可以让您的代码按照最初的方式工作。如果你不是,那么你需要做点别的事情。
一种可能的前进方式是制作一个 class 工厂函数 ,它根据一些输入生成 class 构造函数。您将一些 属性 描述符(嗯,return 此类描述符的函数)放入函数中,然后出现一个 class 构造函数来设置这些属性。它可能看起来像这样:
function ClassFromDescriptorFactories<T extends object>(descriptors: ThisType<T> &
{ [K in keyof T]: (this: T) => TypedPropertyDescriptor<T[K]> }): new () => T {
return class {
constructor() {
let k: keyof T;
for (k in descriptors) {
Object.defineProperty(this, k, (descriptors as any)[k].call(this))
}
}
} as any;
}
调用签名的意思是:对于任何泛型T
对象类型,可以传入一个descriptors
对象,其键来自T
,其属性为零- arg 为来自 T
的每个 属性 生成 属性 描述符的函数;并且工厂函数的结果有一个 construct signature 生成类型 T
.
ThisType<T>
不是必需的,但在您根据其他 class 属性定义描述符的情况下有助于推理。更多内容如下:
当调用构造函数时,实现遍历 descriptors
的所有键,并且对于每个这样的键 k
,它在 this
上定义一个 属性密钥 k
,以及调用 descriptors[k]
.
请注意,编译器无法验证实现是否与调用签名匹配,原因与它无法验证您的原始示例相同;我们还没有声明属性,也没有看到 Object.defineProperty()
初始化它们。这就是 returned class
已经 asserted as any
的原因。这会抑制有关 class 实现的任何警告,因此我们必须注意实现和调用签名匹配。
但是无论如何,一旦我们有了ClassFromDescriptorFactories()
,我们就可以多次使用它。
对于您的调色板示例,您可以制作一个通用的 colorDescriptor()
函数,该函数采用 initValue
字符串输入,并生成一个无参数函数,该函数生成一个 属性 具有您想要的验证的描述符:
function colorDescriptor(initValue: string) {
return () => {
let value = initValue;
return {
enumerable: true,
get() { return value },
set(hex: string) {
if (/^#[0-9a-f]{6}$/i.test(hex)) {
value = hex;
}
}
}
}
}
value
变量用于存储颜色的实际字符串值。间接 () => {...}
的全部要点是最终 class 的每个实例都有其自己的 value
变量;否则,你最终会让 value
成为 class 的静态 属性,这是你不想要的。
现在我们可以将它与 ClassFromDescriptorFactories()
一起使用来定义 ColorPalette
:
class ColorPalette extends ClassFromDescriptorFactories({
foregroundColor: colorDescriptor("#cccccc"),
backgroundColor: colorDescriptor("#333333"),
borderColor: colorDescriptor("#aaaaaa"),
}) {
toString(): string {
return Object.values(this).map(c => c.toString().toUpperCase()).join(", ");
}
}
编译没有错误,编译器将 ColorPalette
的实例识别为在键 foregroundColor
、backgroundColor
和 [=50= 处具有 string
值的属性],并且在运行时这些属性具有适当的验证:
let redPalette = new ColorPalette();
redPalette.foregroundColor = "#ff0000";
console.log(redPalette.toString()); // "#FF0000, #333333, #AAAAAA"
redPalette.backgroundColor = "oopsie";
console.log(redPalette.backgroundColor) // still #333333
为了确保每个实例都有自己的属性,让我们创建一个新实例:
let bluePalette = new ColorPalette();
bluePalette.foregroundColor = "#0000ff";
console.log(redPalette.foregroundColor) // #ff0000
console.log(bluePalette.foregroundColor) // #0000ff
是的,bluePalette
和 redPalette
不共享 foregroundColor
属性。看起来不错!
请注意,在这种情况下,ThisType<T>
代码会派上用场,我们添加了一个新的描述符,该描述符引用了 class 的其他属性:
class ColorPalette extends ClassFromDescriptorFactories({
foregroundColor: colorDescriptor("#cccccc"),
backgroundColor: colorDescriptor("#333333"),
borderColor: colorDescriptor("#aaaaaa"),
foregroundColorNumber() {
const that = this;
const descriptor = {
get() {
return Number.parseInt(that.foregroundColor.slice(1), 16);
}
}
return descriptor;
}
}) {
toString(): string {
return Object.values(this).map(c => c.toString().toUpperCase()).join(", ");
}
}
这里编译器理解为foregroundColorNumber()
是在T
上定义了一个number
属性,实现里面的this
对应的是T
,因此调用如下工作没有错误:
console.log("0x" + redPalette.foregroundColorNumber.toString(16)) // 0xff0000
console.log(redPalette.foregroundColor = "#000000")
console.log("0x" + redPalette.foregroundColorNumber.toString(16)) // 0x0
如果删除 ThisType<T>
,您会看到一些错误。