From 033346252d1630f1a5b6935fb76e58b69a73fdf2 Mon Sep 17 00:00:00 2001 From: Pavel Grinchenko Date: Thu, 9 Apr 2026 23:16:57 +0100 Subject: [PATCH] refactor: align getElement/getElements generics with one/many pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switch from `E extends keyof HTMLElementTagNameMap` to separate overloads: tag name for auto-inference, `E extends Element` for explicit typing, and a plain fallback — matching the ref builder API. --- packages/nanotags/src/setup-context.test.ts | 23 +++++++++- packages/nanotags/src/setup-context.ts | 47 ++++++++++++--------- 2 files changed, 48 insertions(+), 22 deletions(-) diff --git a/packages/nanotags/src/setup-context.test.ts b/packages/nanotags/src/setup-context.test.ts index f42ea69..b25c1aa 100644 --- a/packages/nanotags/src/setup-context.test.ts +++ b/packages/nanotags/src/setup-context.test.ts @@ -286,6 +286,25 @@ describe("getElement / getElements", () => { ); expect(found).toHaveLength(2); }); + + it("accepts custom element type via generic", () => { + type CustomEl = HTMLElement & { custom: true }; + const tag = uniqueTag("ge"); + define(tag, (ctx) => { + const single = ctx.getElement(".item"); + expectTypeOf(single).toEqualTypeOf(); + + const list = ctx.getElements(".item"); + expectTypeOf(list).toEqualTypeOf(); + + const container = ctx.getElement(".container"); + const scoped = ctx.getElement(container, ".item"); + expectTypeOf(scoped).toEqualTypeOf(); + + const scopedList = ctx.getElements(container, ".item"); + expectTypeOf(scopedList).toEqualTypeOf(); + }); + }); }); describe("effect", () => { @@ -552,7 +571,7 @@ describe("bind", () => { const parentTag = uniqueTag("bind-parent"); const $val = atom("from-parent"); define(parentTag, (ctx) => { - const child = ctx.getElement(childTag); + const child = ctx.getElement(childTag); ctx.bind($val, child, { prop: "value", event: "change" }); }); const el = mount(`<${parentTag}><${childTag}>`); @@ -595,7 +614,7 @@ describe("bind", () => { const tag = uniqueTag("bind"); const $theme = atom("dark"); define(tag, (ctx) => { - const ctrl = ctx.getElement(controlTag); + const ctrl = ctx.getElement(controlTag); ctx.bind($theme, ctrl, { prop: "theme", event: "change" }); }); const el = mount(`<${tag}><${controlTag}>`); diff --git a/packages/nanotags/src/setup-context.ts b/packages/nanotags/src/setup-context.ts index 5db394c..749d3bf 100644 --- a/packages/nanotags/src/setup-context.ts +++ b/packages/nanotags/src/setup-context.ts @@ -147,34 +147,41 @@ export class Context< } /** Queries a single required element by CSS selector. Throws if not found. */ - getElement(selector: E | string): HTMLElementTagNameMap[E]; - getElement( + getElement( + selector: Tag, + ): HTMLElementTagNameMap[Tag]; + getElement( root: DocumentFragment | Element, - selector: E | string, - ): HTMLElementTagNameMap[E]; - getElement( - selectorOrRoot: E | string | DocumentFragment | Element, - maybeSelector?: E | string, - ): HTMLElementTagNameMap[E] { - return this.getElements(selectorOrRoot as any, maybeSelector as any)[0]!; + selector: Tag, + ): HTMLElementTagNameMap[Tag]; + getElement(selector: string): E; + getElement(root: DocumentFragment | Element, selector: string): E; + getElement(selector: string): Element; + getElement(root: DocumentFragment | Element, selector: string): Element; + getElement(selectorOrRoot: string | DocumentFragment | Element, maybeSelector?: string): Element { + return this.getElements(selectorOrRoot as any, maybeSelector as any)[0]!; } /** Queries all matching elements by CSS selector. Throws if none found. */ - getElements( - selector: E | string, - ): HTMLElementTagNameMap[E][]; - getElements( + getElements( + selector: Tag, + ): HTMLElementTagNameMap[Tag][]; + getElements( root: DocumentFragment | Element, - selector: E | string, - ): HTMLElementTagNameMap[E][]; - getElements( - selectorOrRoot: E | string | DocumentFragment | Element, - maybeSelector?: E | string, - ): HTMLElementTagNameMap[E][] { + selector: Tag, + ): HTMLElementTagNameMap[Tag][]; + getElements(selector: string): E[]; + getElements(root: DocumentFragment | Element, selector: string): E[]; + getElements(selector: string): Element[]; + getElements(root: DocumentFragment | Element, selector: string): Element[]; + getElements( + selectorOrRoot: string | DocumentFragment | Element, + maybeSelector?: string, + ): Element[] { const hasRoot = maybeSelector !== undefined; const root = hasRoot ? (selectorOrRoot as DocumentFragment | Element) : this.host; const selector = (hasRoot ? maybeSelector : selectorOrRoot) as string; - const elements = Array.from(root.querySelectorAll(selector)); + const elements = Array.from(root.querySelectorAll(selector)); invariant(elements.length > 0, `${this.host.localName}: missing ${selector}`); return elements; }