TypeScript:基于静态字符串数组动态声明属性



我正在用TypeScript写一些前端代码,手工创建一些自定义元素。

自定义元素让我定义"观察属性"作为字符串数组。我想让所有这些观察到的属性也成为类的属性,这样我就可以用myElement.attr1 = 'value'代替myElement.setAttribute('attr1', 'value')。现在,我用这个元程序代码来实现它:

class MyElement extends HTMLElement {
static get observedAttributes() {
return ['attr1', 'attr2', 'attr3'];
}
}
for (const attr of MyElement.observedAttributes) {
Object.defineProperty(MyElement.prototype, attr, {
get: function() { return this.getAttribute(attr) },
set: function(value) {
if (value === null) this.removeAttribute(attr);
else this.setAttribute(attr, value);
}
});
}

这个工作得很棒,但是TypeScript并不知道我添加的属性,即使它们都是完全静态的。是否有一些技巧,我可以用它来告诉typescript,我添加属性的每个字符串在observedAttributes?

我试过这样做:

const myObservedAttributes = {
'attr1': true,
'attr2': true,
'attr3': true,
};
type DynamicAttrs = {
[Key in keyof typeof myObservedAttributes]: string | null;
}
class MyElement extends HTMLElement implements DynamicAttrs {
static get observedAttributes() {
return Object.keys(myObservedAttributes);
}
}
// ... Object.defineProperty loop from above

但是当然TypeScript抱怨我没有实现DynamicAttrs中的任何属性,因为它不理解Object.defineProperty

首先,让我们修改observedAttributes()getter,使返回类型成为字符串文字类型的强类型数组,而不仅仅是string[]:

class MyElement extends HTMLElement {
static get observedAttributes() {
return ['attr1', 'attr2', 'attr3'] as const;
}
}

const断言告诉编译器为该值推断一个更具体的类型。现在,如果你用智能感知检查observedAttributes,你会看到类型是

// (getter) MyElement.observedAttributes: readonly ["attr1", "attr2", "attr3"]

现在,如果你想告诉TypeScript你已经增加了一个类实例类型,你可以使用声明合并来"重新打开"。对应于类实例的interface。对于您的示例,它可能看起来像这样:

interface MyElement extends Record<
typeof MyElement.observedAttributes[number], string | null
> { }

如前所述,typeof MyElement.observedAttributesreadonly ["attr1", "attr2", "attr3"]。当使用number(typeof MyElement.observedAttributes[number])索引到该类型时,得到元素类型的并集:"attr1" | "attr2" | "attr3"。通过将Record<typeof MyElement.observedAttributes[number], string | null,使用Record<K, V>实用程序类型合并到MyElement接口,我们说MyElement实例应该具有string | null类型的attr1,attr2attr3属性。


让我们测试一下,使用自定义元素并将元素类型合并到相关的TypeScript接口中,以便编译器知道它:

if (!customElements.get("my-element"))
customElements.define("my-element", MyElement);
interface HTMLElementTagNameMap {
"my-element": MyElement;
}
const myEl = document.createElement("my-element");
// const myEl: MyElement
myEl.attr1 = "abc";
console.log(myEl.attr1.toUpperCase()) // "ABC"
myEl.attr2 = "def";
console.log(myEl.attr2.toUpperCase()) // "DEF"
myEl.attr2 = null;
console.log(myEl.attr2) // null
myEl.attr3 = "ghi";
console.log(myEl.attr3.toUpperCase()) // "GHI"

看起来不错。编译器理解myElMyElement,因此它具有attr1,attr2attr3属性,如所需。

Playground链接到代码

对于这种情况,我将使用工厂:

function getObservedElementFactory<ObservedProps extends string> (...observed: ObservedProps[]) {
type IMyElement = {
[P in ObservedProps] : ReturnType<HTMLElement["getAttribute"]>;
};

class MyElement extends HTMLElement { }

for (const attr of observed) {
Object.defineProperty(MyElement.prototype, attr, {
get: function () { return this.getAttribute(attr); },
set: function (value) {
if (value === null) this.removeAttribute(attr);
else this.setAttribute(attr, value);
},
});
}

return () => new MyElement() as MyElement & IMyElement;
}

export const createObservedOne = getObservedElementFactory("attr1", "attr2", "attr3");

const a = createObservedOne();
a.attr1 = "toto"; // ok

export const createObservedSecond = getObservedElementFactory("attr4", "attr5", "attr6");

const b = createObservedSecond();
b.attr4 = "toto"; // ok

有一些缺点,每个生成的工厂有一个不同的子类HTMLElement和其他东西。但这可能会根据您的需要有所帮助。

最新更新