Skip to content

Commit dc415c5

Browse files
committed
Infer between closely matching types in unions and intersections
1 parent e8966ce commit dc415c5

File tree

1 file changed

+55
-59
lines changed

1 file changed

+55
-59
lines changed

src/compiler/checker.ts

Lines changed: 55 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -15497,37 +15497,27 @@ namespace ts {
1549715497
}
1549815498
return;
1549915499
}
15500-
// Find each source constituent type that has an identically matching target constituent
15501-
// type, and for each such type infer from the type to itself. When inferring from a
15502-
// type to itself we effectively find all type parameter occurrences within that type
15503-
// and infer themselves as their type arguments. We have special handling for numeric
15504-
// and string literals because the number and string types are not represented as unions
15505-
// of all their possible values.
15506-
let matchingTypes: Type[] | undefined;
15507-
for (const t of (<UnionOrIntersectionType>source).types) {
15508-
const matched = findMatchedType(t, <UnionOrIntersectionType>target);
15509-
if (matched) {
15510-
(matchingTypes || (matchingTypes = [])).push(matched);
15511-
inferFromTypes(matched, matched);
15512-
}
15513-
}
15514-
// Next, to improve the quality of inferences, reduce the source and target types by
15515-
// removing the identically matched constituents. For example, when inferring from
15516-
// 'string | string[]' to 'string | T' we reduce the types to 'string[]' and 'T'.
15517-
if (matchingTypes) {
15518-
const s = removeTypesFromUnionOrIntersection(<UnionOrIntersectionType>source, matchingTypes);
15519-
const t = removeTypesFromUnionOrIntersection(<UnionOrIntersectionType>target, matchingTypes);
15520-
if (!(s && t)) return;
15521-
source = s;
15522-
target = t;
15500+
// First, infer between exactly matching source and target constituents and remove
15501+
// the matching types. Types exactly match when they are identical or, in union
15502+
// types, when the source is a literal and the target is the corresponding primitive.
15503+
const matching = target.flags & TypeFlags.Union ? isTypeOrBaseExactlyMatchedBy : isTypeExactlyMatchedBy;
15504+
const [tempSources, tempTargets] = inferFromMatchingTypes((<UnionOrIntersectionType>source).types, (<UnionOrIntersectionType>target).types, matching);
15505+
// Next, infer between closely matching source and target constituents and remove
15506+
// the matching types. Types closely match when they are instantiations of the same
15507+
// object type or instantiations of the same type alias.
15508+
const [sources, targets] = inferFromMatchingTypes(tempSources, tempTargets, isTypeCloselyMatchedBy);
15509+
if (sources.length === 0 || targets.length === 0) {
15510+
return;
1552315511
}
15512+
source = source.flags & TypeFlags.Union ? getUnionType(sources) : getIntersectionType(sources);
15513+
target = target.flags & TypeFlags.Union ? getUnionType(targets) : getIntersectionType(targets);
1552415514
}
1552515515
else if (target.flags & TypeFlags.Union && !(target.flags & TypeFlags.EnumLiteral) || target.flags & TypeFlags.Intersection) {
15526-
const matched = findMatchedType(source, <UnionOrIntersectionType>target);
15527-
if (matched) {
15528-
inferFromTypes(matched, matched);
15529-
return;
15530-
}
15516+
// This block of code is an optimized version of the block above for the simpler case
15517+
// of a singleton source type.
15518+
const matching = target.flags & TypeFlags.Union ? isTypeOrBaseExactlyMatchedBy : isTypeExactlyMatchedBy;
15519+
if (inferFromMatchingType(source, (<UnionOrIntersectionType>target).types, matching)) return;
15520+
if (inferFromMatchingType(source, (<UnionOrIntersectionType>target).types, isTypeCloselyMatchedBy)) return;
1553115521
}
1553215522
else if (target.flags & (TypeFlags.IndexedAccess | TypeFlags.Substitution)) {
1553315523
target = getActualTypeVariable(target);
@@ -15675,6 +15665,35 @@ namespace ts {
1567515665
visited.set(key, inferenceCount - startCount);
1567615666
}
1567715667

15668+
function inferFromMatchingType(source: Type, targets: Type[], matches: (s: Type, t: Type) => boolean) {
15669+
let matched = false;
15670+
for (const t of targets) {
15671+
if (matches(source, t)) {
15672+
inferFromTypes(source, t);
15673+
matched = true;
15674+
}
15675+
}
15676+
return matched;
15677+
}
15678+
15679+
function inferFromMatchingTypes(sources: Type[], targets: Type[], matches: (s: Type, t: Type) => boolean): [Type[], Type[]] {
15680+
let matchedSources: Type[] | undefined;
15681+
let matchedTargets: Type[] | undefined;
15682+
for (const t of targets) {
15683+
for (const s of sources) {
15684+
if (matches(s, t)) {
15685+
inferFromTypes(s, t);
15686+
matchedSources = appendIfUnique(matchedSources, s);
15687+
matchedTargets = appendIfUnique(matchedTargets, t);
15688+
}
15689+
}
15690+
}
15691+
return [
15692+
matchedSources ? filter(sources, t => !contains(matchedSources, t)) : sources,
15693+
matchedTargets ? filter(targets, t => !contains(matchedTargets, t)) : targets,
15694+
];
15695+
}
15696+
1567815697
function inferFromTypeArguments(sourceTypes: readonly Type[], targetTypes: readonly Type[], variances: readonly VarianceFlags[]) {
1567915698
const count = sourceTypes.length < targetTypes.length ? sourceTypes.length : targetTypes.length;
1568015699
for (let i = 0; i < count; i++) {
@@ -15955,47 +15974,24 @@ namespace ts {
1595515974
}
1595615975
}
1595715976

15958-
function isMatchableType(type: Type) {
15977+
function isNonObjectOrAnonymousType(type: Type) {
1595915978
// We exclude non-anonymous object types because some frameworks (e.g. Ember) rely on the ability to
1596015979
// infer between types that don't witness their type variables. Such types would otherwise be eliminated
1596115980
// because they appear identical.
1596215981
return !(type.flags & TypeFlags.Object) || !!(getObjectFlags(type) & ObjectFlags.Anonymous);
1596315982
}
1596415983

15965-
function typeMatchedBySomeType(type: Type, types: Type[]): boolean {
15966-
for (const t of types) {
15967-
if (t === type || isMatchableType(t) && isMatchableType(type) && isTypeIdenticalTo(t, type)) {
15968-
return true;
15969-
}
15970-
}
15971-
return false;
15984+
function isTypeExactlyMatchedBy(s: Type, t: Type) {
15985+
return s === t || isNonObjectOrAnonymousType(s) && isNonObjectOrAnonymousType(t) && isTypeIdenticalTo(s, t);
1597215986
}
1597315987

15974-
function findMatchedType(type: Type, target: UnionOrIntersectionType) {
15975-
if (typeMatchedBySomeType(type, target.types)) {
15976-
return type;
15977-
}
15978-
if (type.flags & (TypeFlags.NumberLiteral | TypeFlags.StringLiteral) && target.flags & TypeFlags.Union) {
15979-
const base = getBaseTypeOfLiteralType(type);
15980-
if (typeMatchedBySomeType(base, target.types)) {
15981-
return base;
15982-
}
15983-
}
15984-
return undefined;
15988+
function isTypeOrBaseExactlyMatchedBy(s: Type, t: Type) {
15989+
return isTypeExactlyMatchedBy(s, t) || !!(s.flags & (TypeFlags.StringLiteral | TypeFlags.NumberLiteral)) && isTypeIdenticalTo(getBaseTypeOfLiteralType(s), t);
1598515990
}
1598615991

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

1600115997
function hasPrimitiveConstraint(type: TypeParameter): boolean {

0 commit comments

Comments
 (0)