Skip to content

Allow inference to explore multiple instances of the same symbol #31633

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 87 additions & 25 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15598,6 +15598,13 @@ namespace ts {
return result;
}

function getInstanceOfAliasOrReferenceWithMarker(input: Type, typeArguments: readonly Type[]) {
const s = input.aliasSymbol ? getTypeAliasInstantiation(input.aliasSymbol, typeArguments) : createTypeReference((<TypeReference>input).target, typeArguments);
if (s.aliasSymbol) s.aliasTypeArgumentsContainsMarker = true;
else (<TypeReference>s).objectFlags |= ObjectFlags.MarkerType;
return s;
}

function structuredTypeRelatedTo(source: Type, target: Type, reportErrors: boolean, intersectionState: IntersectionState): Ternary {
const flags = source.flags & target.flags;
if (relation === identityRelation && !(flags & TypeFlags.Object)) {
Expand Down Expand Up @@ -15652,6 +15659,57 @@ namespace ts {
}
}

// If a more _general_ version of the source and target are being compared, consider them related with assumptions
// 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'
// is some specialization or subtype of Q
// This is difficult to detect generally, so we scan for prior comparisons of the same instantiated type, and match up matching
// type arguments into sets to create a canonicalization based on those matches
if (relation !== identityRelation && ((source.aliasSymbol && !source.aliasTypeArgumentsContainsMarker && source.aliasTypeArguments) || (getObjectFlags(source) & ObjectFlags.Reference && !!getTypeArguments(<TypeReference>source).length && !(getObjectFlags(source) & ObjectFlags.MarkerType))) &&
((target.aliasSymbol && !target.aliasTypeArgumentsContainsMarker && target.aliasTypeArguments) || (getObjectFlags(target) & ObjectFlags.Reference && !!getTypeArguments(<TypeReference>target).length && !(getObjectFlags(target) & ObjectFlags.MarkerType)))) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might be worth it to extract a function for this predicate, if only to make the name obvious.

(the line length is also too long)

if (source.aliasSymbol || target.aliasSymbol || (<TypeReference>source).target !== (<TypeReference>target).target) { // ensure like symbols are just handled by standard variance analysis
const sourceTypeArguments = source.aliasTypeArguments || getTypeArguments(<TypeReference>source);
const sourceHasMarker = some(sourceTypeArguments, a => a === markerOtherType);
const targetTypeArguments = target.aliasTypeArguments || getTypeArguments(<TypeReference>target);
const targetHasMarker = some(targetTypeArguments, a => a === markerOtherType);
// We're using `markerOtherType` as an existential, so we can't use it again if it's already in use,
// as we'd get spurious equivalencies - we'd need to use a second existential type, and once we're doing
// that we lose a lot of the benefit of canonicalizing back to a single-existential comparison, since then
// we'd need to manufacture new type identities for every new existential we make
// The above checks don't catch all cases this can occur, as they can only detect when the containing type
// was flagged during construction as containing a marker; however if a marker enters a type through instantiation
// we need to catch that here.
if (!sourceHasMarker && !targetHasMarker) {
const originalKey = getRelationKey(source, target, intersectionState, relation);
for (let i = 0; i < sourceTypeArguments.length; i++) {
for (let j = 0; j < targetTypeArguments.length; j++) {
if ((!(sourceTypeArguments[i].flags & TypeFlags.TypeParameter) && !isTypeAny(sourceTypeArguments[i]) && isTypeIdenticalTo(sourceTypeArguments[i], targetTypeArguments[j])) ||
// Similarly, if we're comparing X<Q> to Z<any>, X<Q> is assignable to Z<any> trivially if X<?> is assignable to Z<?>
(!(sourceTypeArguments[i].flags & TypeFlags.TypeParameter) && isTypeAny(targetTypeArguments[j])) ||
// Again, but for `X<any>` vs `Z<Q>`
(isTypeAny(sourceTypeArguments[i]) && !(targetTypeArguments[j].flags & TypeFlags.TypeParameter))) {
const sourceClone = sourceTypeArguments.slice();
sourceClone[i] = markerOtherType;
const s = getInstanceOfAliasOrReferenceWithMarker(source, sourceClone);
const targetClone = targetTypeArguments.slice();
targetClone[j] = markerOtherType;
const t = getInstanceOfAliasOrReferenceWithMarker(target, targetClone);
// If the marker-instantiated form looks "the same" as the type we already have (eg,
// because we replace unconstrained generics with unconstrained generics), skip the check
// since we'll otherwise deliver a spurious `Maybe` result from the key _just_ set upon
// entry into `recursiveTypeRelatedTo`
if (getRelationKey(s, t, intersectionState, relation) !== originalKey) {
const result = isRelatedTo(s, t, /*reportErrors*/ false);
if (result) {
return result;
}
}
}
}
}
}
}
}

if (target.flags & TypeFlags.TypeParameter) {
// A source type { [P in Q]: X } is related to a target type T if keyof T is related to Q and X is related to T[Q].
if (getObjectFlags(source) & ObjectFlags.Mapped && isRelatedTo(getIndexType(target), getConstraintTypeFromMappedType(<MappedType>source))) {
Expand Down Expand Up @@ -16835,7 +16893,7 @@ namespace ts {
// In addition, this will also detect when an indexed access has been chained off of 5 or more times (which is essentially
// the dual of the structural comparison), and likewise mark the type as deeply nested, potentially adding false positives
// for finite but deeply expanding indexed accesses (eg, for `Q[P1][P2][P3][P4][P5]`).
function isDeeplyNestedType(type: Type, stack: Type[], depth: number): boolean {
function isDeeplyNestedType(type: Type, stack: Type[], depth: number, maxCount = 5): boolean {
// We track all object types that have an associated symbol (representing the origin of the type)
if (depth >= 5 && type.flags & TypeFlags.Object && !isObjectOrArrayLiteralType(type)) {
const symbol = type.symbol;
Expand All @@ -16845,7 +16903,7 @@ namespace ts {
const t = stack[i];
if (t.flags & TypeFlags.Object && t.symbol === symbol) {
count++;
if (count >= 5) return true;
if (count >= maxCount) return true;
}
}
}
Expand Down Expand Up @@ -17886,18 +17944,44 @@ namespace ts {
}

function inferTypes(inferences: InferenceInfo[], originalSource: Type, originalTarget: Type, priority: InferencePriority = 0, contravariant = false) {
let symbolStack: Symbol[];
let sourceStack: Type[];
let targetStack: Type[];
let depth = 0;
let visited: Map<number>;
let bivariant = false;
let propagationType: Type;
let inferencePriority = InferencePriority.MaxValue;
let allowComplexConstraintInference = true;
let expandingFlags = ExpandingFlags.None;
inferFromTypes(originalSource, originalTarget);

function inferFromTypes(source: Type, target: Type): void {
const maxdepth = every(inferences, i => !!(length(i.candidates) || length(i.contraCandidates))) ? 1 : 5; // Expand up to 5 layers deep unless we've found an inference, in which case stop at 1
const saveExpandingFlags = expandingFlags;
if (!(expandingFlags & ExpandingFlags.Source) && isDeeplyNestedType(source, sourceStack, depth, maxdepth)) expandingFlags |= ExpandingFlags.Source;
if (!(expandingFlags & ExpandingFlags.Target) && isDeeplyNestedType(target, targetStack, depth, maxdepth)) expandingFlags |= ExpandingFlags.Target;
if (expandingFlags === ExpandingFlags.Both) {
expandingFlags = saveExpandingFlags;
return;
}

if (!sourceStack) {
sourceStack = [];
targetStack = [];
}
sourceStack[depth] = source;
targetStack[depth] = target;
depth++;
inferFromTypesWorker(source, target);
depth--;
expandingFlags = saveExpandingFlags;
}

function inferFromTypesWorker(source: Type, target: Type): void {
if (!couldContainTypeVariables(target)) {
return;
}

if (source === wildcardType) {
// We are inferring from an 'any' type. We want to infer this type for every type parameter
// referenced in the target type, so we record it as the propagation type and infer from the
Expand Down Expand Up @@ -18318,28 +18402,6 @@ namespace ts {
}

function inferFromObjectTypes(source: Type, target: Type) {
// If we are already processing another target type with the same associated symbol (such as
// an instantiation of the same generic type), we do not explore this target as it would yield
// no further inferences. We exclude the static side of classes from this check since it shares
// its symbol with the instance side which would lead to false positives.
const isNonConstructorObject = target.flags & TypeFlags.Object &&
!(getObjectFlags(target) & ObjectFlags.Anonymous && target.symbol && target.symbol.flags & SymbolFlags.Class);
const symbol = isNonConstructorObject ? target.symbol : undefined;
if (symbol) {
if (contains(symbolStack, symbol)) {
inferencePriority = InferencePriority.Circularity;
return;
}
(symbolStack || (symbolStack = [])).push(symbol);
inferFromObjectTypesWorker(source, target);
symbolStack.pop();
}
else {
inferFromObjectTypesWorker(source, target);
}
}

function inferFromObjectTypesWorker(source: Type, target: Type) {
if (getObjectFlags(source) & ObjectFlags.Reference && getObjectFlags(target) & ObjectFlags.Reference && (
(<TypeReference>source).target === (<TypeReference>target).target || isArrayType(source) && isArrayType(target))) {
// If source and target are references to the same generic type, infer from type arguments
Expand Down
18 changes: 18 additions & 0 deletions tests/baselines/reference/mappedTypeAliasSubstitutability.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//// [mappedTypeAliasSubstitutability.ts]
// repro from https://github.com/microsoft/TypeScript/issues/31616

const v = { test: { smth: 5 } };
type Field<A extends string, R> = { [K in A]: R };
const f = <A extends string, B extends string, R>(x: { [K in A]: Field<B, R> } ): R => ({} as any);
const g = <A extends string, B extends string, R>(x: Field<A, Field<B, R>>): R => ({} as any);
const r1 = f(v); // number
const r2 = g(v); // unknown


//// [mappedTypeAliasSubstitutability.js]
// repro from https://github.com/microsoft/TypeScript/issues/31616
var v = { test: { smth: 5 } };
var f = function (x) { return ({}); };
var g = function (x) { return ({}); };
var r1 = f(v); // number
var r2 = g(v); // unknown
52 changes: 52 additions & 0 deletions tests/baselines/reference/mappedTypeAliasSubstitutability.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
=== tests/cases/compiler/mappedTypeAliasSubstitutability.ts ===
// repro from https://github.com/microsoft/TypeScript/issues/31616

const v = { test: { smth: 5 } };
>v : Symbol(v, Decl(mappedTypeAliasSubstitutability.ts, 2, 5))
>test : Symbol(test, Decl(mappedTypeAliasSubstitutability.ts, 2, 11))
>smth : Symbol(smth, Decl(mappedTypeAliasSubstitutability.ts, 2, 19))

type Field<A extends string, R> = { [K in A]: R };
>Field : Symbol(Field, Decl(mappedTypeAliasSubstitutability.ts, 2, 32))
>A : Symbol(A, Decl(mappedTypeAliasSubstitutability.ts, 3, 11))
>R : Symbol(R, Decl(mappedTypeAliasSubstitutability.ts, 3, 28))
>K : Symbol(K, Decl(mappedTypeAliasSubstitutability.ts, 3, 37))
>A : Symbol(A, Decl(mappedTypeAliasSubstitutability.ts, 3, 11))
>R : Symbol(R, Decl(mappedTypeAliasSubstitutability.ts, 3, 28))

const f = <A extends string, B extends string, R>(x: { [K in A]: Field<B, R> } ): R => ({} as any);
>f : Symbol(f, Decl(mappedTypeAliasSubstitutability.ts, 4, 5))
>A : Symbol(A, Decl(mappedTypeAliasSubstitutability.ts, 4, 11))
>B : Symbol(B, Decl(mappedTypeAliasSubstitutability.ts, 4, 28))
>R : Symbol(R, Decl(mappedTypeAliasSubstitutability.ts, 4, 46))
>x : Symbol(x, Decl(mappedTypeAliasSubstitutability.ts, 4, 50))
>K : Symbol(K, Decl(mappedTypeAliasSubstitutability.ts, 4, 56))
>A : Symbol(A, Decl(mappedTypeAliasSubstitutability.ts, 4, 11))
>Field : Symbol(Field, Decl(mappedTypeAliasSubstitutability.ts, 2, 32))
>B : Symbol(B, Decl(mappedTypeAliasSubstitutability.ts, 4, 28))
>R : Symbol(R, Decl(mappedTypeAliasSubstitutability.ts, 4, 46))
>R : Symbol(R, Decl(mappedTypeAliasSubstitutability.ts, 4, 46))

const g = <A extends string, B extends string, R>(x: Field<A, Field<B, R>>): R => ({} as any);
>g : Symbol(g, Decl(mappedTypeAliasSubstitutability.ts, 5, 5))
>A : Symbol(A, Decl(mappedTypeAliasSubstitutability.ts, 5, 11))
>B : Symbol(B, Decl(mappedTypeAliasSubstitutability.ts, 5, 28))
>R : Symbol(R, Decl(mappedTypeAliasSubstitutability.ts, 5, 46))
>x : Symbol(x, Decl(mappedTypeAliasSubstitutability.ts, 5, 50))
>Field : Symbol(Field, Decl(mappedTypeAliasSubstitutability.ts, 2, 32))
>A : Symbol(A, Decl(mappedTypeAliasSubstitutability.ts, 5, 11))
>Field : Symbol(Field, Decl(mappedTypeAliasSubstitutability.ts, 2, 32))
>B : Symbol(B, Decl(mappedTypeAliasSubstitutability.ts, 5, 28))
>R : Symbol(R, Decl(mappedTypeAliasSubstitutability.ts, 5, 46))
>R : Symbol(R, Decl(mappedTypeAliasSubstitutability.ts, 5, 46))

const r1 = f(v); // number
>r1 : Symbol(r1, Decl(mappedTypeAliasSubstitutability.ts, 6, 5))
>f : Symbol(f, Decl(mappedTypeAliasSubstitutability.ts, 4, 5))
>v : Symbol(v, Decl(mappedTypeAliasSubstitutability.ts, 2, 5))

const r2 = g(v); // unknown
>r2 : Symbol(r2, Decl(mappedTypeAliasSubstitutability.ts, 7, 5))
>g : Symbol(g, Decl(mappedTypeAliasSubstitutability.ts, 5, 5))
>v : Symbol(v, Decl(mappedTypeAliasSubstitutability.ts, 2, 5))

42 changes: 42 additions & 0 deletions tests/baselines/reference/mappedTypeAliasSubstitutability.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
=== tests/cases/compiler/mappedTypeAliasSubstitutability.ts ===
// repro from https://github.com/microsoft/TypeScript/issues/31616

const v = { test: { smth: 5 } };
>v : { test: { smth: number; }; }
>{ test: { smth: 5 } } : { test: { smth: number; }; }
>test : { smth: number; }
>{ smth: 5 } : { smth: number; }
>smth : number
>5 : 5

type Field<A extends string, R> = { [K in A]: R };
>Field : Field<A, R>

const f = <A extends string, B extends string, R>(x: { [K in A]: Field<B, R> } ): R => ({} as any);
>f : <A extends string, B extends string, R>(x: { [K in A]: Field<B, R>; }) => R
><A extends string, B extends string, R>(x: { [K in A]: Field<B, R> } ): R => ({} as any) : <A extends string, B extends string, R>(x: { [K in A]: Field<B, R>; }) => R
>x : { [K in A]: Field<B, R>; }
>({} as any) : any
>{} as any : any
>{} : {}

const g = <A extends string, B extends string, R>(x: Field<A, Field<B, R>>): R => ({} as any);
>g : <A extends string, B extends string, R>(x: Field<A, Field<B, R>>) => R
><A extends string, B extends string, R>(x: Field<A, Field<B, R>>): R => ({} as any) : <A extends string, B extends string, R>(x: Field<A, Field<B, R>>) => R
>x : Field<A, Field<B, R>>
>({} as any) : any
>{} as any : any
>{} : {}

const r1 = f(v); // number
>r1 : number
>f(v) : number
>f : <A extends string, B extends string, R>(x: { [K in A]: Field<B, R>; }) => R
>v : { test: { smth: number; }; }

const r2 = g(v); // unknown
>r2 : number
>g(v) : number
>g : <A extends string, B extends string, R>(x: Field<A, Field<B, R>>) => R
>v : { test: { smth: number; }; }

Loading