Skip to content

Commit 4cc6618

Browse files
authored
Merge pull request #32558 from microsoft/fix32247
Infer between closely matching types in unions and intersections
2 parents b963e1a + a9e0a77 commit 4cc6618

7 files changed

+527
-119
lines changed

src/compiler/checker.ts

Lines changed: 94 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -15308,7 +15308,7 @@ namespace ts {
1530815308
objectFlags & ObjectFlags.Reference && forEach((<TypeReference>type).typeArguments, couldContainTypeVariables) ||
1530915309
objectFlags & ObjectFlags.Anonymous && type.symbol && type.symbol.flags & (SymbolFlags.Function | SymbolFlags.Method | SymbolFlags.Class | SymbolFlags.TypeLiteral | SymbolFlags.ObjectLiteral) && type.symbol.declarations ||
1531015310
objectFlags & ObjectFlags.Mapped ||
15311-
type.flags & TypeFlags.UnionOrIntersection && couldUnionOrIntersectionContainTypeVariables(<UnionOrIntersectionType>type));
15311+
type.flags & TypeFlags.UnionOrIntersection && !(type.flags & TypeFlags.EnumLiteral) && couldUnionOrIntersectionContainTypeVariables(<UnionOrIntersectionType>type));
1531215312
}
1531315313

1531415314
function couldUnionOrIntersectionContainTypeVariables(type: UnionOrIntersectionType): boolean {
@@ -15460,7 +15460,7 @@ namespace ts {
1546015460
let visited: Map<number>;
1546115461
let bivariant = false;
1546215462
let propagationType: Type;
15463-
let inferenceCount = 0;
15463+
let inferenceMatch = false;
1546415464
let inferenceIncomplete = false;
1546515465
let allowComplexConstraintInference = true;
1546615466
inferFromTypes(originalSource, originalTarget);
@@ -15485,46 +15485,50 @@ namespace ts {
1548515485
inferFromTypeArguments(source.aliasTypeArguments, target.aliasTypeArguments!, getAliasVariances(source.aliasSymbol));
1548615486
return;
1548715487
}
15488-
if (source.flags & TypeFlags.Union && target.flags & TypeFlags.Union && !(source.flags & TypeFlags.EnumLiteral && target.flags & TypeFlags.EnumLiteral) ||
15489-
source.flags & TypeFlags.Intersection && target.flags & TypeFlags.Intersection) {
15490-
// Source and target are both unions or both intersections. If source and target
15491-
// are the same type, just relate each constituent type to itself.
15492-
if (source === target) {
15493-
for (const t of (<UnionOrIntersectionType>source).types) {
15494-
inferFromTypes(t, t);
15488+
if (source === target && source.flags & TypeFlags.UnionOrIntersection) {
15489+
// When source and target are the same union or intersection type, just relate each constituent
15490+
// type to itself.
15491+
for (const t of (<UnionOrIntersectionType>source).types) {
15492+
inferFromTypes(t, t);
15493+
}
15494+
return;
15495+
}
15496+
if (target.flags & TypeFlags.Union) {
15497+
if (source.flags & TypeFlags.Union) {
15498+
// First, infer between identically matching source and target constituents and remove the
15499+
// matching types.
15500+
const [tempSources, tempTargets] = inferFromMatchingTypes((<UnionType>source).types, (<UnionType>target).types, isTypeOrBaseIdenticalTo);
15501+
// Next, infer between closely matching source and target constituents and remove
15502+
// the matching types. Types closely match when they are instantiations of the same
15503+
// object type or instantiations of the same type alias.
15504+
const [sources, targets] = inferFromMatchingTypes(tempSources, tempTargets, isTypeCloselyMatchedBy);
15505+
if (sources.length === 0 || targets.length === 0) {
15506+
return;
1549515507
}
15496-
return;
15508+
source = getUnionType(sources);
15509+
target = getUnionType(targets);
1549715510
}
15498-
// Find each source constituent type that has an identically matching target constituent
15499-
// type, and for each such type infer from the type to itself. When inferring from a
15500-
// type to itself we effectively find all type parameter occurrences within that type
15501-
// and infer themselves as their type arguments. We have special handling for numeric
15502-
// and string literals because the number and string types are not represented as unions
15503-
// of all their possible values.
15504-
let matchingTypes: Type[] | undefined;
15505-
for (const t of (<UnionOrIntersectionType>source).types) {
15506-
const matched = findMatchedType(t, <UnionOrIntersectionType>target);
15507-
if (matched) {
15508-
(matchingTypes || (matchingTypes = [])).push(matched);
15509-
inferFromTypes(matched, matched);
15510-
}
15511-
}
15512-
// Next, to improve the quality of inferences, reduce the source and target types by
15513-
// removing the identically matched constituents. For example, when inferring from
15514-
// 'string | string[]' to 'string | T' we reduce the types to 'string[]' and 'T'.
15515-
if (matchingTypes) {
15516-
const s = removeTypesFromUnionOrIntersection(<UnionOrIntersectionType>source, matchingTypes);
15517-
const t = removeTypesFromUnionOrIntersection(<UnionOrIntersectionType>target, matchingTypes);
15518-
if (!(s && t)) return;
15519-
source = s;
15520-
target = t;
15521-
}
15522-
}
15523-
else if (target.flags & TypeFlags.Union && !(target.flags & TypeFlags.EnumLiteral) || target.flags & TypeFlags.Intersection) {
15524-
const matched = findMatchedType(source, <UnionOrIntersectionType>target);
15525-
if (matched) {
15526-
inferFromTypes(matched, matched);
15527-
return;
15511+
else {
15512+
if (inferFromMatchingType(source, (<UnionType>target).types, isTypeOrBaseIdenticalTo)) return;
15513+
if (inferFromMatchingType(source, (<UnionType>target).types, isTypeCloselyMatchedBy)) return;
15514+
}
15515+
}
15516+
else if (target.flags & TypeFlags.Intersection && some((<IntersectionType>target).types, t => !!getInferenceInfoForType(t))) {
15517+
// We reduce intersection types only when they contain naked type parameters. For example, when
15518+
// inferring from 'string[] & { extra: any }' to 'string[] & T' we want to remove string[] and
15519+
// infer { extra: any } for T. But when inferring to 'string[] & Iterable<T>' we want to keep the
15520+
// string[] on the source side and infer string for T.
15521+
if (source.flags & TypeFlags.Intersection) {
15522+
// Infer between identically matching source and target constituents and remove the matching types.
15523+
const [sources, targets] = inferFromMatchingTypes((<IntersectionType>source).types, (<IntersectionType>target).types, isTypeIdenticalTo);
15524+
if (sources.length === 0 || targets.length === 0) {
15525+
return;
15526+
}
15527+
source = getIntersectionType(sources);
15528+
target = getIntersectionType(targets);
15529+
}
15530+
else if (!(source.flags & TypeFlags.Union)) {
15531+
if (inferFromMatchingType(source, (<IntersectionType>target).types, isTypeIdenticalTo)) return;
1552815532
}
1552915533
}
1553015534
else if (target.flags & (TypeFlags.IndexedAccess | TypeFlags.Substitution)) {
@@ -15570,7 +15574,7 @@ namespace ts {
1557015574
clearCachedInferences(inferences);
1557115575
}
1557215576
}
15573-
inferenceCount++;
15577+
inferenceMatch = true;
1557415578
return;
1557515579
}
1557615580
else {
@@ -15662,15 +15666,50 @@ namespace ts {
1566215666

1566315667
function invokeOnce(source: Type, target: Type, action: (source: Type, target: Type) => void) {
1566415668
const key = source.id + "," + target.id;
15665-
const count = visited && visited.get(key);
15666-
if (count !== undefined) {
15667-
inferenceCount += count;
15669+
const status = visited && visited.get(key);
15670+
if (status !== undefined) {
15671+
if (status & 1) inferenceMatch = true;
15672+
if (status & 2) inferenceIncomplete = true;
1566815673
return;
1566915674
}
1567015675
(visited || (visited = createMap<number>())).set(key, 0);
15671-
const startCount = inferenceCount;
15676+
const saveInferenceMatch = inferenceMatch;
15677+
const saveInferenceIncomplete = inferenceIncomplete;
15678+
inferenceMatch = false;
15679+
inferenceIncomplete = false;
1567215680
action(source, target);
15673-
visited.set(key, inferenceCount - startCount);
15681+
visited.set(key, (inferenceMatch ? 1 : 0) | (inferenceIncomplete ? 2 : 0));
15682+
inferenceMatch = inferenceMatch || saveInferenceMatch;
15683+
inferenceIncomplete = inferenceIncomplete || saveInferenceIncomplete;
15684+
}
15685+
15686+
function inferFromMatchingType(source: Type, targets: Type[], matches: (s: Type, t: Type) => boolean) {
15687+
let matched = false;
15688+
for (const t of targets) {
15689+
if (matches(source, t)) {
15690+
inferFromTypes(source, t);
15691+
matched = true;
15692+
}
15693+
}
15694+
return matched;
15695+
}
15696+
15697+
function inferFromMatchingTypes(sources: Type[], targets: Type[], matches: (s: Type, t: Type) => boolean): [Type[], Type[]] {
15698+
let matchedSources: Type[] | undefined;
15699+
let matchedTargets: Type[] | undefined;
15700+
for (const t of targets) {
15701+
for (const s of sources) {
15702+
if (matches(s, t)) {
15703+
inferFromTypes(s, t);
15704+
matchedSources = appendIfUnique(matchedSources, s);
15705+
matchedTargets = appendIfUnique(matchedTargets, t);
15706+
}
15707+
}
15708+
}
15709+
return [
15710+
matchedSources ? filter(sources, t => !contains(matchedSources, t)) : sources,
15711+
matchedTargets ? filter(targets, t => !contains(matchedTargets, t)) : targets,
15712+
];
1567415713
}
1567515714

1567615715
function inferFromTypeArguments(sourceTypes: readonly Type[], targetTypes: readonly Type[], variances: readonly VarianceFlags[]) {
@@ -15724,9 +15763,11 @@ namespace ts {
1572415763
}
1572515764
else {
1572615765
for (let i = 0; i < sources.length; i++) {
15727-
const count = inferenceCount;
15766+
const saveInferenceMatch = inferenceMatch;
15767+
inferenceMatch = false;
1572815768
inferFromTypes(sources[i], t);
15729-
if (count !== inferenceCount) matched[i] = true;
15769+
if (inferenceMatch) matched[i] = true;
15770+
inferenceMatch = inferenceMatch || saveInferenceMatch;
1573015771
}
1573115772
}
1573215773
}
@@ -15953,47 +15994,13 @@ namespace ts {
1595315994
}
1595415995
}
1595515996

15956-
function isMatchableType(type: Type) {
15957-
// We exclude non-anonymous object types because some frameworks (e.g. Ember) rely on the ability to
15958-
// infer between types that don't witness their type variables. Such types would otherwise be eliminated
15959-
// because they appear identical.
15960-
return !(type.flags & TypeFlags.Object) || !!(getObjectFlags(type) & ObjectFlags.Anonymous);
15961-
}
15962-
15963-
function typeMatchedBySomeType(type: Type, types: Type[]): boolean {
15964-
for (const t of types) {
15965-
if (t === type || isMatchableType(t) && isMatchableType(type) && isTypeIdenticalTo(t, type)) {
15966-
return true;
15967-
}
15968-
}
15969-
return false;
15970-
}
15971-
15972-
function findMatchedType(type: Type, target: UnionOrIntersectionType) {
15973-
if (typeMatchedBySomeType(type, target.types)) {
15974-
return type;
15975-
}
15976-
if (type.flags & (TypeFlags.NumberLiteral | TypeFlags.StringLiteral) && target.flags & TypeFlags.Union) {
15977-
const base = getBaseTypeOfLiteralType(type);
15978-
if (typeMatchedBySomeType(base, target.types)) {
15979-
return base;
15980-
}
15981-
}
15982-
return undefined;
15997+
function isTypeOrBaseIdenticalTo(s: Type, t: Type) {
15998+
return isTypeIdenticalTo(s, t) || !!(s.flags & (TypeFlags.StringLiteral | TypeFlags.NumberLiteral)) && isTypeIdenticalTo(getBaseTypeOfLiteralType(s), t);
1598315999
}
1598416000

15985-
/**
15986-
* Return a new union or intersection type computed by removing a given set of types
15987-
* from a given union or intersection type.
15988-
*/
15989-
function removeTypesFromUnionOrIntersection(type: UnionOrIntersectionType, typesToRemove: Type[]) {
15990-
const reducedTypes: Type[] = [];
15991-
for (const t of type.types) {
15992-
if (!typeMatchedBySomeType(t, typesToRemove)) {
15993-
reducedTypes.push(t);
15994-
}
15995-
}
15996-
return reducedTypes.length ? type.flags & TypeFlags.Union ? getUnionType(reducedTypes) : getIntersectionType(reducedTypes) : undefined;
16001+
function isTypeCloselyMatchedBy(s: Type, t: Type) {
16002+
return !!(s.flags & TypeFlags.Object && t.flags & TypeFlags.Object && s.symbol && s.symbol === t.symbol ||
16003+
s.aliasSymbol && s.aliasTypeArguments && s.aliasSymbol === t.aliasSymbol);
1599716004
}
1599816005

1599916006
function hasPrimitiveConstraint(type: TypeParameter): boolean {

0 commit comments

Comments
 (0)