如何让 TypeScript 的编译器知道动态生成的 getter 和 setter?



下面的代码在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'.

这是类似于我需要的,我正在尝试。操场上的链接。我想在编译时检查属性,因为它们将来可能会改变。

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 ColorPalette {
foregroundColor: string; // <-- must be here
backgroundColor: string; // <-- must be here
borderColor: string; // <-- must be here
// ...

在microsoft/TypeScript#766中有一个拒绝的建议,要求这样的构造函数内声明,在microsoft/TypeScript#12613中有一个开放但不活动的建议,要求同样的,但在可预见的将来,它不是语言的一部分。

第二个问题是,在TypeScript代码中,在构造函数中调用Object.defineProperty()并不能使编译器相信所讨论的属性是明确赋值的,所以当你使用--strict编译器选项时,你需要像明确赋值断言这样的东西来平息警告:
class ColorPalette {
foregroundColor!: string; // declared and asserted
backgroundColor!: string; // ditto
borderColor!: string; // ditto
// ...

在microsoft/TypeScript#42919中有一个建议,建议编译器将Object.defineProperty()识别为初始化属性,但现在,它也不是语言的一部分。

如果你愿意把每个属性的名称和类型写两次,那么你可以让你的代码按照你原来的方式工作。如果你没有,那么你需要做一些其他的事情。


一种可能的方法是创建一个类工厂函数,从一些输入生成类构造函数。将一些属性描述符(也就是返回这些描述符的函数)放入函数中,然后生成一个设置这些属性的类构造函数。它可以像这样:

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,它的属性是0 -arg函数,它为T的每个属性生成属性描述符;从工厂函数出来的东西有一个构造签名,它产生T类型的实例。

ThisType<T>不是必需的,但是在您根据其他类属性定义描述符的情况下有助于进行推断。更低:

该实现在调用构造函数时遍历descriptors的所有键,并且对于k的每个键,它在this上定义一个带有键k的属性,以及在调用descriptors[k]时出现的属性描述符。

请注意,编译器无法验证实现是否匹配调用签名,原因与无法验证原始示例的原因相同;我们没有声明属性,也没有看到Object.defineProperty()初始化它们。这就是返回的class被声明为as any的原因。这抑制了关于类实现的任何警告,因此我们必须小心实现和调用签名匹配。

但是无论如何,一旦我们有了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变量用于存储颜色的实际字符串值。间接() => {...}的全部意义在于,最终类的每个实例都有自己的value变量;否则,您最终会让value成为类的静态属性,这是您不想要的。

现在我们可以用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,backgroundColorborderColor键处具有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

是的,bluePaletteredPalette没有共同的foregroundColor属性。看起来不错!


请注意,ThisType<T>代码在这种情况下很有用,在这种情况下,我们添加一个新的描述符来引用类的其他属性:

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>,你会看到一些错误显示。

Playground链接到代码

最新更新