Created
July 4, 2026 17:58
-
-
Save barhatsor/d78f4a34e379d4b7b4153322d9400157 to your computer and use it in GitHub Desktop.
Strict Custom Element Type
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| type ElementAttribute = string | null | |
| type StrictArrayTuple<Arr extends readonly unknown[], ArrTuple = Arr[number]> = | |
| Arr[number][] extends Arr ? // widened array — `as const` forgotten | |
| { missingAsConstOnArr: never } | |
| : Arr[number] extends ArrTuple ? | |
| // brackets compare the types as single units. unbracketed, a naked (eg. non-indexed-access) | |
| // type param like ArrTuple distributes per union member (TS rules). `never` is | |
| // regarded by TypeScript as an empty union, so with no members to distribute over, | |
| // the conditional always returns `never`, instead of taking this exact-match | |
| // branch when both the arr and tuple are absent (`never` does equal `never`) | |
| [ArrTuple] extends [Arr[number]] ? // exact match | |
| unknown | |
| : Arr[number] extends never ? // ArrTuple used but arr undefined/empty | |
| { triedToUseTupleButMissingArr: never } | |
| : { tupleWiderThanArr: never } // strict superset, e.g. `'a' | 'b' | 'extra'` or `string` | |
| : { tupleNotMatchingArr: never } // not a superset, e.g. too-narrow `'a'` or mismatched `'a' | 'x'` | |
| type CustomElementClass<Attrs extends readonly string[], Name = Attrs[number]> = { | |
| observedAttributes?: Attrs | |
| new (...args: any[]): HTMLElement & { | |
| connectedCallback?(): void | |
| disconnectedCallback?(): void | |
| adoptedCallback?(oldDocument: Document, newDocument: Document): void | |
| connectedMoveCallback?(): void | |
| attributeChangedCallback?: ( | |
| name: Name, | |
| oldValue: ElementAttribute, | |
| newValue: ElementAttribute | |
| ) => void | |
| } | |
| } & StrictArrayTuple<Attrs, Name> | |
| type ObservesNothing = readonly [] | |
| /** https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/define#valid_custom_element_names */ | |
| type CustomElementName = Lowercase<`${string}-${string}`> | |
| function defineCustomElement< | |
| // ObservesNothing - we have to fallback to a literal empty array here so an allowed absent | |
| // `observedAttributes` definition doesn't fallback to its generic `readonly string[]` | |
| // constraint, inadvertently triggering `missingAsConstOnArr` | |
| const Attrs extends readonly string[] = ObservesNothing, | |
| Name = Attrs[number], | |
| >( | |
| name: CustomElementName, | |
| constructor: CustomElementClass<Attrs, Name>, | |
| options?: ElementDefinitionOptions | |
| ) { | |
| customElements.define(name, constructor, options) | |
| } | |
| /* | |
| // Example usage: | |
| class Test extends HTMLElement { | |
| static observedAttributes = ['a', 'b'] as const | |
| attributeChangedCallback( | |
| name: typeof Test['observedAttributes'][number], | |
| oldValue: ElementAttribute, | |
| newValue: ElementAttribute | |
| ) { | |
| } | |
| } | |
| defineCustomElement('test-el', Test) | |
| */ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment