Skip to content

Commit b3856bd

Browse files
committed
When structurally comparing similar types, check if we are already in the middle of a more general comparison of those same types
1 parent 664ed17 commit b3856bd

5 files changed

+928
-0
lines changed

src/compiler/checker.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17773,6 +17773,13 @@ namespace ts {
1777317773
return result;
1777417774
}
1777517775

17776+
function getInstanceOfAliasOrReferenceWithMarker(input: Type, typeArguments: readonly Type[]) {
17777+
const s = input.aliasSymbol ? getTypeAliasInstantiation(input.aliasSymbol, typeArguments) : createTypeReference((<TypeReference>input).target, typeArguments);
17778+
if (s.aliasSymbol) s.aliasTypeArgumentsContainsMarker = true;
17779+
else (<TypeReference>s).objectFlags |= ObjectFlags.MarkerType;
17780+
return s;
17781+
}
17782+
1777617783
function structuredTypeRelatedToWorker(source: Type, target: Type, reportErrors: boolean, intersectionState: IntersectionState): Ternary {
1777717784
if (intersectionState & IntersectionState.PropertyCheck) {
1777817785
return propertiesRelatedTo(source, target, reportErrors, /*excludedProperties*/ undefined, IntersectionState.None);
@@ -17861,6 +17868,67 @@ namespace ts {
1786117868
}
1786217869
}
1786317870

17871+
// If a more _general_ version of the source and target are being compared, consider them related with assumptions
17872+
// eg, if { x: Q } and { x: Q, y: A } are being compared and we're about to look at { x: Q' } and { x: Q', y: A } where Q'
17873+
// is some specialization or subtype of Q
17874+
// This is difficult to detect generally, so we scan for prior comparisons of the same instantiated type, and match up matching
17875+
// type arguments into sets to create a canonicalization based on those matches
17876+
if (relation !== identityRelation && ((source.aliasSymbol && !source.aliasTypeArgumentsContainsMarker && source.aliasTypeArguments) || (getObjectFlags(source) & ObjectFlags.Reference && !!getTypeArguments(<TypeReference>source).length && !(getObjectFlags(source) & ObjectFlags.MarkerType))) &&
17877+
((target.aliasSymbol && !target.aliasTypeArgumentsContainsMarker && target.aliasTypeArguments) || (getObjectFlags(target) & ObjectFlags.Reference && !!getTypeArguments(<TypeReference>target).length && !(getObjectFlags(target) & ObjectFlags.MarkerType)))) {
17878+
if (source.aliasSymbol || target.aliasSymbol || (<TypeReference>source).target !== (<TypeReference>target).target) { // ensure like symbols are just handled by standard variance analysis
17879+
const sourceTypeArguments = source.aliasTypeArguments || getTypeArguments(<TypeReference>source);
17880+
const sourceHasMarker = some(sourceTypeArguments, a => a === markerOtherType);
17881+
const targetTypeArguments = target.aliasTypeArguments || getTypeArguments(<TypeReference>target);
17882+
const targetHasMarker = some(targetTypeArguments, a => a === markerOtherType);
17883+
// We're using `markerOtherType` as an existential, so we can't use it again if it's already in use,
17884+
// as we'd get spurious equivalencies - we'd need to use a second existential type, and once we're doing
17885+
// that we lose a lot of the benefit of canonicalizing back to a single-existential comparison, since then
17886+
// we'd need to manufacture new type identities for every new existential we make
17887+
// The above checks don't catch all cases this can occur, as they can only detect when the containing type
17888+
// was flagged during construction as containing a marker; however if a marker enters a type through instantiation
17889+
// we need to catch that here.
17890+
// We only do this when there's a handful of possible ways to match the type parameters up, as otherwise we manufacture
17891+
// an inordinate quantity of types just to calculate their IDs!
17892+
if (!sourceHasMarker && !targetHasMarker && sourceTypeArguments.length * targetTypeArguments.length < 10) {
17893+
const originalKey = getRelationKey(source, target, intersectionState, relation);
17894+
for (let i = 0; i < sourceTypeArguments.length; i++) {
17895+
for (let j = 0; j < targetTypeArguments.length; j++) {
17896+
if ((!(sourceTypeArguments[i].flags & TypeFlags.TypeParameter) && !isTypeAny(sourceTypeArguments[i]) && sourceTypeArguments[i] === targetTypeArguments[j]) ||
17897+
// Similarly, if we're comparing X<Q> to Z<any>, X<Q> is assignable to Z<any> trivially if X<?> is assignable to Z<?>
17898+
(!(sourceTypeArguments[i].flags & TypeFlags.TypeParameter) && isTypeAny(targetTypeArguments[j])) ||
17899+
// Again, but for `X<any>` vs `Z<Q>`
17900+
(isTypeAny(sourceTypeArguments[i]) && !(targetTypeArguments[j].flags & TypeFlags.TypeParameter)) ||
17901+
// Likewise, if we're comparing X<U> to Z<U> and are already comparing X<T> to Z<T>, we can assume it to be true
17902+
!!(sourceTypeArguments[i].flags & TypeFlags.TypeParameter) && sourceTypeArguments[i] === targetTypeArguments[j]) {
17903+
const sourceClone = sourceTypeArguments.slice();
17904+
sourceClone[i] = markerOtherType;
17905+
const s = getInstanceOfAliasOrReferenceWithMarker(source, sourceClone);
17906+
const targetClone = targetTypeArguments.slice();
17907+
targetClone[j] = markerOtherType;
17908+
const t = getInstanceOfAliasOrReferenceWithMarker(target, targetClone);
17909+
// If the marker-instantiated form looks "the same" as the type we already have (eg,
17910+
// because we replace unconstrained generics with unconstrained generics), skip the check
17911+
// since we'll otherwise deliver a spurious `Maybe` result from the key _just_ set upon
17912+
// entry into `recursiveTypeRelatedTo`
17913+
const existentialKey = getRelationKey(s, t, intersectionState, relation);
17914+
if (existentialKey !== originalKey) {
17915+
// We don't actually trigger the comparison, since we'd rather not do an extra comparison
17916+
// if we haven't already started that more general comparison; instead we just look for the
17917+
// key in the maybeKeys stack
17918+
for (let i = 0; i < maybeCount; i++) {
17919+
// If source and target are already being compared, consider them related with assumptions
17920+
if (existentialKey === maybeKeys[i]) {
17921+
return Ternary.Maybe;
17922+
}
17923+
}
17924+
}
17925+
}
17926+
}
17927+
}
17928+
}
17929+
}
17930+
}
17931+
1786417932
// For a generic type T and a type U that is assignable to T, [...U] is assignable to T, U is assignable to readonly [...T],
1786517933
// and U is assignable to [...T] when U is constrained to a mutable array or tuple type.
1786617934
if (isSingleElementGenericTupleType(source) && !source.target.readonly && (result = isRelatedTo(getTypeArguments(source)[0], target)) ||
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
//// [performanceComparisonOfStructurallyIdenticalInterfacesWithGenericSignatures.ts]
2+
export declare type ThenArg<T> = T extends any ? any : T extends PromiseLike<infer U> ? U : T;
3+
4+
export interface InterfaceA<T> {
5+
filter(callback: (newValue: T, oldValue: T) => boolean): InterfaceA<T>;
6+
map<D>(callback: (value: T) => D): InterfaceA<D>;
7+
await<R extends ThenArg<T>>(): InterfaceA<R>;
8+
awaitLatest<R extends ThenArg<T>>(): InterfaceA<R>;
9+
awaitOrdered<R extends ThenArg<T>>(): InterfaceA<R>;
10+
awaitOrdered2<R extends ThenArg<T>>(): InterfaceA<R>;
11+
awaitOrdered3<R extends ThenArg<T>>(): InterfaceA<R>;
12+
awaitOrdered4<R extends ThenArg<T>>(): InterfaceA<R>;
13+
awaitOrdered5<R extends ThenArg<T>>(): InterfaceA<R>;
14+
awaitOrdered6<R extends ThenArg<T>>(): InterfaceA<R>;
15+
awaitOrdered7<R extends ThenArg<T>>(): InterfaceA<R>;
16+
awaitOrdered8<R extends ThenArg<T>>(): InterfaceA<R>;
17+
awaitOrdered9<R extends ThenArg<T>>(): InterfaceA<R>;
18+
}
19+
20+
export interface InterfaceB<T> extends InterfaceA<T> {
21+
map<D>(callback: (value: T) => D): InterfaceB<D>;
22+
await<R extends ThenArg<T>>(): InterfaceB<R>;
23+
awaitLatest<R extends ThenArg<T>>(): InterfaceB<R>;
24+
awaitOrdered<R extends ThenArg<T>>(): InterfaceB<R>;
25+
awaitOrdered2<R extends ThenArg<T>>(): InterfaceB<R>;
26+
awaitOrdered3<R extends ThenArg<T>>(): InterfaceB<R>;
27+
awaitOrdered4<R extends ThenArg<T>>(): InterfaceB<R>;
28+
awaitOrdered5<R extends ThenArg<T>>(): InterfaceB<R>;
29+
awaitOrdered6<R extends ThenArg<T>>(): InterfaceB<R>;
30+
awaitOrdered7<R extends ThenArg<T>>(): InterfaceB<R>;
31+
awaitOrdered8<R extends ThenArg<T>>(): InterfaceB<R>;
32+
awaitOrdered9<R extends ThenArg<T>>(): InterfaceB<R>;
33+
}
34+
35+
export class A<T> implements InterfaceB<T> {
36+
public filter(callback: (newValue: T, oldValue: T) => boolean): B<T> {
37+
return undefined as any;
38+
}
39+
40+
public map<D>(callback: (value: T) => D): B<D> {
41+
return undefined as any;
42+
}
43+
44+
public await<R extends ThenArg<T>>(): B<R> {
45+
return undefined as any;
46+
}
47+
48+
public awaitOrdered<R extends ThenArg<T>>(): B<R> {
49+
return undefined as any;
50+
}
51+
52+
public awaitOrdered2<R extends ThenArg<T>>(): B<R> {
53+
return undefined as any;
54+
}
55+
56+
public awaitOrdered3<R extends ThenArg<T>>(): B<R> {
57+
return undefined as any;
58+
}
59+
60+
public awaitOrdered4<R extends ThenArg<T>>(): B<R> {
61+
return undefined as any;
62+
}
63+
64+
public awaitOrdered5<R extends ThenArg<T>>(): B<R> {
65+
return undefined as any;
66+
}
67+
68+
public awaitOrdered6<R extends ThenArg<T>>(): B<R> {
69+
return undefined as any;
70+
}
71+
72+
public awaitOrdered7<R extends ThenArg<T>>(): B<R> {
73+
return undefined as any;
74+
}
75+
76+
public awaitOrdered8<R extends ThenArg<T>>(): B<R> {
77+
return undefined as any;
78+
}
79+
80+
public awaitOrdered9<R extends ThenArg<T>>(): B<R> {
81+
return undefined as any;
82+
}
83+
84+
public awaitLatest<R extends ThenArg<T>>(): B<R> {
85+
return undefined as any;
86+
}
87+
}
88+
89+
export class B<T> extends A<T> { }
90+
91+
//// [performanceComparisonOfStructurallyIdenticalInterfacesWithGenericSignatures.js]
92+
"use strict";
93+
var __extends = (this && this.__extends) || (function () {
94+
var extendStatics = function (d, b) {
95+
extendStatics = Object.setPrototypeOf ||
96+
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
97+
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
98+
return extendStatics(d, b);
99+
};
100+
return function (d, b) {
101+
if (typeof b !== "function" && b !== null)
102+
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
103+
extendStatics(d, b);
104+
function __() { this.constructor = d; }
105+
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
106+
};
107+
})();
108+
exports.__esModule = true;
109+
exports.B = exports.A = void 0;
110+
var A = /** @class */ (function () {
111+
function A() {
112+
}
113+
A.prototype.filter = function (callback) {
114+
return undefined;
115+
};
116+
A.prototype.map = function (callback) {
117+
return undefined;
118+
};
119+
A.prototype.await = function () {
120+
return undefined;
121+
};
122+
A.prototype.awaitOrdered = function () {
123+
return undefined;
124+
};
125+
A.prototype.awaitOrdered2 = function () {
126+
return undefined;
127+
};
128+
A.prototype.awaitOrdered3 = function () {
129+
return undefined;
130+
};
131+
A.prototype.awaitOrdered4 = function () {
132+
return undefined;
133+
};
134+
A.prototype.awaitOrdered5 = function () {
135+
return undefined;
136+
};
137+
A.prototype.awaitOrdered6 = function () {
138+
return undefined;
139+
};
140+
A.prototype.awaitOrdered7 = function () {
141+
return undefined;
142+
};
143+
A.prototype.awaitOrdered8 = function () {
144+
return undefined;
145+
};
146+
A.prototype.awaitOrdered9 = function () {
147+
return undefined;
148+
};
149+
A.prototype.awaitLatest = function () {
150+
return undefined;
151+
};
152+
return A;
153+
}());
154+
exports.A = A;
155+
var B = /** @class */ (function (_super) {
156+
__extends(B, _super);
157+
function B() {
158+
return _super !== null && _super.apply(this, arguments) || this;
159+
}
160+
return B;
161+
}(A));
162+
exports.B = B;

0 commit comments

Comments
 (0)