From de22dfcb3d0e0d5e934e20cb05323dfefae28cd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Tue, 1 Sep 2026 18:05:59 +0200 Subject: [PATCH] Keep instantiation expression symbols distinct and stable --- tsc/internal/checker/checker.go | 5 +- tsc/internal/checker/utilities.go | 21 ++++- ...endsInstantiationExpressionType.errors.txt | 31 +++++++ ...ExtendsInstantiationExpressionType.symbols | 79 ++++++++++++++++ ...ssExtendsInstantiationExpressionType.types | 89 +++++++++++++++++++ ...classExtendsInstantiationExpressionType.ts | 23 +++++ 6 files changed, 245 insertions(+), 3 deletions(-) create mode 100644 tsc/testdata/baselines/reference/compiler/classExtendsInstantiationExpressionType.errors.txt create mode 100644 tsc/testdata/baselines/reference/compiler/classExtendsInstantiationExpressionType.symbols create mode 100644 tsc/testdata/baselines/reference/compiler/classExtendsInstantiationExpressionType.types create mode 100644 tsc/testdata/tests/cases/compiler/classExtendsInstantiationExpressionType.ts diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index fb3c33c01b814..8d49c7810be38 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -10785,7 +10785,10 @@ func (c *Checker) getInstantiationExpressionType(exprType *Type, node *ast.Node) hasSignatures = hasSignatures || len(resolved.CallSignatures()) != 0 || len(resolved.ConstructSignatures()) != 0 hasApplicableSignature = hasApplicableSignature || len(callSignatures) != 0 || len(constructSignatures) != 0 if !core.Same(callSignatures, resolved.CallSignatures()) || !core.Same(constructSignatures, resolved.ConstructSignatures()) { - result := c.newObjectType(ObjectFlagsAnonymous|ObjectFlagsInstantiationExpressionType, t.symbol) + symbol := c.newSymbol(ast.SymbolFlagsNone, ast.InternalSymbolNameInstantiationExpression) + debug.Assert(t.symbol != nil, "Instantiation expression source type must have a symbol") + symbol.Declarations = t.symbol.Declarations + result := c.newObjectType(ObjectFlagsAnonymous|ObjectFlagsInstantiationExpressionType, symbol) c.setStructuredTypeMembers(result, resolved.members, callSignatures, constructSignatures, resolved.indexInfos) result.AsInstantiationExpressionType().node = node return result diff --git a/tsc/internal/checker/utilities.go b/tsc/internal/checker/utilities.go index 9c486a5bb90ab..4cd255d9a207e 100644 --- a/tsc/internal/checker/utilities.go +++ b/tsc/internal/checker/utilities.go @@ -438,8 +438,25 @@ func CompareTypes(t1, t2 *Type) int { case t1.flags&(TypeFlagsAny|TypeFlagsUnknown|TypeFlagsString|TypeFlagsNumber|TypeFlagsBoolean|TypeFlagsBigInt|TypeFlagsESSymbol|TypeFlagsVoid|TypeFlagsUndefined|TypeFlagsNull|TypeFlagsNever|TypeFlagsNonPrimitive) != 0: // Only distinguished by type IDs, handled below. case t1.flags&TypeFlagsObject != 0: - // Order unnamed or identically named object types by symbol. - if c := t1.checker.compareSymbols(t1.symbol, t2.symbol); c != 0 { + // Order instantiation expression types without relying on lazy symbol IDs. + // Order other unnamed or identically named object types by symbol. + if t1.objectFlags&ObjectFlagsInstantiationExpressionType != 0 && t2.objectFlags&ObjectFlagsInstantiationExpressionType != 0 { + var declaration1, declaration2 *ast.Node + if t1.symbol != nil && len(t1.symbol.Declarations) != 0 { + declaration1 = t1.symbol.Declarations[0] + } + if t2.symbol != nil && len(t2.symbol.Declarations) != 0 { + declaration2 = t2.symbol.Declarations[0] + } + // A single instantiation expression can produce multiple types for union constituents, + // so compare their source declarations before comparing the shared expression node. + if c := t1.checker.compareNodes(declaration1, declaration2); c != 0 { + return c + } + if c := t1.checker.compareNodes(t1.AsInstantiationExpressionType().node, t2.AsInstantiationExpressionType().node); c != 0 { + return c + } + } else if c := t1.checker.compareSymbols(t1.symbol, t2.symbol); c != 0 { return c } // When object types have the same or no symbol, order by kind. We order type references before other kinds. diff --git a/tsc/testdata/baselines/reference/compiler/classExtendsInstantiationExpressionType.errors.txt b/tsc/testdata/baselines/reference/compiler/classExtendsInstantiationExpressionType.errors.txt new file mode 100644 index 0000000000000..c67d4a71a9c1f --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/classExtendsInstantiationExpressionType.errors.txt @@ -0,0 +1,31 @@ +classExtendsInstantiationExpressionType.ts(16,13): error TS2345: Argument of type '"foo3"' is not assignable to parameter of type '"foo" | "foo2"'. +classExtendsInstantiationExpressionType.ts(21,13): error TS2345: Argument of type '"foo3"' is not assignable to parameter of type '"foo" | "foo2"'. + + +==== classExtendsInstantiationExpressionType.ts (2 errors) ==== + // Regression test for https://github.com/microsoft/TypeScript/issues/64116 + declare function createComponent( + render: { props: Props, events: Events } + ): typeof Component; + + declare class Component> { + $on(event: K, handler: (e: Events[K]) => void): void; + } + + const B = createComponent({ + props: {}, + events: { foo: "", foo2: "2" } + }); + + new B().$on("foo", e => {}); + new B().$on("foo3", e => {}); + ~~~~~~ +!!! error TS2345: Argument of type '"foo3"' is not assignable to parameter of type '"foo" | "foo2"'. + + class C extends B {} + + new C().$on("foo", e => {}); + new C().$on("foo3", e => {}); + ~~~~~~ +!!! error TS2345: Argument of type '"foo3"' is not assignable to parameter of type '"foo" | "foo2"'. + \ No newline at end of file diff --git a/tsc/testdata/baselines/reference/compiler/classExtendsInstantiationExpressionType.symbols b/tsc/testdata/baselines/reference/compiler/classExtendsInstantiationExpressionType.symbols new file mode 100644 index 0000000000000..d2898a714c626 --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/classExtendsInstantiationExpressionType.symbols @@ -0,0 +1,79 @@ +//// [tests/cases/compiler/classExtendsInstantiationExpressionType.ts] //// + +=== classExtendsInstantiationExpressionType.ts === +// Regression test for https://github.com/microsoft/TypeScript/issues/64116 +declare function createComponent( +>createComponent : Symbol(createComponent, Decl(classExtendsInstantiationExpressionType.ts, 0, 0)) +>Props : Symbol(Props, Decl(classExtendsInstantiationExpressionType.ts, 1, 33)) +>Events : Symbol(Events, Decl(classExtendsInstantiationExpressionType.ts, 1, 50)) + + render: { props: Props, events: Events } +>render : Symbol(render, Decl(classExtendsInstantiationExpressionType.ts, 1, 70)) +>props : Symbol(props, Decl(classExtendsInstantiationExpressionType.ts, 2, 13)) +>Props : Symbol(Props, Decl(classExtendsInstantiationExpressionType.ts, 1, 33)) +>events : Symbol(events, Decl(classExtendsInstantiationExpressionType.ts, 2, 27)) +>Events : Symbol(Events, Decl(classExtendsInstantiationExpressionType.ts, 1, 50)) + +): typeof Component; +>Component : Symbol(Component, Decl(classExtendsInstantiationExpressionType.ts, 3, 28)) +>Events : Symbol(Events, Decl(classExtendsInstantiationExpressionType.ts, 1, 50)) + +declare class Component> { +>Component : Symbol(Component, Decl(classExtendsInstantiationExpressionType.ts, 3, 28)) +>Events : Symbol(Events, Decl(classExtendsInstantiationExpressionType.ts, 5, 24)) +>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --)) + + $on(event: K, handler: (e: Events[K]) => void): void; +>$on : Symbol(Component.$on, Decl(classExtendsInstantiationExpressionType.ts, 5, 66)) +>K : Symbol(K, Decl(classExtendsInstantiationExpressionType.ts, 6, 8)) +>Events : Symbol(Events, Decl(classExtendsInstantiationExpressionType.ts, 5, 24)) +>event : Symbol(event, Decl(classExtendsInstantiationExpressionType.ts, 6, 32)) +>K : Symbol(K, Decl(classExtendsInstantiationExpressionType.ts, 6, 8)) +>handler : Symbol(handler, Decl(classExtendsInstantiationExpressionType.ts, 6, 41)) +>e : Symbol(e, Decl(classExtendsInstantiationExpressionType.ts, 6, 52)) +>Events : Symbol(Events, Decl(classExtendsInstantiationExpressionType.ts, 5, 24)) +>K : Symbol(K, Decl(classExtendsInstantiationExpressionType.ts, 6, 8)) +} + +const B = createComponent({ +>B : Symbol(B, Decl(classExtendsInstantiationExpressionType.ts, 9, 5)) +>createComponent : Symbol(createComponent, Decl(classExtendsInstantiationExpressionType.ts, 0, 0)) + + props: {}, +>props : Symbol(props, Decl(classExtendsInstantiationExpressionType.ts, 9, 27)) + + events: { foo: "", foo2: "2" } +>events : Symbol(events, Decl(classExtendsInstantiationExpressionType.ts, 10, 14)) +>foo : Symbol(foo, Decl(classExtendsInstantiationExpressionType.ts, 11, 13)) +>foo2 : Symbol(foo2, Decl(classExtendsInstantiationExpressionType.ts, 11, 22)) + +}); + +new B().$on("foo", e => {}); +>new B().$on : Symbol(Component.$on, Decl(classExtendsInstantiationExpressionType.ts, 5, 66)) +>B : Symbol(B, Decl(classExtendsInstantiationExpressionType.ts, 9, 5)) +>$on : Symbol(Component.$on, Decl(classExtendsInstantiationExpressionType.ts, 5, 66)) +>e : Symbol(e, Decl(classExtendsInstantiationExpressionType.ts, 14, 18)) + +new B().$on("foo3", e => {}); +>new B().$on : Symbol(Component.$on, Decl(classExtendsInstantiationExpressionType.ts, 5, 66)) +>B : Symbol(B, Decl(classExtendsInstantiationExpressionType.ts, 9, 5)) +>$on : Symbol(Component.$on, Decl(classExtendsInstantiationExpressionType.ts, 5, 66)) +>e : Symbol(e, Decl(classExtendsInstantiationExpressionType.ts, 15, 19)) + +class C extends B {} +>C : Symbol(C, Decl(classExtendsInstantiationExpressionType.ts, 15, 29)) +>B : Symbol(B, Decl(classExtendsInstantiationExpressionType.ts, 9, 5)) + +new C().$on("foo", e => {}); +>new C().$on : Symbol(Component.$on, Decl(classExtendsInstantiationExpressionType.ts, 5, 66)) +>C : Symbol(C, Decl(classExtendsInstantiationExpressionType.ts, 15, 29)) +>$on : Symbol(Component.$on, Decl(classExtendsInstantiationExpressionType.ts, 5, 66)) +>e : Symbol(e, Decl(classExtendsInstantiationExpressionType.ts, 19, 18)) + +new C().$on("foo3", e => {}); +>new C().$on : Symbol(Component.$on, Decl(classExtendsInstantiationExpressionType.ts, 5, 66)) +>C : Symbol(C, Decl(classExtendsInstantiationExpressionType.ts, 15, 29)) +>$on : Symbol(Component.$on, Decl(classExtendsInstantiationExpressionType.ts, 5, 66)) +>e : Symbol(e, Decl(classExtendsInstantiationExpressionType.ts, 20, 19)) + diff --git a/tsc/testdata/baselines/reference/compiler/classExtendsInstantiationExpressionType.types b/tsc/testdata/baselines/reference/compiler/classExtendsInstantiationExpressionType.types new file mode 100644 index 0000000000000..8a0be71426697 --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/classExtendsInstantiationExpressionType.types @@ -0,0 +1,89 @@ +//// [tests/cases/compiler/classExtendsInstantiationExpressionType.ts] //// + +=== classExtendsInstantiationExpressionType.ts === +// Regression test for https://github.com/microsoft/TypeScript/issues/64116 +declare function createComponent( +>createComponent : (render: { props: Props; events: Events; }) => typeof Component + + render: { props: Props, events: Events } +>render : { props: Props; events: Events; } +>props : Props +>events : Events + +): typeof Component; +>Component : typeof Component + +declare class Component> { +>Component : Component + + $on(event: K, handler: (e: Events[K]) => void): void; +>$on : (event: K, handler: (e: Events[K]) => void) => void +>event : K +>handler : (e: Events[K]) => void +>e : Events[K] +} + +const B = createComponent({ +>B : { new (): Component<{ foo: string; foo2: string; }>; prototype: Component; } +>createComponent({ props: {}, events: { foo: "", foo2: "2" }}) : { new (): Component<{ foo: string; foo2: string; }>; prototype: Component; } +>createComponent : (render: { props: Props; events: Events; }) => typeof Component +>{ props: {}, events: { foo: "", foo2: "2" }} : { props: {}; events: { foo: string; foo2: string; }; } + + props: {}, +>props : {} +>{} : {} + + events: { foo: "", foo2: "2" } +>events : { foo: string; foo2: string; } +>{ foo: "", foo2: "2" } : { foo: string; foo2: string; } +>foo : string +>"" : "" +>foo2 : string +>"2" : "2" + +}); + +new B().$on("foo", e => {}); +>new B().$on("foo", e => {}) : void +>new B().$on : (event: K, handler: (e: { foo: string; foo2: string; }[K]) => void) => void +>new B() : Component<{ foo: string; foo2: string; }> +>B : { new (): Component<{ foo: string; foo2: string; }>; prototype: Component; } +>$on : (event: K, handler: (e: { foo: string; foo2: string; }[K]) => void) => void +>"foo" : "foo" +>e => {} : (e: string) => void +>e : string + +new B().$on("foo3", e => {}); +>new B().$on("foo3", e => {}) : void +>new B().$on : (event: K, handler: (e: { foo: string; foo2: string; }[K]) => void) => void +>new B() : Component<{ foo: string; foo2: string; }> +>B : { new (): Component<{ foo: string; foo2: string; }>; prototype: Component; } +>$on : (event: K, handler: (e: { foo: string; foo2: string; }[K]) => void) => void +>"foo3" : "foo3" +>e => {} : (e: string) => void +>e : string + +class C extends B {} +>C : C +>B : Component<{ foo: string; foo2: string; }> + +new C().$on("foo", e => {}); +>new C().$on("foo", e => {}) : void +>new C().$on : (event: K, handler: (e: { foo: string; foo2: string; }[K]) => void) => void +>new C() : C +>C : typeof C +>$on : (event: K, handler: (e: { foo: string; foo2: string; }[K]) => void) => void +>"foo" : "foo" +>e => {} : (e: string) => void +>e : string + +new C().$on("foo3", e => {}); +>new C().$on("foo3", e => {}) : void +>new C().$on : (event: K, handler: (e: { foo: string; foo2: string; }[K]) => void) => void +>new C() : C +>C : typeof C +>$on : (event: K, handler: (e: { foo: string; foo2: string; }[K]) => void) => void +>"foo3" : "foo3" +>e => {} : (e: string) => void +>e : string + diff --git a/tsc/testdata/tests/cases/compiler/classExtendsInstantiationExpressionType.ts b/tsc/testdata/tests/cases/compiler/classExtendsInstantiationExpressionType.ts new file mode 100644 index 0000000000000..8d6e0b50b5488 --- /dev/null +++ b/tsc/testdata/tests/cases/compiler/classExtendsInstantiationExpressionType.ts @@ -0,0 +1,23 @@ +// @noEmit: true + +// Regression test for https://github.com/microsoft/TypeScript/issues/64116 +declare function createComponent( + render: { props: Props, events: Events } +): typeof Component; + +declare class Component> { + $on(event: K, handler: (e: Events[K]) => void): void; +} + +const B = createComponent({ + props: {}, + events: { foo: "", foo2: "2" } +}); + +new B().$on("foo", e => {}); +new B().$on("foo3", e => {}); + +class C extends B {} + +new C().$on("foo", e => {}); +new C().$on("foo3", e => {});