Skip to content
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

Infer between closely matching types in unions and intersections #32558

Merged
merged 10 commits into from
Jul 29, 2019
181 changes: 94 additions & 87 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15310,7 +15310,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 @@ -15462,7 +15462,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 @@ -15487,46 +15487,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 @@ -15572,7 +15576,7 @@ namespace ts {
clearCachedInferences(inferences);
}
}
inferenceCount++;
inferenceMatch = true;
return;
}
else {
Expand Down Expand Up @@ -15664,15 +15668,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 @@ -15726,9 +15765,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 @@ -15955,47 +15996,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