Search code examples
typescriptcomputed-propertiestype-declaration

How to properly declare computed property names in TypeScript classes?


In short

How can I declare in TypeScript: In this class, any property beginning with '$' is a reference to an Element?

Context

I have a Custom Element class method that automatically stores any child element having attribute data-reference in a property named after its value prefixed by '$'. It works perfectly in vanilla JavaScript :

class SomeComponent extends HTMLElement {
  connectedCallback() {
    this.setReferences()
    console.log(this.$myRef) // <div data-reference="myRef"></div>
  }

  setReferences() {
    const referencedElements = this.querySelectorAll('[data-reference]')

    referencedElements.forEach((element) => {
      const referenceName = '$' + element.getAttribute('data-reference')
      this[referenceName] = element
    })
  }
}
customElements.define('some-component', SomeComponent)
<some-component>
  <div data-reference="myRef"></div>
</some-component>

But this won't compile in TypeScript, who doesn't know about $myRef property:

console.log(this.$myRef) // Property '$myRef' does not exist on type 'SomeComponent'
                 ^^^^^^

So far I've come up with 3 workarounds but none of them is fully satisfying.

1. Type assertion

Nope!

console.log((this as any).$myRef) // <div data-reference="myRef"></div>

The point is to provide a quick access to any $ref, so adding as any or <any> (plus parenthesis) is not a viable solution.

2. Object litteral

Probably the cleanest, but not exactly what I want to achieve.

type ElementReferenceMap = { [key: string]: Element }

class SomeComponent extends HTMLElement {
    ref: ElementReferenceMap = {};

    setReferences() {
        const referencedElements = this.querySelectorAll('[data-reference]');

        referencedElements.forEach((element) => {
            const referenceName = element.getAttribute('data-reference');
            this.ref[referenceName] = element as Element;
        })

        console.log(this.ref.myRef); // <div data-reference="myRef"></div>
    }
}

By renaming it to this.$.myRef I get closer, but it's still not equivalent to the vanilla JavaScript version.

3. Index property

Works as expected (like the JavaScript version), but it doesn't feel right, as it allows any property value.

class SomeComponent extends HTMLElement {
    // [x: string]: Element; // conflict with other properties
    [x: string]: any; // ok

    setReferences() {
        const referencedElements = this.querySelectorAll('[data-reference]');

        referencedElements.forEach((element) => {
            const referenceName = '$' + element.getAttribute('data-reference');
            this[referenceName] = element;
        })

        console.log(this.$myRef); // <div data-reference="myRef"></div>
    }
}

I may be missing the obvious solution since I'm quite new to TypeScript. Thanks in advance!


Solution

  • There is no way typescript can generate static checks on your html code, it just can't given html being dynamic (you can dynamically generate DOM with data-reference attributes) 2 or 3 both are viable options. Just think about this: typescript checks types during compile time, before delivery, when HTML can be populated with extra elements later, during runtime.

    If you know your elements beforehand, you could provide static type info for typescript. For example for a (3) option:

    class SomeComponent extends HTMLElement {
        [x: '$myRef' | '$myOtherRef']: HTMLElement; // ok
        ....
    

    Or you could just have a plain property:

    class SomeComponent extends HTMLElement {
        $myProp: HTMLElement
        ....