我正在用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.observedAttributes
为readonly ["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
,attr2
和attr3
属性。
让我们测试一下,使用自定义元素并将元素类型合并到相关的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"
看起来不错。编译器理解myEl
是MyElement
,因此它具有attr1
,attr2
和attr3
属性,如所需。
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和其他东西。但这可能会根据您的需要有所帮助。