Skip to content

Instantly share code, notes, and snippets.

@barhatsor
Created July 4, 2026 17:58
Show Gist options
  • Select an option

  • Save barhatsor/d78f4a34e379d4b7b4153322d9400157 to your computer and use it in GitHub Desktop.

Select an option

Save barhatsor/d78f4a34e379d4b7b4153322d9400157 to your computer and use it in GitHub Desktop.
Strict Custom Element Type
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