mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-05-30 10:29:18 -05:00
Improve soundness of indexed access type relations
This commit is contained in:
@@ -7188,36 +7188,39 @@ namespace ts {
|
||||
// Return the lower bound of the key type in a mapped type. Intuitively, the lower
|
||||
// bound includes those keys that are known to always be present, for example because
|
||||
// because of constraints on type parameters (e.g. 'keyof T' for a constrained T).
|
||||
function getLowerBoundOfKeyType(type: Type): Type {
|
||||
// The isIndexType flag indicates that the type is the index type of an indexed
|
||||
// access that is the target of an assignment.
|
||||
function getLowerBoundOfKeyType(type: Type, isIndexType: boolean): Type {
|
||||
if (type.flags & (TypeFlags.Any | TypeFlags.Primitive)) {
|
||||
return type;
|
||||
}
|
||||
if (type.flags & TypeFlags.Index) {
|
||||
return getIndexType(getApparentType((<IndexType>type).type));
|
||||
const keys = getIndexType(getApparentType((<IndexType>type).type));
|
||||
return isIndexType ? filterType(keys, t => !!(t.flags & TypeFlags.Literal)) : keys;
|
||||
}
|
||||
if (type.flags & TypeFlags.Conditional) {
|
||||
return getLowerBoundOfConditionalType(<ConditionalType>type);
|
||||
if ((<ConditionalType>type).root.isDistributive) {
|
||||
const checkType = (<ConditionalType>type).checkType;
|
||||
const constraint = getLowerBoundOfKeyType(checkType, isIndexType);
|
||||
if (constraint !== checkType) {
|
||||
const mapper = makeUnaryTypeMapper((<ConditionalType>type).root.checkType, constraint);
|
||||
return getConditionalTypeInstantiation(<ConditionalType>type, combineTypeMappers(mapper, (<ConditionalType>type).mapper));
|
||||
}
|
||||
}
|
||||
return type;
|
||||
}
|
||||
if (type.flags & TypeFlags.Union) {
|
||||
return getUnionType(sameMap((<UnionType>type).types, getLowerBoundOfKeyType));
|
||||
return getUnionType(sameMap((<UnionType>type).types, t => getLowerBoundOfKeyType(t, isIndexType)));
|
||||
}
|
||||
if (type.flags & TypeFlags.Intersection) {
|
||||
return getIntersectionType(sameMap((<UnionType>type).types, getLowerBoundOfKeyType));
|
||||
return getIntersectionType(sameMap((<UnionType>type).types, t => getLowerBoundOfKeyType(t, isIndexType)));
|
||||
}
|
||||
if (isIndexType && type.flags & TypeFlags.Instantiable) {
|
||||
return getLowerBoundOfKeyType(getConstraintOfType(type) || neverType, isIndexType);
|
||||
}
|
||||
return neverType;
|
||||
}
|
||||
|
||||
function getLowerBoundOfConditionalType(type: ConditionalType) {
|
||||
if (type.root.isDistributive) {
|
||||
const constraint = getLowerBoundOfKeyType(type.checkType);
|
||||
if (constraint !== type.checkType) {
|
||||
const mapper = makeUnaryTypeMapper(type.root.checkType, constraint);
|
||||
return getConditionalTypeInstantiation(type, combineTypeMappers(mapper, type.mapper));
|
||||
}
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
/** Resolve the members of a mapped type { [P in K]: T } */
|
||||
function resolveMappedTypeMembers(type: MappedType) {
|
||||
const members: SymbolTable = createSymbolTable();
|
||||
@@ -7246,7 +7249,7 @@ namespace ts {
|
||||
}
|
||||
}
|
||||
else {
|
||||
forEachType(getLowerBoundOfKeyType(constraintType), addMemberForKeyType);
|
||||
forEachType(getLowerBoundOfKeyType(constraintType, /*isIndexType*/ false), addMemberForKeyType);
|
||||
}
|
||||
setStructuredTypeMembers(type, members, emptyArray, emptyArray, stringIndexInfo, numberIndexInfo);
|
||||
|
||||
@@ -7523,7 +7526,7 @@ namespace ts {
|
||||
// a union - once negated types exist and are applied to the conditional false branch, this "constraint"
|
||||
// likely doesn't need to exist.
|
||||
if (type.root.isDistributive && type.restrictiveInstantiation !== type) {
|
||||
const simplified = getSimplifiedType(type.checkType);
|
||||
const simplified = getSimplifiedType(type.checkType, /*writing*/ false);
|
||||
const constraint = simplified === type.checkType ? getConstraintOfType(simplified) : simplified;
|
||||
if (constraint && constraint !== type.checkType) {
|
||||
const mapper = makeUnaryTypeMapper(type.root.checkType, constraint);
|
||||
@@ -7630,7 +7633,7 @@ namespace ts {
|
||||
return t.immediateBaseConstraint = noConstraintType;
|
||||
}
|
||||
constraintDepth++;
|
||||
let result = computeBaseConstraint(getSimplifiedType(t));
|
||||
let result = computeBaseConstraint(getSimplifiedType(t, /*writing*/ false));
|
||||
constraintDepth--;
|
||||
if (!popTypeResolution()) {
|
||||
if (t.flags & TypeFlags.TypeParameter) {
|
||||
@@ -10003,53 +10006,68 @@ namespace ts {
|
||||
return maybeTypeOfKind(type, TypeFlags.InstantiableNonPrimitive | TypeFlags.Index);
|
||||
}
|
||||
|
||||
function getSimplifiedType(type: Type): Type {
|
||||
return type.flags & TypeFlags.IndexedAccess ? getSimplifiedIndexedAccessType(<IndexedAccessType>type) : type;
|
||||
function getSimplifiedType(type: Type, writing: boolean): Type {
|
||||
return type.flags & TypeFlags.IndexedAccess ? getSimplifiedIndexedAccessType(<IndexedAccessType>type, writing) : type;
|
||||
}
|
||||
|
||||
function distributeIndexOverObjectType(objectType: Type, indexType: Type) {
|
||||
// (T | U)[K] -> T[K] | U[K]
|
||||
if (objectType.flags & TypeFlags.Union) {
|
||||
return mapType(objectType, t => getSimplifiedType(getIndexedAccessType(t, indexType)));
|
||||
}
|
||||
function distributeIndexOverObjectType(objectType: Type, indexType: Type, writing: boolean) {
|
||||
// (T | U)[K] -> T[K] | U[K] (reading)
|
||||
// (T | U)[K] -> T[K] & U[K] (writing)
|
||||
// (T & U)[K] -> T[K] & U[K]
|
||||
if (objectType.flags & TypeFlags.Intersection) {
|
||||
return getIntersectionType(map((objectType as IntersectionType).types, t => getSimplifiedType(getIndexedAccessType(t, indexType))));
|
||||
if (objectType.flags & TypeFlags.UnionOrIntersection) {
|
||||
const types = map((objectType as UnionOrIntersectionType).types, t => getSimplifiedType(getIndexedAccessType(t, indexType), writing));
|
||||
return objectType.flags & TypeFlags.Intersection || writing ? getIntersectionType(types) : getUnionType(types);
|
||||
}
|
||||
}
|
||||
|
||||
function distributeObjectOverIndexType(objectType: Type, indexType: Type, writing: boolean) {
|
||||
// T[A | B] -> T[A] | T[B] (reading)
|
||||
// T[A | B] -> T[A] & T[B] (writing)
|
||||
if (indexType.flags & TypeFlags.Union) {
|
||||
const types = map((indexType as UnionType).types, t => getSimplifiedType(getIndexedAccessType(objectType, t), writing));
|
||||
return writing ? getIntersectionType(types) : getUnionType(types);
|
||||
}
|
||||
}
|
||||
|
||||
// Transform an indexed access to a simpler form, if possible. Return the simpler form, or return
|
||||
// the type itself if no transformation is possible.
|
||||
function getSimplifiedIndexedAccessType(type: IndexedAccessType): Type {
|
||||
if (type.simplified) {
|
||||
return type.simplified === circularConstraintType ? type : type.simplified;
|
||||
// the type itself if no transformation is possible. The writing flag indicates that the type is
|
||||
// the target of an assignment.
|
||||
function getSimplifiedIndexedAccessType(type: IndexedAccessType, writing: boolean): Type {
|
||||
const cache = writing ? "simplifiedForWriting" : "simplifiedForReading";
|
||||
if (type[cache]) {
|
||||
return type[cache] === circularConstraintType ? type : type[cache]!;
|
||||
}
|
||||
type.simplified = circularConstraintType;
|
||||
type[cache] = circularConstraintType;
|
||||
// We recursively simplify the object type as it may in turn be an indexed access type. For example, with
|
||||
// '{ [P in T]: { [Q in U]: number } }[T][U]' we want to first simplify the inner indexed access type.
|
||||
const objectType = getSimplifiedType(type.objectType);
|
||||
const indexType = getSimplifiedType(type.indexType);
|
||||
// T[A | B] -> T[A] | T[B]
|
||||
if (indexType.flags & TypeFlags.Union) {
|
||||
return type.simplified = mapType(indexType, t => getSimplifiedType(getIndexedAccessType(objectType, t)));
|
||||
const objectType = getSimplifiedType(type.objectType, writing);
|
||||
const indexType = getSimplifiedType(type.indexType, writing);
|
||||
// T[A | B] -> T[A] | T[B] (reading)
|
||||
// T[A | B] -> T[A] & T[B] (writing)
|
||||
const distributedOverIndex = distributeObjectOverIndexType(objectType, indexType, writing);
|
||||
if (distributedOverIndex) {
|
||||
return type[cache] = distributedOverIndex;
|
||||
}
|
||||
// Only do the inner distributions if the index can no longer be instantiated to cause index distribution again
|
||||
if (!(indexType.flags & TypeFlags.Instantiable)) {
|
||||
const simplified = distributeIndexOverObjectType(objectType, indexType);
|
||||
if (simplified) {
|
||||
return type.simplified = simplified;
|
||||
// (T | U)[K] -> T[K] | U[K] (reading)
|
||||
// (T | U)[K] -> T[K] & U[K] (writing)
|
||||
// (T & U)[K] -> T[K] & U[K]
|
||||
const distributedOverObject = distributeIndexOverObjectType(objectType, indexType, writing);
|
||||
if (distributedOverObject) {
|
||||
return type[cache] = distributedOverObject;
|
||||
}
|
||||
}
|
||||
// So ultimately:
|
||||
// So ultimately (reading):
|
||||
// ((A & B) | C)[K1 | K2] -> ((A & B) | C)[K1] | ((A & B) | C)[K2] -> (A & B)[K1] | C[K1] | (A & B)[K2] | C[K2] -> (A[K1] & B[K1]) | C[K1] | (A[K2] & B[K2]) | C[K2]
|
||||
|
||||
// If the object type is a mapped type { [P in K]: E }, where K is generic, instantiate E using a mapper
|
||||
// that substitutes the index type for P. For example, for an index access { [P in K]: Box<T[P]> }[X], we
|
||||
// construct the type Box<T[X]>.
|
||||
if (isGenericMappedType(objectType)) {
|
||||
return type.simplified = mapType(substituteIndexedMappedType(objectType, type.indexType), getSimplifiedType);
|
||||
return type[cache] = mapType(substituteIndexedMappedType(objectType, type.indexType), t => getSimplifiedType(t, writing));
|
||||
}
|
||||
return type.simplified = type;
|
||||
return type[cache] = type;
|
||||
}
|
||||
|
||||
function substituteIndexedMappedType(objectType: MappedType, index: Type) {
|
||||
@@ -12197,10 +12215,10 @@ namespace ts {
|
||||
target = (<SubstitutionType>target).typeVariable;
|
||||
}
|
||||
if (source.flags & TypeFlags.IndexedAccess) {
|
||||
source = getSimplifiedType(source);
|
||||
source = getSimplifiedType(source, /*writing*/ false);
|
||||
}
|
||||
if (target.flags & TypeFlags.IndexedAccess) {
|
||||
target = getSimplifiedType(target);
|
||||
target = getSimplifiedType(target, /*writing*/ true);
|
||||
}
|
||||
|
||||
// Try to see if we're relating something like `Foo` -> `Bar | null | undefined`.
|
||||
@@ -12780,7 +12798,7 @@ namespace ts {
|
||||
}
|
||||
// A type S is assignable to keyof T if S is assignable to keyof C, where C is the
|
||||
// simplified form of T or, if T doesn't simplify, the constraint of T.
|
||||
const simplified = getSimplifiedType((<IndexType>target).type);
|
||||
const simplified = getSimplifiedType((<IndexType>target).type, /*writing*/ false);
|
||||
const constraint = simplified !== (<IndexType>target).type ? simplified : getConstraintOfType((<IndexType>target).type);
|
||||
if (constraint) {
|
||||
// We require Ternary.True here such that circular constraints don't cause
|
||||
@@ -12795,12 +12813,26 @@ namespace ts {
|
||||
else if (target.flags & TypeFlags.IndexedAccess) {
|
||||
// A type S is related to a type T[K], where T and K aren't both type variables, if S is related to C,
|
||||
// where C is the base constraint of T[K]
|
||||
if (relation !== identityRelation &&
|
||||
!(isGenericObjectType((<IndexedAccessType>target).objectType) && isGenericIndexType((<IndexedAccessType>target).indexType))) {
|
||||
const constraint = getBaseConstraintOfType(target);
|
||||
if (constraint && constraint !== target) {
|
||||
if (result = isRelatedTo(source, constraint, reportErrors)) {
|
||||
return result;
|
||||
if (relation !== identityRelation) {
|
||||
const objectType = (<IndexedAccessType>target).objectType
|
||||
const indexType = (<IndexedAccessType>target).indexType;
|
||||
if (indexType.flags & TypeFlags.StructuredOrInstantiable) {
|
||||
const keyType = getLowerBoundOfKeyType(indexType, /*isIndexType*/ true);
|
||||
if (keyType !== indexType && !(keyType.flags & TypeFlags.Never)) {
|
||||
const targetType = keyType.flags & TypeFlags.Union ?
|
||||
getIntersectionType(map((<UnionType>keyType).types, t => getIndexedAccessType(objectType, t))) :
|
||||
getIndexedAccessType(objectType, keyType);
|
||||
if (result = isRelatedTo(source, targetType, reportErrors)) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
const constraint = getConstraintOfType(objectType);
|
||||
if (constraint) {
|
||||
if (result = isRelatedTo(source, getIndexedAccessType(constraint, indexType), reportErrors)) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14660,16 +14692,16 @@ namespace ts {
|
||||
}
|
||||
else {
|
||||
// Infer to the simplified version of an indexed access, if possible, to (hopefully) expose more bare type parameters to the inference engine
|
||||
const simplified = getSimplifiedType(target);
|
||||
const simplified = getSimplifiedType(target, /*writing*/ false);
|
||||
if (simplified !== target) {
|
||||
inferFromTypesOnce(source, simplified);
|
||||
}
|
||||
else if (target.flags & TypeFlags.IndexedAccess) {
|
||||
const indexType = getSimplifiedType((target as IndexedAccessType).indexType);
|
||||
const indexType = getSimplifiedType((target as IndexedAccessType).indexType, /*writing*/ false);
|
||||
// Generally simplifications of instantiable indexes are avoided to keep relationship checking correct, however if our target is an access, we can consider
|
||||
// that key of that access to be "instantiated", since we're looking to find the infernce goal in any way we can.
|
||||
if (indexType.flags & TypeFlags.Instantiable) {
|
||||
const simplified = distributeIndexOverObjectType(getSimplifiedType((target as IndexedAccessType).objectType), indexType);
|
||||
const simplified = distributeIndexOverObjectType(getSimplifiedType((target as IndexedAccessType).objectType, /*writing*/ false), indexType, /*writing*/ false);
|
||||
if (simplified && simplified !== target) {
|
||||
inferFromTypesOnce(source, simplified);
|
||||
}
|
||||
@@ -15697,24 +15729,19 @@ namespace ts {
|
||||
if (!(type.flags & TypeFlags.Union)) {
|
||||
return mapper(type);
|
||||
}
|
||||
const types = (<UnionType>type).types;
|
||||
let mappedType: Type | undefined;
|
||||
let mappedTypes: Type[] | undefined;
|
||||
for (const current of types) {
|
||||
const t = mapper(current);
|
||||
if (t) {
|
||||
if (!mappedType) {
|
||||
mappedType = t;
|
||||
}
|
||||
else if (!mappedTypes) {
|
||||
mappedTypes = [mappedType, t];
|
||||
for (const t of (<UnionType>type).types) {
|
||||
const mapped = mapper(t);
|
||||
if (mapped) {
|
||||
if (!mappedTypes) {
|
||||
mappedTypes = [mapped];
|
||||
}
|
||||
else {
|
||||
mappedTypes.push(t);
|
||||
mappedTypes.push(mapped);
|
||||
}
|
||||
}
|
||||
}
|
||||
return mappedTypes ? getUnionType(mappedTypes, noReductions ? UnionReduction.None : UnionReduction.Literal) : mappedType;
|
||||
return mappedTypes && getUnionType(mappedTypes, noReductions ? UnionReduction.None : UnionReduction.Literal);
|
||||
}
|
||||
|
||||
function extractTypesOfKind(type: Type, kind: TypeFlags) {
|
||||
|
||||
@@ -4279,7 +4279,8 @@ namespace ts {
|
||||
objectType: Type;
|
||||
indexType: Type;
|
||||
constraint?: Type;
|
||||
simplified?: Type;
|
||||
simplifiedForReading?: Type;
|
||||
simplifiedForWriting?: Type;
|
||||
}
|
||||
|
||||
export type TypeVariable = TypeParameter | IndexedAccessType;
|
||||
|
||||
Reference in New Issue
Block a user