Skip to content

Commit d29324e

Browse files
committed
perf(checker): reuse this-independent interface members
Avoid repeated synthetic-this member instantiation for proved completed nongeneric interfaces. Preserve captured and polymorphic this, generic interfaces, and unresolved bases. Refs #63555
1 parent 98add1b commit d29324e

6 files changed

Lines changed: 1600 additions & 1 deletion

File tree

‎tsc/internal/checker/checker.go‎

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19549,6 +19549,13 @@ func (c *Checker) resolveObjectTypeMembers(t *Type, source *Type, typeParameters
1954919549
var constructSignatures []*Signature
1955019550
var indexInfos []*IndexInfo
1955119551
var instantiated bool
19552+
thislessMembers := source.objectFlags&ObjectFlagsClassOrInterface == ObjectFlagsInterface &&
19553+
source.symbol.Flags()&(ast.SymbolFlagsClass|ast.SymbolFlagsInterface) == ast.SymbolFlagsInterface &&
19554+
len(source.AsInterfaceType().TypeParameters()) == 0 &&
19555+
len(source.symbol.Declarations()) != 0 &&
19556+
core.Every(source.symbol.Declarations(), func(d *ast.Node) bool {
19557+
return ast.IsInterfaceDeclaration(d) && d.Flags&ast.NodeFlagsContainsThis == 0
19558+
})
1955219559
resolved := c.resolveDeclaredMembers(source)
1955319560
if slices.Equal(typeParameters, typeArguments) {
1955419561
members = resolved.declaredMembers
@@ -19586,9 +19593,33 @@ func (c *Checker) resolveObjectTypeMembers(t *Type, source *Type, typeParameters
1958619593
indexInfos = core.Concatenate(indexInfos, core.Filter(inheritedIndexInfos, func(info *IndexInfo) bool {
1958719594
return findIndexInfo(indexInfos, info.keyType) == nil
1958819595
}))
19596+
thislessMembers = thislessMembers && c.isThislessResolvedBase(baseType, instantiatedBaseType)
1958919597
}
1959019598
}
1959119599
c.setStructuredTypeMembers(t, members, callSignatures, constructSignatures, indexInfos)
19600+
if thislessMembers && source.AsInterfaceType().baseTypesResolved {
19601+
source.AsInterfaceType().thislessMembers = true
19602+
}
19603+
}
19604+
19605+
// Prove this independence only from completed bases. In particular, mapped types
19606+
// publish an empty member table during resolution; that placeholder is not a proof.
19607+
func (c *Checker) isThislessResolvedBase(base *Type, resolved *Type) bool {
19608+
if resolved.objectFlags&ObjectFlagsMembersResolved == 0 {
19609+
return false
19610+
}
19611+
if base.objectFlags&ObjectFlagsMapped != 0 {
19612+
return base == resolved && base.AsMappedType().membersFinalized && !base.AsMappedType().containsError &&
19613+
!c.couldContainTypeVariables(base) &&
19614+
(base.alias == nil || !core.Some(base.alias.typeArguments, c.couldContainTypeVariables))
19615+
}
19616+
if base.objectFlags&(ObjectFlagsClassOrInterface|ObjectFlagsReference) != 0 {
19617+
target := getTargetType(base)
19618+
return target.objectFlags&ObjectFlagsClassOrInterface == ObjectFlagsInterface &&
19619+
target.AsInterfaceType().baseTypesResolved && len(target.AsInterfaceType().TypeParameters()) == 0 &&
19620+
(target.AsInterfaceType().thisType == nil || target.AsInterfaceType().thislessMembers)
19621+
}
19622+
return false
1959219623
}
1959319624

1959419625
func findIndexInfo(indexInfos []*IndexInfo, keyType *Type) *IndexInfo {
@@ -20011,7 +20042,7 @@ func (c *Checker) getTypeWithThisArgument(t *Type, thisArgument *Type, needAppar
2001120042
target := t.Target()
2001220043
typeArguments := c.getTypeArguments(t)
2001320044
if len(target.AsInterfaceType().TypeParameters()) == len(typeArguments) {
20014-
if thisArgument == nil {
20045+
if thisArgument == nil || target.AsInterfaceType().thislessMembers {
2001520046
thisArgument = target.AsInterfaceType().thisType
2001620047
}
2001720048
return c.createTypeReference(target, core.Concatenate(typeArguments, []*Type{thisArgument}))
@@ -21420,6 +21451,7 @@ func (c *Checker) resolveMappedTypeMembers(t *Type) {
2142021451
forEachType(c.getLowerBoundOfKeyType(constraintType), addMemberForKeyType)
2142121452
}
2142221453
c.setStructuredTypeMembers(t, members, nil, nil, indexInfos)
21454+
t.AsMappedType().membersFinalized = true
2142321455
}
2142421456

2142521457
func (c *Checker) getTypeOfMappedSymbol(symbol *ast.Symbol) *Type {

‎tsc/internal/checker/types.go‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1227,6 +1227,7 @@ type InterfaceType struct {
12271227
allTypeParameters []*Type // Type parameters (outer + local + thisType)
12281228
outerTypeParameterCount int // Count of outer type parameters
12291229
thisType *Type // The "this" type (nil if none)
1230+
thislessMembers bool // Completed non-generic interface members are unaffected by a this argument
12301231
baseTypesResolved bool
12311232
declaredMembersResolved bool
12321233
resolvedBaseConstructorType *Type
@@ -1326,6 +1327,7 @@ type MappedType struct {
13261327
templateType *Type
13271328
modifiersType *Type
13281329
resolvedApparentType *Type
1330+
membersFinalized bool
13291331
containsError bool
13301332
}
13311333

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
//// [tests/cases/compiler/thisIndependentInterfaceMembers.ts] ////
2+
3+
//// [thisIndependentInterfaceMembers.ts]
4+
// The member reuse optimization must preserve real, outer and foreign `this`.
5+
namespace capturedOuterThis {
6+
class Owner {
7+
read() {
8+
type OuterMap = { readonly [K in "owner"]: this };
9+
interface Base extends OuterMap {}
10+
interface Derived extends Base { readonly derived: "derived" }
11+
const value = null! as Derived;
12+
const exact: this = value.owner;
13+
// @ts-expect-error The owner is not the mapped property's interface.
14+
const wrong: Derived = value.owner;
15+
return exact;
16+
}
17+
}
18+
class SpecialOwner extends Owner { readonly special = "special" }
19+
declare const owner: SpecialOwner;
20+
const exactOwner: SpecialOwner = owner.read();
21+
}
22+
23+
namespace fluentAlias {
24+
interface Fluent { self(): this }
25+
type FluentAlias = Fluent;
26+
type Marker = { readonly [K in "marker"]: "marker" };
27+
interface Mixed extends Marker, FluentAlias {}
28+
interface Derived extends Mixed { readonly derived: "derived" }
29+
declare const value: Derived;
30+
const exact: Derived = value.self();
31+
}
32+
33+
namespace foreignMappedThis {
34+
interface Foreign { self(): this; readonly foreign: "foreign" }
35+
type Copy<T> = { readonly [K in keyof T]: T[K] };
36+
interface Copied extends Copy<Foreign> {}
37+
interface Derived extends Copied { readonly derived: "derived" }
38+
declare const value: Derived;
39+
const exact: Foreign = value.self();
40+
// @ts-expect-error A concrete map does not rebind the foreign method's this.
41+
const wrong: Derived = value.self();
42+
}
43+
44+
namespace mergedDeclarations {
45+
type Marker = { readonly [K in "marker"]: "marker" };
46+
interface Merged extends Marker {}
47+
interface Merged { self(): this }
48+
interface Derived extends Merged { readonly derived: "derived" }
49+
declare const value: Derived;
50+
const exact: Derived = value.self();
51+
const marker: "marker" = value.marker;
52+
53+
interface Plain extends Marker {}
54+
interface Plain { readonly merged: "merged" }
55+
interface PlainDerived extends Plain { readonly derived: "derived" }
56+
declare const plain: PlainDerived;
57+
const inherited: "merged" = plain.merged;
58+
}
59+
60+
namespace genericInterfaces {
61+
type Marker<T> = { readonly [K in "value"]: T };
62+
interface Generic<T> extends Marker<T> { self(): this }
63+
interface NumberValue extends Generic<number> { readonly numeric: "numeric" }
64+
interface StringValue extends Generic<string> { readonly textual: "textual" }
65+
declare const numeric: NumberValue;
66+
declare const textual: StringValue;
67+
const numberValue: number = numeric.value;
68+
const stringValue: string = textual.value;
69+
const exact: NumberValue = numeric.self();
70+
// @ts-expect-error Distinct type arguments may not share member substitutions.
71+
const wrong: number = textual.value;
72+
73+
function enclosing<T>(input: T) {
74+
type OuterMap = { readonly [K in "value"]: T };
75+
interface Local extends OuterMap {}
76+
interface Derived extends Local { readonly local: "local" }
77+
const value = null! as Derived;
78+
const exact: T = value.value;
79+
return { input, exact };
80+
}
81+
const numericOuter: number = enclosing(1).exact;
82+
const stringOuter: string = enclosing("text").exact;
83+
// @ts-expect-error An enclosing generic remains specific to the invocation.
84+
const wrongOuter: number = enclosing("text").exact;
85+
}
86+
87+
namespace mappedAliasArguments {
88+
type Value<T> = { readonly [K in "value"]: T };
89+
type Alias<T> = Value<T>;
90+
interface Generic<T> extends Alias<T> {}
91+
interface NumberValue extends Generic<number> {}
92+
interface StringValue extends Generic<string> {}
93+
declare const numeric: NumberValue;
94+
declare const textual: StringValue;
95+
const numberValue: number = numeric.value;
96+
const stringValue: string = textual.value;
97+
// @ts-expect-error A mapped alias must retain its variable-containing argument.
98+
const wrong: number = textual.value;
99+
100+
interface Foreign { self(): this; readonly foreign: "foreign" }
101+
type Copy<T> = { readonly [K in keyof T]: T[K] };
102+
type ConcreteAlias = Copy<Foreign>;
103+
interface Copied extends ConcreteAlias {}
104+
interface Derived extends Copied { readonly derived: "derived" }
105+
declare const derived: Derived;
106+
const foreign: Foreign = derived.self();
107+
// @ts-expect-error Mapping must preserve foreign-this even behind an alias.
108+
const notDerived: Derived = derived.self();
109+
}
110+
111+
namespace recursiveMembers {
112+
type Links = { readonly [K in "next"]: Node };
113+
interface Node extends Links { readonly value: number }
114+
interface Derived extends Node { readonly own: string }
115+
declare const value: Derived;
116+
const next: Node = value.next;
117+
const nested: number = value.next.next.value;
118+
// @ts-expect-error Recursive members retain the declared Node, not Derived.
119+
const wrong: Derived = value.next;
120+
121+
// This forces lookup of a mapped member through a recursively referenced type.
122+
type Reenter<T> = { readonly [K in "item"]: T };
123+
interface Recursive extends Reenter<Recursive> { readonly own: "own" }
124+
interface RecursiveDerived extends Recursive { readonly more: "more" }
125+
declare const recursive: RecursiveDerived;
126+
const own: "own" = recursive.item.item.own;
127+
// @ts-expect-error A re-entrant lookup does not substitute a descendant.
128+
const wrongRecursive: RecursiveDerived = recursive.item;
129+
}
130+
131+
namespace rejectedBases {
132+
type LeftMap = { readonly [K in "value"]: number };
133+
type RightMap = { readonly [K in "value"]: string };
134+
interface Left extends LeftMap {}
135+
interface Right extends RightMap {}
136+
// @ts-expect-error Conflicting inherited properties must still reject.
137+
interface Conflict extends Left, Right {}
138+
139+
// @ts-expect-error A circular mapped constraint remains an error.
140+
type Reentered = { [K in keyof Circular]: Circular[K] };
141+
// @ts-expect-error Resolution placeholders must not prove valid ancestry.
142+
interface Circular extends Reentered { readonly own: string }
143+
144+
// @ts-expect-error An unresolved mapped key still reports its error.
145+
type ErrorMap = { [K in MissingKey]: number };
146+
interface ErrorBase extends ErrorMap {}
147+
// @ts-expect-error The erroneous key must not erase the numeric index contract.
148+
interface ErrorDerived extends ErrorBase { readonly own: "own" }
149+
}
150+
151+
namespace concreteMappedMembers {
152+
type Value<T> = { readonly [K in "value"]: T };
153+
interface Plain extends Value<number> {}
154+
interface Derived extends Plain { readonly own: "own" }
155+
declare const value: Derived;
156+
const exact: number = value.value;
157+
// @ts-expect-error Concrete mapped properties keep their exact type.
158+
const wrong: string = value.value;
159+
160+
interface WithIndex { readonly [key: string]: number }
161+
interface IndexBase extends WithIndex {}
162+
interface IndexDerived extends IndexBase { readonly own: 1 }
163+
declare const indexed: IndexDerived;
164+
const entry: number = indexed["arbitrary"];
165+
166+
type Callable = { (): number; readonly [K: string]: unknown };
167+
interface CallBase extends Callable {}
168+
interface CallDerived extends CallBase { readonly own: "own" }
169+
declare const callable: CallDerived;
170+
const result: number = callable();
171+
}
172+
173+
174+
//// [thisIndependentInterfaceMembers.js]
175+
"use strict";
176+
// The member reuse optimization must preserve real, outer and foreign `this`.
177+
var capturedOuterThis;
178+
(function (capturedOuterThis) {
179+
class Owner {
180+
read() {
181+
const value = null;
182+
const exact = value.owner;
183+
// @ts-expect-error The owner is not the mapped property's interface.
184+
const wrong = value.owner;
185+
return exact;
186+
}
187+
}
188+
class SpecialOwner extends Owner {
189+
special = "special";
190+
}
191+
const exactOwner = owner.read();
192+
})(capturedOuterThis || (capturedOuterThis = {}));
193+
var fluentAlias;
194+
(function (fluentAlias) {
195+
const exact = value.self();
196+
})(fluentAlias || (fluentAlias = {}));
197+
var foreignMappedThis;
198+
(function (foreignMappedThis) {
199+
const exact = value.self();
200+
// @ts-expect-error A concrete map does not rebind the foreign method's this.
201+
const wrong = value.self();
202+
})(foreignMappedThis || (foreignMappedThis = {}));
203+
var mergedDeclarations;
204+
(function (mergedDeclarations) {
205+
const exact = value.self();
206+
const marker = value.marker;
207+
const inherited = plain.merged;
208+
})(mergedDeclarations || (mergedDeclarations = {}));
209+
var genericInterfaces;
210+
(function (genericInterfaces) {
211+
const numberValue = numeric.value;
212+
const stringValue = textual.value;
213+
const exact = numeric.self();
214+
// @ts-expect-error Distinct type arguments may not share member substitutions.
215+
const wrong = textual.value;
216+
function enclosing(input) {
217+
const value = null;
218+
const exact = value.value;
219+
return { input, exact };
220+
}
221+
const numericOuter = enclosing(1).exact;
222+
const stringOuter = enclosing("text").exact;
223+
// @ts-expect-error An enclosing generic remains specific to the invocation.
224+
const wrongOuter = enclosing("text").exact;
225+
})(genericInterfaces || (genericInterfaces = {}));
226+
var mappedAliasArguments;
227+
(function (mappedAliasArguments) {
228+
const numberValue = numeric.value;
229+
const stringValue = textual.value;
230+
// @ts-expect-error A mapped alias must retain its variable-containing argument.
231+
const wrong = textual.value;
232+
const foreign = derived.self();
233+
// @ts-expect-error Mapping must preserve foreign-this even behind an alias.
234+
const notDerived = derived.self();
235+
})(mappedAliasArguments || (mappedAliasArguments = {}));
236+
var recursiveMembers;
237+
(function (recursiveMembers) {
238+
const next = value.next;
239+
const nested = value.next.next.value;
240+
// @ts-expect-error Recursive members retain the declared Node, not Derived.
241+
const wrong = value.next;
242+
const own = recursive.item.item.own;
243+
// @ts-expect-error A re-entrant lookup does not substitute a descendant.
244+
const wrongRecursive = recursive.item;
245+
})(recursiveMembers || (recursiveMembers = {}));
246+
var concreteMappedMembers;
247+
(function (concreteMappedMembers) {
248+
const exact = value.value;
249+
// @ts-expect-error Concrete mapped properties keep their exact type.
250+
const wrong = value.value;
251+
const entry = indexed["arbitrary"];
252+
const result = callable();
253+
})(concreteMappedMembers || (concreteMappedMembers = {}));

0 commit comments

Comments
 (0)