From 62a948840fcf4fbbd8257aec542e7af166540bef Mon Sep 17 00:00:00 2001 From: cjc0013 Date: Wed, 27 May 2026 21:49:54 -0400 Subject: [PATCH 1/2] Fix Element.matches strict narrowing --- baselines/dom.generated.d.ts | 11 ++++++++--- baselines/ts5.5/dom.generated.d.ts | 11 ++++++++--- baselines/ts5.6/dom.generated.d.ts | 11 ++++++++--- baselines/ts5.9/dom.generated.d.ts | 11 ++++++++--- src/build/emitter.ts | 15 ++++++++++++++- unittests/files/matches.ts | 19 +++++++++++++++++++ 6 files changed, 65 insertions(+), 13 deletions(-) create mode 100644 unittests/files/matches.ts diff --git a/baselines/dom.generated.d.ts b/baselines/dom.generated.d.ts index a6556cb39..3e805338f 100644 --- a/baselines/dom.generated.d.ts +++ b/baselines/dom.generated.d.ts @@ -14051,9 +14051,12 @@ interface Element extends Node, ARIAMixin, Animatable, ChildNode, NonDocumentTyp * * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/matches) */ - matches(selectors: K): this is HTMLElementTagNameMap[K]; - matches(selectors: K): this is SVGElementTagNameMap[K]; - matches(selectors: K): this is MathMLElementTagNameMap[K]; + matches>(selectors: K): this is Extract; + matches>(selectors: K): this is Extract; + matches>(selectors: K): this is Extract; + matches(selectors: K): boolean; + matches(selectors: K): boolean; + matches(selectors: K): boolean; matches(selectors: string): boolean; /** * The **`releasePointerCapture()`** method of the Element interface releases (stops) pointer capture that was previously set for a specific (PointerEvent) pointer. @@ -43880,6 +43883,8 @@ interface MathMLElementTagNameMap { "semantics": MathMLElement; } +type ElementMatchesMap = { [K in keyof T as T[K] extends U ? U extends T[K] ? never : K : never]: T[K] }; + /** @deprecated Directly use HTMLElementTagNameMap or SVGElementTagNameMap as appropriate, instead. */ type ElementTagNameMap = HTMLElementTagNameMap & Pick>; diff --git a/baselines/ts5.5/dom.generated.d.ts b/baselines/ts5.5/dom.generated.d.ts index e1e2ad25d..a962e6705 100644 --- a/baselines/ts5.5/dom.generated.d.ts +++ b/baselines/ts5.5/dom.generated.d.ts @@ -14038,9 +14038,12 @@ interface Element extends Node, ARIAMixin, Animatable, ChildNode, NonDocumentTyp * * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/matches) */ - matches(selectors: K): this is HTMLElementTagNameMap[K]; - matches(selectors: K): this is SVGElementTagNameMap[K]; - matches(selectors: K): this is MathMLElementTagNameMap[K]; + matches>(selectors: K): this is Extract; + matches>(selectors: K): this is Extract; + matches>(selectors: K): this is Extract; + matches(selectors: K): boolean; + matches(selectors: K): boolean; + matches(selectors: K): boolean; matches(selectors: string): boolean; /** * The **`releasePointerCapture()`** method of the Element interface releases (stops) pointer capture that was previously set for a specific (PointerEvent) pointer. @@ -43854,6 +43857,8 @@ interface MathMLElementTagNameMap { "semantics": MathMLElement; } +type ElementMatchesMap = { [K in keyof T as T[K] extends U ? U extends T[K] ? never : K : never]: T[K] }; + /** @deprecated Directly use HTMLElementTagNameMap or SVGElementTagNameMap as appropriate, instead. */ type ElementTagNameMap = HTMLElementTagNameMap & Pick>; diff --git a/baselines/ts5.6/dom.generated.d.ts b/baselines/ts5.6/dom.generated.d.ts index be2edd668..a7f3ee97d 100644 --- a/baselines/ts5.6/dom.generated.d.ts +++ b/baselines/ts5.6/dom.generated.d.ts @@ -14048,9 +14048,12 @@ interface Element extends Node, ARIAMixin, Animatable, ChildNode, NonDocumentTyp * * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/matches) */ - matches(selectors: K): this is HTMLElementTagNameMap[K]; - matches(selectors: K): this is SVGElementTagNameMap[K]; - matches(selectors: K): this is MathMLElementTagNameMap[K]; + matches>(selectors: K): this is Extract; + matches>(selectors: K): this is Extract; + matches>(selectors: K): this is Extract; + matches(selectors: K): boolean; + matches(selectors: K): boolean; + matches(selectors: K): boolean; matches(selectors: string): boolean; /** * The **`releasePointerCapture()`** method of the Element interface releases (stops) pointer capture that was previously set for a specific (PointerEvent) pointer. @@ -43877,6 +43880,8 @@ interface MathMLElementTagNameMap { "semantics": MathMLElement; } +type ElementMatchesMap = { [K in keyof T as T[K] extends U ? U extends T[K] ? never : K : never]: T[K] }; + /** @deprecated Directly use HTMLElementTagNameMap or SVGElementTagNameMap as appropriate, instead. */ type ElementTagNameMap = HTMLElementTagNameMap & Pick>; diff --git a/baselines/ts5.9/dom.generated.d.ts b/baselines/ts5.9/dom.generated.d.ts index 24a34aa36..7ffeb235f 100644 --- a/baselines/ts5.9/dom.generated.d.ts +++ b/baselines/ts5.9/dom.generated.d.ts @@ -14048,9 +14048,12 @@ interface Element extends Node, ARIAMixin, Animatable, ChildNode, NonDocumentTyp * * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/matches) */ - matches(selectors: K): this is HTMLElementTagNameMap[K]; - matches(selectors: K): this is SVGElementTagNameMap[K]; - matches(selectors: K): this is MathMLElementTagNameMap[K]; + matches>(selectors: K): this is Extract; + matches>(selectors: K): this is Extract; + matches>(selectors: K): this is Extract; + matches(selectors: K): boolean; + matches(selectors: K): boolean; + matches(selectors: K): boolean; matches(selectors: string): boolean; /** * The **`releasePointerCapture()`** method of the Element interface releases (stops) pointer capture that was previously set for a specific (PointerEvent) pointer. @@ -43877,6 +43880,8 @@ interface MathMLElementTagNameMap { "semantics": MathMLElement; } +type ElementMatchesMap = { [K in keyof T as T[K] extends U ? U extends T[K] ? never : K : never]: T[K] }; + /** @deprecated Directly use HTMLElementTagNameMap or SVGElementTagNameMap as appropriate, instead. */ type ElementTagNameMap = HTMLElementTagNameMap & Pick>; diff --git a/src/build/emitter.ts b/src/build/emitter.ts index a05c088b3..af9dd16c0 100644 --- a/src/build/emitter.ts +++ b/src/build/emitter.ts @@ -645,7 +645,12 @@ export function emitWebIdl( const paramName = m.signature[0].param![0].name; for (const mapName of tagNameMapNames) { printer.printLine( - `matches(${paramName}: K): this is ${mapName}[K];`, + `matches>(${paramName}: K): this is Extract<${mapName}[K], this>;`, + ); + } + for (const mapName of tagNameMapNames) { + printer.printLine( + `matches(${paramName}: K): boolean;`, ); } printer.printLine(`matches(${paramName}: string): boolean;`); @@ -715,6 +720,13 @@ export function emitWebIdl( printer.printLine(""); } + function emitElementMatchesMap() { + printer.printLine( + "type ElementMatchesMap = { [K in keyof T as T[K] extends U ? U extends T[K] ? never : K : never]: T[K] };", + ); + printer.printLine(""); + } + /// Emit overloads for the createEvent method function emitCreateEventOverloads(m: Browser.Method) { if (matchParamMethodSignature(m, "createEvent", "Event", "string")) { @@ -1664,6 +1676,7 @@ export function emitWebIdl( "MathMLElementTagNameMap", tagNameToEleName.mathMLResult, ); + emitElementMatchesMap(); emitDeprecatedHTMLOrSVGElementTagNameMap(); emitNamedConstructors(); } diff --git a/unittests/files/matches.ts b/unittests/files/matches.ts new file mode 100644 index 000000000..d9c6346e5 --- /dev/null +++ b/unittests/files/matches.ts @@ -0,0 +1,19 @@ +declare const element: Element; +declare const htmlElement: HTMLElement; +declare const htmlTableCellElement: HTMLTableCellElement; + +if (element.matches("dt")) { + const narrowed: HTMLElement = element; +} + +if (!htmlElement.matches("dt")) { + htmlElement.id; +} + +if (htmlElement.matches("td")) { + const narrowed: HTMLTableCellElement = htmlElement; +} + +if (!htmlTableCellElement.matches("th")) { + htmlTableCellElement.id; +} From f49b6ab03c8bd1cf5ef616ef85fc4c4c3c922de3 Mon Sep 17 00:00:00 2001 From: cjc0013 Date: Tue, 30 Jun 2026 12:55:39 -0400 Subject: [PATCH 2/2] Refine Element.matches narrowing map --- baselines/dom.generated.d.ts | 34 +++++++++++++++--- baselines/ts5.5/dom.generated.d.ts | 34 +++++++++++++++--- baselines/ts5.6/dom.generated.d.ts | 34 +++++++++++++++--- baselines/ts5.9/dom.generated.d.ts | 34 +++++++++++++++--- src/build/emitter.ts | 58 ++++++++++++++++++++++++++++-- unittests/files/matches.ts | 14 ++++++++ 6 files changed, 190 insertions(+), 18 deletions(-) diff --git a/baselines/dom.generated.d.ts b/baselines/dom.generated.d.ts index 3e805338f..f77d88b30 100644 --- a/baselines/dom.generated.d.ts +++ b/baselines/dom.generated.d.ts @@ -14051,9 +14051,9 @@ interface Element extends Node, ARIAMixin, Animatable, ChildNode, NonDocumentTyp * * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/matches) */ - matches>(selectors: K): this is Extract; - matches>(selectors: K): this is Extract; - matches>(selectors: K): this is Extract; + matches>(selectors: K): this is Extract; + matches>(selectors: K): this is Extract; + matches>(selectors: K): this is Extract; matches(selectors: K): boolean; matches(selectors: K): boolean; matches(selectors: K): boolean; @@ -43883,7 +43883,33 @@ interface MathMLElementTagNameMap { "semantics": MathMLElement; } -type ElementMatchesMap = { [K in keyof T as T[K] extends U ? U extends T[K] ? never : K : never]: T[K] }; +type ElementMatchesNarrowingMap = + ElementType extends Element + ? Element extends ElementType + ? ElementMap + : [ElementMatchesBase] extends [never] + ? {} + : ElementMatchesStrictMap> + : {}; + +type ElementMatchesBase = + HTMLElement extends ElementType + ? HTMLElement + : SVGElement extends ElementType + ? SVGElement + : MathMLElement extends ElementType + ? MathMLElement + : never; + +type ElementMatchesStrictMap = { + [Key in keyof ElementMap as ( + ElementMap[Key] extends BaseElement + ? BaseElement extends ElementMap[Key] + ? never + : Key + : never + )]: ElementMap[Key]; +}; /** @deprecated Directly use HTMLElementTagNameMap or SVGElementTagNameMap as appropriate, instead. */ type ElementTagNameMap = HTMLElementTagNameMap & Pick>; diff --git a/baselines/ts5.5/dom.generated.d.ts b/baselines/ts5.5/dom.generated.d.ts index a962e6705..1eaf2218e 100644 --- a/baselines/ts5.5/dom.generated.d.ts +++ b/baselines/ts5.5/dom.generated.d.ts @@ -14038,9 +14038,9 @@ interface Element extends Node, ARIAMixin, Animatable, ChildNode, NonDocumentTyp * * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/matches) */ - matches>(selectors: K): this is Extract; - matches>(selectors: K): this is Extract; - matches>(selectors: K): this is Extract; + matches>(selectors: K): this is Extract; + matches>(selectors: K): this is Extract; + matches>(selectors: K): this is Extract; matches(selectors: K): boolean; matches(selectors: K): boolean; matches(selectors: K): boolean; @@ -43857,7 +43857,33 @@ interface MathMLElementTagNameMap { "semantics": MathMLElement; } -type ElementMatchesMap = { [K in keyof T as T[K] extends U ? U extends T[K] ? never : K : never]: T[K] }; +type ElementMatchesNarrowingMap = + ElementType extends Element + ? Element extends ElementType + ? ElementMap + : [ElementMatchesBase] extends [never] + ? {} + : ElementMatchesStrictMap> + : {}; + +type ElementMatchesBase = + HTMLElement extends ElementType + ? HTMLElement + : SVGElement extends ElementType + ? SVGElement + : MathMLElement extends ElementType + ? MathMLElement + : never; + +type ElementMatchesStrictMap = { + [Key in keyof ElementMap as ( + ElementMap[Key] extends BaseElement + ? BaseElement extends ElementMap[Key] + ? never + : Key + : never + )]: ElementMap[Key]; +}; /** @deprecated Directly use HTMLElementTagNameMap or SVGElementTagNameMap as appropriate, instead. */ type ElementTagNameMap = HTMLElementTagNameMap & Pick>; diff --git a/baselines/ts5.6/dom.generated.d.ts b/baselines/ts5.6/dom.generated.d.ts index a7f3ee97d..ff7f2cd35 100644 --- a/baselines/ts5.6/dom.generated.d.ts +++ b/baselines/ts5.6/dom.generated.d.ts @@ -14048,9 +14048,9 @@ interface Element extends Node, ARIAMixin, Animatable, ChildNode, NonDocumentTyp * * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/matches) */ - matches>(selectors: K): this is Extract; - matches>(selectors: K): this is Extract; - matches>(selectors: K): this is Extract; + matches>(selectors: K): this is Extract; + matches>(selectors: K): this is Extract; + matches>(selectors: K): this is Extract; matches(selectors: K): boolean; matches(selectors: K): boolean; matches(selectors: K): boolean; @@ -43880,7 +43880,33 @@ interface MathMLElementTagNameMap { "semantics": MathMLElement; } -type ElementMatchesMap = { [K in keyof T as T[K] extends U ? U extends T[K] ? never : K : never]: T[K] }; +type ElementMatchesNarrowingMap = + ElementType extends Element + ? Element extends ElementType + ? ElementMap + : [ElementMatchesBase] extends [never] + ? {} + : ElementMatchesStrictMap> + : {}; + +type ElementMatchesBase = + HTMLElement extends ElementType + ? HTMLElement + : SVGElement extends ElementType + ? SVGElement + : MathMLElement extends ElementType + ? MathMLElement + : never; + +type ElementMatchesStrictMap = { + [Key in keyof ElementMap as ( + ElementMap[Key] extends BaseElement + ? BaseElement extends ElementMap[Key] + ? never + : Key + : never + )]: ElementMap[Key]; +}; /** @deprecated Directly use HTMLElementTagNameMap or SVGElementTagNameMap as appropriate, instead. */ type ElementTagNameMap = HTMLElementTagNameMap & Pick>; diff --git a/baselines/ts5.9/dom.generated.d.ts b/baselines/ts5.9/dom.generated.d.ts index 7ffeb235f..222589452 100644 --- a/baselines/ts5.9/dom.generated.d.ts +++ b/baselines/ts5.9/dom.generated.d.ts @@ -14048,9 +14048,9 @@ interface Element extends Node, ARIAMixin, Animatable, ChildNode, NonDocumentTyp * * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/matches) */ - matches>(selectors: K): this is Extract; - matches>(selectors: K): this is Extract; - matches>(selectors: K): this is Extract; + matches>(selectors: K): this is Extract; + matches>(selectors: K): this is Extract; + matches>(selectors: K): this is Extract; matches(selectors: K): boolean; matches(selectors: K): boolean; matches(selectors: K): boolean; @@ -43880,7 +43880,33 @@ interface MathMLElementTagNameMap { "semantics": MathMLElement; } -type ElementMatchesMap = { [K in keyof T as T[K] extends U ? U extends T[K] ? never : K : never]: T[K] }; +type ElementMatchesNarrowingMap = + ElementType extends Element + ? Element extends ElementType + ? ElementMap + : [ElementMatchesBase] extends [never] + ? {} + : ElementMatchesStrictMap> + : {}; + +type ElementMatchesBase = + HTMLElement extends ElementType + ? HTMLElement + : SVGElement extends ElementType + ? SVGElement + : MathMLElement extends ElementType + ? MathMLElement + : never; + +type ElementMatchesStrictMap = { + [Key in keyof ElementMap as ( + ElementMap[Key] extends BaseElement + ? BaseElement extends ElementMap[Key] + ? never + : Key + : never + )]: ElementMap[Key]; +}; /** @deprecated Directly use HTMLElementTagNameMap or SVGElementTagNameMap as appropriate, instead. */ type ElementTagNameMap = HTMLElementTagNameMap & Pick>; diff --git a/src/build/emitter.ts b/src/build/emitter.ts index af9dd16c0..de674eb4d 100644 --- a/src/build/emitter.ts +++ b/src/build/emitter.ts @@ -645,7 +645,7 @@ export function emitWebIdl( const paramName = m.signature[0].param![0].name; for (const mapName of tagNameMapNames) { printer.printLine( - `matches>(${paramName}: K): this is Extract<${mapName}[K], this>;`, + `matches>(${paramName}: K): this is Extract<${mapName}[K], this>;`, ); } for (const mapName of tagNameMapNames) { @@ -722,8 +722,62 @@ export function emitWebIdl( function emitElementMatchesMap() { printer.printLine( - "type ElementMatchesMap = { [K in keyof T as T[K] extends U ? U extends T[K] ? never : K : never]: T[K] };", + "type ElementMatchesNarrowingMap =", ); + printer.increaseIndent(); + printer.printLine("ElementType extends Element"); + printer.increaseIndent(); + printer.printLine("? Element extends ElementType"); + printer.increaseIndent(); + printer.printLine("? ElementMap"); + printer.printLine(": [ElementMatchesBase] extends [never]"); + printer.increaseIndent(); + printer.printLine("? {}"); + printer.printLine( + ": ElementMatchesStrictMap>", + ); + printer.decreaseIndent(); + printer.decreaseIndent(); + printer.printLine(": {};"); + printer.decreaseIndent(); + printer.decreaseIndent(); + printer.printLine(""); + printer.printLine("type ElementMatchesBase ="); + printer.increaseIndent(); + printer.printLine("HTMLElement extends ElementType"); + printer.increaseIndent(); + printer.printLine("? HTMLElement"); + printer.printLine(": SVGElement extends ElementType"); + printer.increaseIndent(); + printer.printLine("? SVGElement"); + printer.printLine(": MathMLElement extends ElementType"); + printer.increaseIndent(); + printer.printLine("? MathMLElement"); + printer.printLine(": never;"); + printer.decreaseIndent(); + printer.decreaseIndent(); + printer.decreaseIndent(); + printer.decreaseIndent(); + printer.printLine(""); + printer.printLine( + "type ElementMatchesStrictMap = {", + ); + printer.increaseIndent(); + printer.printLine("[Key in keyof ElementMap as ("); + printer.increaseIndent(); + printer.printLine("ElementMap[Key] extends BaseElement"); + printer.increaseIndent(); + printer.printLine("? BaseElement extends ElementMap[Key]"); + printer.increaseIndent(); + printer.printLine("? never"); + printer.printLine(": Key"); + printer.decreaseIndent(); + printer.printLine(": never"); + printer.decreaseIndent(); + printer.decreaseIndent(); + printer.printLine(")]: ElementMap[Key];"); + printer.decreaseIndent(); + printer.printLine("};"); printer.printLine(""); } diff --git a/unittests/files/matches.ts b/unittests/files/matches.ts index d9c6346e5..c95e14bc5 100644 --- a/unittests/files/matches.ts +++ b/unittests/files/matches.ts @@ -1,5 +1,6 @@ declare const element: Element; declare const htmlElement: HTMLElement; +declare const htmlDivElement: HTMLDivElement; declare const htmlTableCellElement: HTMLTableCellElement; if (element.matches("dt")) { @@ -14,6 +15,19 @@ if (htmlElement.matches("td")) { const narrowed: HTMLTableCellElement = htmlElement; } +if (htmlElement.matches("div")) { + const narrowed: HTMLDivElement = htmlElement; +} + +if (!htmlDivElement.matches("div")) { + htmlDivElement.id; +} + +if (htmlDivElement.matches("object")) { + // @ts-expect-error HTMLDivElement should not narrow to HTMLObjectElement. + const narrowed: HTMLObjectElement = htmlDivElement; +} + if (!htmlTableCellElement.matches("th")) { htmlTableCellElement.id; }