Skip to content

Commit

Permalink
Merge pull request #32558 from microsoft/fix32247
Browse files Browse the repository at this point in the history
Infer between closely matching types in unions and intersections
  • Loading branch information
ahejlsberg committed Jul 29, 2019
2 parents b963e1a + a9e0a77 commit 4cc6618
Show file tree
Hide file tree
Showing 7 changed files with 527 additions and 119 deletions.
181 changes: 94 additions & 87 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15308,7 +15308,7 @@ namespace ts {
objectFlags & ObjectFlags.Reference && forEach((<TypeReference>type).typeArguments, couldContainTypeVariables) ||
objectFlags & ObjectFlags.Anonymous && type.symbol && type.symbol.flags & (SymbolFlags.Function | SymbolFlags.Method | SymbolFlags.Class | SymbolFlags.TypeLiteral | SymbolFlags.ObjectLiteral) && type.symbol.declarations ||
objectFlags & ObjectFlags.Mapped ||
type.flags & TypeFlags.UnionOrIntersection && couldUnionOrIntersectionContainTypeVariables(<UnionOrIntersectionType>type));
type.flags & TypeFlags.UnionOrIntersection && !(type.flags & TypeFlags.EnumLiteral) && couldUnionOrIntersectionContainTypeVariables(<UnionOrIntersectionType>type));
}

function couldUnionOrIntersectionContainTypeVariables(type: UnionOrIntersectionType): boolean {
Expand Down Expand Up @@ -15460,7 +15460,7 @@ namespace ts {
let visited: Map<number>;
let bivariant = false;
let propagationType: Type;
let inferenceCount = 0;
let inferenceMatch = false;
let inferenceIncomplete = false;
let allowComplexConstraintInference = true;
inferFromTypes(originalSource, originalTarget);
Expand All @@ -15485,46 +15485,50 @@ namespace ts {
inferFromTypeArguments(source.aliasTypeArguments, target.aliasTypeArguments!, getAliasVariances(source.aliasSymbol));
return;
}
if (source.flags & TypeFlags.Union && target.flags & TypeFlags.Union && !(source.flags & TypeFlags.EnumLiteral && target.flags & TypeFlags.EnumLiteral) ||
source.flags & TypeFlags.Intersection && target.flags & TypeFlags.Intersection) {
// Source and target are both unions or both intersections. If source and target
// are the same type, just relate each constituent type to itself.
if (source === target) {
for (const t of (<UnionOrIntersectionType>source).types) {
inferFromTypes(t, t);
if (source === target && source.flags & TypeFlags.UnionOrIntersection) {
// When source and target are the same union or intersection type, just relate each constituent
// type to itself.
for (const t of (<UnionOrIntersectionType>source).types) {
inferFromTypes(t, t);
}
return;
}
if (target.flags & TypeFlags.Union) {
if (source.flags & TypeFlags.Union) {
// First, infer between identically matching source and target constituents and remove the
// matching types.
const [tempSources, tempTargets] = inferFromMatchingTypes((<UnionType>source).types, (<UnionType>target).types, isTypeOrBaseIdenticalTo);
// Next, infer between closely matching source and target constituents and remove
// the matching types. Types closely match when they are instantiations of the same
// object type or instantiations of the same type alias.
const [sources, targets] = inferFromMatchingTypes(tempSources, tempTargets, isTypeCloselyMatchedBy);
if (sources.length === 0 || targets.length === 0) {
return;
}
return;
source = getUnionType(sources);
target = getUnionType(targets);
}
// Find each source constituent type that has an identically matching target constituent
// type, and for each such type infer from the type to itself. When inferring from a
// type to itself we effectively find all type parameter occurrences within that type
// and infer themselves as their type arguments. We have special handling for numeric
// and string literals because the number and string types are not represented as unions
// of all their possible values.
let matchingTypes: Type[] | undefined;
for (const t of (<UnionOrIntersectionType>source).types) {
const matched = findMatchedType(t, <UnionOrIntersectionType>target);
if (matched) {
(matchingTypes || (matchingTypes = [])).push(matched);
inferFromTypes(matched, matched);
}
}
// Next, to improve the quality of inferences, reduce the source and target types by
// removing the identically matched constituents. For example, when inferring from
// 'string | string[]' to 'string | T' we reduce the types to 'string[]' and 'T'.
if (matchingTypes) {
const s = removeTypesFromUnionOrIntersection(<UnionOrIntersectionType>source, matchingTypes);
const t = removeTypesFromUnionOrIntersection(<UnionOrIntersectionType>target, matchingTypes);
if (!(s && t)) return;
source = s;
target = t;
}
}
else if (target.flags & TypeFlags.Union && !(target.flags & TypeFlags.EnumLiteral) || target.flags & TypeFlags.Intersection) {
const matched = findMatchedType(source, <UnionOrIntersectionType>target);
if (matched) {
inferFromTypes(matched, matched);
return;
else {
if (inferFromMatchingType(source, (<UnionType>target).types, isTypeOrBaseIdenticalTo)) return;
if (inferFromMatchingType(source, (<UnionType>target).types, isTypeCloselyMatchedBy)) return;
}
}
else if (target.flags & TypeFlags.Intersection && some((<IntersectionType>target).types, t => !!getInferenceInfoForType(t))) {
// We reduce intersection types only when they contain naked type parameters. For example, when
// inferring from 'string[] & { extra: any }' to 'string[] & T' we want to remove string[] and
// infer { extra: any } for T. But when inferring to 'string[] & Iterable<T>' we want to keep the
// string[] on the source side and infer string for T.
if (source.flags & TypeFlags.Intersection) {
// Infer between identically matching source and target constituents and remove the matching types.
const [sources, targets] = inferFromMatchingTypes((<IntersectionType>source).types, (<IntersectionType>target).types, isTypeIdenticalTo);
if (sources.length === 0 || targets.length === 0) {
return;
}
source = getIntersectionType(sources);
target = getIntersectionType(targets);
}
else if (!(source.flags & TypeFlags.Union)) {
if (inferFromMatchingType(source, (<IntersectionType>target).types, isTypeIdenticalTo)) return;
}
}
else if (target.flags & (TypeFlags.IndexedAccess | TypeFlags.Substitution)) {
Expand Down Expand Up @@ -15570,7 +15574,7 @@ namespace ts {
clearCachedInferences(inferences);
}
}
inferenceCount++;
inferenceMatch = true;
return;
}
else {
Expand Down Expand Up @@ -15662,15 +15666,50 @@ namespace ts {

function invokeOnce(source: Type, target: Type, action: (source: Type, target: Type) => void) {
const key = source.id + "," + target.id;
const count = visited && visited.get(key);
if (count !== undefined) {
inferenceCount += count;
const status = visited && visited.get(key);
if (status !== undefined) {
if (status & 1) inferenceMatch = true;
if (status & 2) inferenceIncomplete = true;
return;
}
(visited || (visited = createMap<number>())).set(key, 0);
const startCount = inferenceCount;
const saveInferenceMatch = inferenceMatch;
const saveInferenceIncomplete = inferenceIncomplete;
inferenceMatch = false;
inferenceIncomplete = false;
action(source, target);
visited.set(key, inferenceCount - startCount);
visited.set(key, (inferenceMatch ? 1 : 0) | (inferenceIncomplete ? 2 : 0));
inferenceMatch = inferenceMatch || saveInferenceMatch;
inferenceIncomplete = inferenceIncomplete || saveInferenceIncomplete;
}

function inferFromMatchingType(source: Type, targets: Type[], matches: (s: Type, t: Type) => boolean) {
let matched = false;
for (const t of targets) {
if (matches(source, t)) {
inferFromTypes(source, t);
matched = true;
}
}
return matched;
}

function inferFromMatchingTypes(sources: Type[], targets: Type[], matches: (s: Type, t: Type) => boolean): [Type[], Type[]] {
let matchedSources: Type[] | undefined;
let matchedTargets: Type[] | undefined;
for (const t of targets) {
for (const s of sources) {
if (matches(s, t)) {
inferFromTypes(s, t);
matchedSources = appendIfUnique(matchedSources, s);
matchedTargets = appendIfUnique(matchedTargets, t);
}
}
}
return [
matchedSources ? filter(sources, t => !contains(matchedSources, t)) : sources,
matchedTargets ? filter(targets, t => !contains(matchedTargets, t)) : targets,
];
}

function inferFromTypeArguments(sourceTypes: readonly Type[], targetTypes: readonly Type[], variances: readonly VarianceFlags[]) {
Expand Down Expand Up @@ -15724,9 +15763,11 @@ namespace ts {
}
else {
for (let i = 0; i < sources.length; i++) {
const count = inferenceCount;
const saveInferenceMatch = inferenceMatch;
inferenceMatch = false;
inferFromTypes(sources[i], t);
if (count !== inferenceCount) matched[i] = true;
if (inferenceMatch) matched[i] = true;
inferenceMatch = inferenceMatch || saveInferenceMatch;
}
}
}
Expand Down Expand Up @@ -15953,47 +15994,13 @@ namespace ts {
}
}

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

function typeMatchedBySomeType(type: Type, types: Type[]): boolean {
for (const t of types) {
if (t === type || isMatchableType(t) && isMatchableType(type) && isTypeIdenticalTo(t, type)) {
return true;
}
}
return false;
}

function findMatchedType(type: Type, target: UnionOrIntersectionType) {
if (typeMatchedBySomeType(type, target.types)) {
return type;
}
if (type.flags & (TypeFlags.NumberLiteral | TypeFlags.StringLiteral) && target.flags & TypeFlags.Union) {
const base = getBaseTypeOfLiteralType(type);
if (typeMatchedBySomeType(base, target.types)) {
return base;
}
}
return undefined;
function isTypeOrBaseIdenticalTo(s: Type, t: Type) {
return isTypeIdenticalTo(s, t) || !!(s.flags & (TypeFlags.StringLiteral | TypeFlags.NumberLiteral)) && isTypeIdenticalTo(getBaseTypeOfLiteralType(s), t);
}

/**
* Return a new union or intersection type computed by removing a given set of types
* from a given union or intersection type.
*/
function removeTypesFromUnionOrIntersection(type: UnionOrIntersectionType, typesToRemove: Type[]) {
const reducedTypes: Type[] = [];
for (const t of type.types) {
if (!typeMatchedBySomeType(t, typesToRemove)) {
reducedTypes.push(t);
}
}
return reducedTypes.length ? type.flags & TypeFlags.Union ? getUnionType(reducedTypes) : getIntersectionType(reducedTypes) : undefined;
function isTypeCloselyMatchedBy(s: Type, t: Type) {
return !!(s.flags & TypeFlags.Object && t.flags & TypeFlags.Object && s.symbol && s.symbol === t.symbol ||
s.aliasSymbol && s.aliasTypeArguments && s.aliasSymbol === t.aliasSymbol);
}

function hasPrimitiveConstraint(type: TypeParameter): boolean {
Expand Down

0 comments on commit 4cc6618

Please sign in to comment.