mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-05-15 12:51:30 -05:00
Recursive conditional types (#40002)
* Support recursive conditional types * Accept new API baselines * Accept new baselines * Simplify recursive type tracking in type inference * Accept new baselines * Add tests * Accept new baselines * Revise recursion tracking in type inference * Revise tests * Accept new baselines * Add more tests * Accept new baselines
This commit is contained in:
@@ -13810,11 +13810,11 @@ namespace ts {
|
||||
if (!(inferredExtendsType.flags & TypeFlags.AnyOrUnknown) && (checkType.flags & TypeFlags.Any || !isTypeAssignableTo(getPermissiveInstantiation(checkType), getPermissiveInstantiation(inferredExtendsType)))) {
|
||||
// Return union of trueType and falseType for 'any' since it matches anything
|
||||
if (checkType.flags & TypeFlags.Any) {
|
||||
(extraTypes || (extraTypes = [])).push(instantiateTypeWithoutDepthIncrease(root.trueType, combinedMapper || mapper));
|
||||
(extraTypes || (extraTypes = [])).push(instantiateType(getTypeFromTypeNode(root.node.trueType), combinedMapper || mapper));
|
||||
}
|
||||
// If falseType is an immediately nested conditional type that isn't distributive or has an
|
||||
// identical checkType, switch to that type and loop.
|
||||
const falseType = root.falseType;
|
||||
const falseType = getTypeFromTypeNode(root.node.falseType);
|
||||
if (falseType.flags & TypeFlags.Conditional) {
|
||||
const newRoot = (<ConditionalType>falseType).root;
|
||||
if (newRoot.node.parent === root.node && (!newRoot.isDistributive || newRoot.checkType === root.checkType)) {
|
||||
@@ -13822,7 +13822,7 @@ namespace ts {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
result = instantiateTypeWithoutDepthIncrease(falseType, mapper);
|
||||
result = instantiateType(falseType, mapper);
|
||||
break;
|
||||
}
|
||||
// Return trueType for a definitely true extends check. We check instantiations of the two
|
||||
@@ -13831,7 +13831,7 @@ namespace ts {
|
||||
// type Foo<T extends { x: any }> = T extends { x: string } ? string : number
|
||||
// doesn't immediately resolve to 'string' instead of being deferred.
|
||||
if (inferredExtendsType.flags & TypeFlags.AnyOrUnknown || isTypeAssignableTo(getRestrictiveInstantiation(checkType), getRestrictiveInstantiation(inferredExtendsType))) {
|
||||
result = instantiateTypeWithoutDepthIncrease(root.trueType, combinedMapper || mapper);
|
||||
result = instantiateType(getTypeFromTypeNode(root.node.trueType), combinedMapper || mapper);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -13851,15 +13851,15 @@ namespace ts {
|
||||
}
|
||||
|
||||
function getTrueTypeFromConditionalType(type: ConditionalType) {
|
||||
return type.resolvedTrueType || (type.resolvedTrueType = instantiateType(type.root.trueType, type.mapper));
|
||||
return type.resolvedTrueType || (type.resolvedTrueType = instantiateType(getTypeFromTypeNode(type.root.node.trueType), type.mapper));
|
||||
}
|
||||
|
||||
function getFalseTypeFromConditionalType(type: ConditionalType) {
|
||||
return type.resolvedFalseType || (type.resolvedFalseType = instantiateType(type.root.falseType, type.mapper));
|
||||
return type.resolvedFalseType || (type.resolvedFalseType = instantiateType(getTypeFromTypeNode(type.root.node.falseType), type.mapper));
|
||||
}
|
||||
|
||||
function getInferredTrueTypeFromConditionalType(type: ConditionalType) {
|
||||
return type.resolvedInferredTrueType || (type.resolvedInferredTrueType = type.combinedMapper ? instantiateType(type.root.trueType, type.combinedMapper) : getTrueTypeFromConditionalType(type));
|
||||
return type.resolvedInferredTrueType || (type.resolvedInferredTrueType = type.combinedMapper ? instantiateType(getTypeFromTypeNode(type.root.node.trueType), type.combinedMapper) : getTrueTypeFromConditionalType(type));
|
||||
}
|
||||
|
||||
function getInferTypeParameters(node: ConditionalTypeNode): TypeParameter[] | undefined {
|
||||
@@ -13886,8 +13886,6 @@ namespace ts {
|
||||
node,
|
||||
checkType,
|
||||
extendsType: getTypeFromTypeNode(node.extendsType),
|
||||
trueType: getTypeFromTypeNode(node.trueType),
|
||||
falseType: getTypeFromTypeNode(node.falseType),
|
||||
isDistributive: !!(checkType.flags & TypeFlags.TypeParameter),
|
||||
inferTypeParameters: getInferTypeParameters(node),
|
||||
outerTypeParameters,
|
||||
@@ -14878,17 +14876,6 @@ namespace ts {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* This can be used to avoid the penalty on instantiation depth for types which result from immediate
|
||||
* simplification. It essentially removes the depth increase done in `instantiateType`.
|
||||
*/
|
||||
function instantiateTypeWithoutDepthIncrease(type: Type, mapper: TypeMapper | undefined) {
|
||||
instantiationDepth--;
|
||||
const result = instantiateType(type, mapper);
|
||||
instantiationDepth++;
|
||||
return result;
|
||||
}
|
||||
|
||||
function instantiateTypeWorker(type: Type, mapper: TypeMapper): Type {
|
||||
const flags = type.flags;
|
||||
if (flags & TypeFlags.TypeParameter) {
|
||||
@@ -18200,56 +18187,53 @@ namespace ts {
|
||||
// has expanded into `[A<NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<T>>>>>>]`
|
||||
// in such cases we need to terminate the expansion, and we do so here.
|
||||
function isDeeplyNestedType(type: Type, stack: Type[], depth: number): boolean {
|
||||
// We track all object types that have an associated symbol (representing the origin of the type)
|
||||
if (depth >= 5 && type.flags & TypeFlags.Object) {
|
||||
if (!isObjectOrArrayLiteralType(type)) {
|
||||
const symbol = type.symbol;
|
||||
if (symbol) {
|
||||
let count = 0;
|
||||
for (let i = 0; i < depth; i++) {
|
||||
const t = stack[i];
|
||||
if (t.flags & TypeFlags.Object && t.symbol === symbol) {
|
||||
count++;
|
||||
if (count >= 5) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (getObjectFlags(type) && ObjectFlags.Reference && !!(type as TypeReference).node) {
|
||||
const root = (type as TypeReference).target;
|
||||
if (depth >= 5) {
|
||||
const identity = getRecursionIdentity(type);
|
||||
if (identity) {
|
||||
let count = 0;
|
||||
for (let i = 0; i < depth; i++) {
|
||||
const t = stack[i];
|
||||
if (getObjectFlags(t) && ObjectFlags.Reference && !!(t as TypeReference).node && (t as TypeReference).target === root) {
|
||||
if (getRecursionIdentity(stack[i]) === identity) {
|
||||
count++;
|
||||
if (count >= 5) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (depth >= 5 && type.flags & TypeFlags.IndexedAccess) {
|
||||
const root = getRootObjectTypeFromIndexedAccessChain(type);
|
||||
let count = 0;
|
||||
for (let i = 0; i < depth; i++) {
|
||||
const t = stack[i];
|
||||
if (getRootObjectTypeFromIndexedAccessChain(t) === root) {
|
||||
count++;
|
||||
if (count >= 5) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the leftmost object type in a chain of indexed accesses, eg, in A[P][Q], returns A
|
||||
*/
|
||||
function getRootObjectTypeFromIndexedAccessChain(type: Type) {
|
||||
let t = type;
|
||||
while (t.flags & TypeFlags.IndexedAccess) {
|
||||
t = (t as IndexedAccessType).objectType;
|
||||
// Types with constituents that could circularly reference the type have a recursion identity. The recursion
|
||||
// identity is some object that is common to instantiations of the type with the same origin.
|
||||
function getRecursionIdentity(type: Type): object | undefined {
|
||||
if (type.flags & TypeFlags.Object && !isObjectOrArrayLiteralType(type)) {
|
||||
if (getObjectFlags(type) && ObjectFlags.Reference && (type as TypeReference).node) {
|
||||
// Deferred type references are tracked through their associated AST node. This gives us finer
|
||||
// granularity than using their associated target because each manifest type reference has a
|
||||
// unique AST node.
|
||||
return (type as TypeReference).node;
|
||||
}
|
||||
if (type.symbol && !(getObjectFlags(type) & ObjectFlags.Anonymous && type.symbol.flags & SymbolFlags.Class)) {
|
||||
// We track all object types that have an associated symbol (representing the origin of the type), but
|
||||
// exclude the static side of classes from this check since it shares its symbol with the instance side.
|
||||
return type.symbol;
|
||||
}
|
||||
if (isTupleType(type)) {
|
||||
// Tuple types are tracked through their target type
|
||||
return type.target;
|
||||
}
|
||||
}
|
||||
return t;
|
||||
if (type.flags & TypeFlags.IndexedAccess) {
|
||||
// Identity is the leftmost object type in a chain of indexed accesses, eg, in A[P][Q] it is A
|
||||
do {
|
||||
type = (type as IndexedAccessType).objectType;
|
||||
} while (type.flags & TypeFlags.IndexedAccess);
|
||||
return type;
|
||||
}
|
||||
if (type.flags & TypeFlags.Conditional) {
|
||||
// The root object represents the origin of the conditional type
|
||||
return (type as ConditionalType).root;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isPropertyIdenticalTo(sourceProp: Symbol, targetProp: Symbol): boolean {
|
||||
@@ -19154,9 +19138,7 @@ namespace ts {
|
||||
function isTypeParameterAtTopLevel(type: Type, typeParameter: TypeParameter): boolean {
|
||||
return !!(type === typeParameter ||
|
||||
type.flags & TypeFlags.UnionOrIntersection && some((<UnionOrIntersectionType>type).types, t => isTypeParameterAtTopLevel(t, typeParameter)) ||
|
||||
type.flags & TypeFlags.Conditional && (
|
||||
isTypeParameterAtTopLevel(getTrueTypeFromConditionalType(<ConditionalType>type), typeParameter) ||
|
||||
isTypeParameterAtTopLevel(getFalseTypeFromConditionalType(<ConditionalType>type), typeParameter)));
|
||||
type.flags & TypeFlags.Conditional && (getTrueTypeFromConditionalType(<ConditionalType>type) === typeParameter || getFalseTypeFromConditionalType(<ConditionalType>type) === typeParameter));
|
||||
}
|
||||
|
||||
/** Create an object with properties named in the string literal type. Every property has type `any` */
|
||||
@@ -19306,14 +19288,14 @@ namespace ts {
|
||||
}
|
||||
|
||||
function inferTypes(inferences: InferenceInfo[], originalSource: Type, originalTarget: Type, priority: InferencePriority = 0, contravariant = false) {
|
||||
let symbolOrTypeStack: (Symbol | Type)[];
|
||||
let visited: ESMap<string, number>;
|
||||
let bivariant = false;
|
||||
let propagationType: Type;
|
||||
let inferencePriority = InferencePriority.MaxValue;
|
||||
let allowComplexConstraintInference = true;
|
||||
let objectTypeComparisonDepth = 0;
|
||||
const targetStack: Type[] = [];
|
||||
let visited: ESMap<string, number>;
|
||||
let sourceStack: object[];
|
||||
let targetStack: object[];
|
||||
let expandingFlags = ExpandingFlags.None;
|
||||
inferFromTypes(originalSource, originalTarget);
|
||||
|
||||
function inferFromTypes(source: Type, target: Type): void {
|
||||
@@ -19472,18 +19454,8 @@ namespace ts {
|
||||
inferFromTypes((<IndexedAccessType>source).objectType, (<IndexedAccessType>target).objectType);
|
||||
inferFromTypes((<IndexedAccessType>source).indexType, (<IndexedAccessType>target).indexType);
|
||||
}
|
||||
else if (source.flags & TypeFlags.Conditional && target.flags & TypeFlags.Conditional) {
|
||||
inferFromTypes((<ConditionalType>source).checkType, (<ConditionalType>target).checkType);
|
||||
inferFromTypes((<ConditionalType>source).extendsType, (<ConditionalType>target).extendsType);
|
||||
inferFromTypes(getTrueTypeFromConditionalType(<ConditionalType>source), getTrueTypeFromConditionalType(<ConditionalType>target));
|
||||
inferFromTypes(getFalseTypeFromConditionalType(<ConditionalType>source), getFalseTypeFromConditionalType(<ConditionalType>target));
|
||||
}
|
||||
else if (target.flags & TypeFlags.Conditional) {
|
||||
const savePriority = priority;
|
||||
priority |= contravariant ? InferencePriority.ContravariantConditional : 0;
|
||||
const targetTypes = [getTrueTypeFromConditionalType(<ConditionalType>target), getFalseTypeFromConditionalType(<ConditionalType>target)];
|
||||
inferToMultipleTypes(source, targetTypes, target.flags);
|
||||
priority = savePriority;
|
||||
invokeOnce(source, target, inferToConditionalType);
|
||||
}
|
||||
else if (target.flags & TypeFlags.UnionOrIntersection) {
|
||||
inferToMultipleTypes(source, (<UnionOrIntersectionType>target).types, target.flags);
|
||||
@@ -19544,7 +19516,24 @@ namespace ts {
|
||||
(visited || (visited = new Map<string, number>())).set(key, InferencePriority.Circularity);
|
||||
const saveInferencePriority = inferencePriority;
|
||||
inferencePriority = InferencePriority.MaxValue;
|
||||
action(source, target);
|
||||
// We stop inferring and report a circularity if we encounter duplicate recursion identities on both
|
||||
// the source side and the target side.
|
||||
const saveExpandingFlags = expandingFlags;
|
||||
const sourceIdentity = getRecursionIdentity(source);
|
||||
const targetIdentity = getRecursionIdentity(target);
|
||||
if (sourceIdentity && contains(sourceStack, sourceIdentity)) expandingFlags |= ExpandingFlags.Source;
|
||||
if (targetIdentity && contains(targetStack, targetIdentity)) expandingFlags |= ExpandingFlags.Target;
|
||||
if (expandingFlags !== ExpandingFlags.Both) {
|
||||
if (sourceIdentity) (sourceStack || (sourceStack = [])).push(sourceIdentity);
|
||||
if (targetIdentity) (targetStack || (targetStack = [])).push(targetIdentity);
|
||||
action(source, target);
|
||||
if (targetIdentity) targetStack.pop();
|
||||
if (sourceIdentity) sourceStack.pop();
|
||||
}
|
||||
else {
|
||||
inferencePriority = InferencePriority.Circularity;
|
||||
}
|
||||
expandingFlags = saveExpandingFlags;
|
||||
visited.set(key, inferencePriority);
|
||||
inferencePriority = Math.min(inferencePriority, saveInferencePriority);
|
||||
}
|
||||
@@ -19740,41 +19729,23 @@ namespace ts {
|
||||
return false;
|
||||
}
|
||||
|
||||
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 symbolOrType = getObjectFlags(target) & ObjectFlags.Reference && (target as TypeReference).node ? getNormalizedType(target, /*writing*/ false) : isNonConstructorObject ? isTupleType(target) ? target.target : target.symbol : undefined;
|
||||
if (symbolOrType) {
|
||||
if (contains(symbolOrTypeStack, symbolOrType)) {
|
||||
if (getObjectFlags(target) & ObjectFlags.Reference && (target as TypeReference).node) {
|
||||
// Don't set the circularity flag for re-encountered recursive type references just because we're already exploring them
|
||||
return;
|
||||
}
|
||||
inferencePriority = InferencePriority.Circularity;
|
||||
return;
|
||||
}
|
||||
targetStack[objectTypeComparisonDepth] = target;
|
||||
objectTypeComparisonDepth++;
|
||||
if (isDeeplyNestedType(target, targetStack, objectTypeComparisonDepth)) {
|
||||
inferencePriority = InferencePriority.Circularity;
|
||||
objectTypeComparisonDepth--;
|
||||
return;
|
||||
}
|
||||
(symbolOrTypeStack || (symbolOrTypeStack = [])).push(symbolOrType);
|
||||
inferFromObjectTypesWorker(source, target);
|
||||
symbolOrTypeStack.pop();
|
||||
objectTypeComparisonDepth--;
|
||||
function inferToConditionalType(source: Type, target: ConditionalType) {
|
||||
if (source.flags & TypeFlags.Conditional) {
|
||||
inferFromTypes((<ConditionalType>source).checkType, target.checkType);
|
||||
inferFromTypes((<ConditionalType>source).extendsType, target.extendsType);
|
||||
inferFromTypes(getTrueTypeFromConditionalType(<ConditionalType>source), getTrueTypeFromConditionalType(target));
|
||||
inferFromTypes(getFalseTypeFromConditionalType(<ConditionalType>source), getFalseTypeFromConditionalType(target));
|
||||
}
|
||||
else {
|
||||
inferFromObjectTypesWorker(source, target);
|
||||
const savePriority = priority;
|
||||
priority |= contravariant ? InferencePriority.ContravariantConditional : 0;
|
||||
const targetTypes = [getTrueTypeFromConditionalType(target), getFalseTypeFromConditionalType(target)];
|
||||
inferToMultipleTypes(source, targetTypes, target.flags);
|
||||
priority = savePriority;
|
||||
}
|
||||
}
|
||||
|
||||
function inferFromObjectTypesWorker(source: Type, target: Type) {
|
||||
function inferFromObjectTypes(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
|
||||
|
||||
@@ -5290,8 +5290,6 @@ namespace ts {
|
||||
node: ConditionalTypeNode;
|
||||
checkType: Type;
|
||||
extendsType: Type;
|
||||
trueType: Type;
|
||||
falseType: Type;
|
||||
isDistributive: boolean;
|
||||
inferTypeParameters?: TypeParameter[];
|
||||
outerTypeParameters?: TypeParameter[];
|
||||
|
||||
Reference in New Issue
Block a user