Skip to content

Commit fc95dc4

Browse files
committed
Improve soundness of indexed access type relations
1 parent 17cedda commit fc95dc4

File tree

2 files changed

+98
-70
lines changed

2 files changed

+98
-70
lines changed

src/compiler/checker.ts

Lines changed: 96 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -7188,34 +7188,37 @@ namespace ts {
71887188
// Return the lower bound of the key type in a mapped type. Intuitively, the lower
71897189
// bound includes those keys that are known to always be present, for example because
71907190
// because of constraints on type parameters (e.g. 'keyof T' for a constrained T).
7191-
function getLowerBoundOfKeyType(type: Type): Type {
7191+
// The isIndexType flag indicates that the type is the index type of an indexed
7192+
// access that is the target of an assignment.
7193+
function getLowerBoundOfKeyType(type: Type, isIndexType: boolean): Type {
71927194
if (type.flags & (TypeFlags.Any | TypeFlags.Primitive)) {
71937195
return type;
71947196
}
71957197
if (type.flags & TypeFlags.Index) {
7196-
return getIndexType(getApparentType((<IndexType>type).type));
7198+
const keys = getIndexType(getApparentType((<IndexType>type).type));
7199+
return isIndexType ? filterType(keys, t => !!(t.flags & TypeFlags.Literal)) : keys;
71977200
}
71987201
if (type.flags & TypeFlags.Conditional) {
7199-
return getLowerBoundOfConditionalType(<ConditionalType>type);
7202+
if ((<ConditionalType>type).root.isDistributive) {
7203+
const checkType = (<ConditionalType>type).checkType;
7204+
const constraint = getLowerBoundOfKeyType(checkType, isIndexType);
7205+
if (constraint !== checkType) {
7206+
const mapper = makeUnaryTypeMapper((<ConditionalType>type).root.checkType, constraint);
7207+
return getConditionalTypeInstantiation(<ConditionalType>type, combineTypeMappers(mapper, (<ConditionalType>type).mapper));
7208+
}
7209+
}
7210+
return type;
72007211
}
72017212
if (type.flags & TypeFlags.Union) {
7202-
return getUnionType(sameMap((<UnionType>type).types, getLowerBoundOfKeyType));
7213+
return getUnionType(sameMap((<UnionType>type).types, t => getLowerBoundOfKeyType(t, isIndexType)));
72037214
}
72047215
if (type.flags & TypeFlags.Intersection) {
7205-
return getIntersectionType(sameMap((<UnionType>type).types, getLowerBoundOfKeyType));
7216+
return getIntersectionType(sameMap((<UnionType>type).types, t => getLowerBoundOfKeyType(t, isIndexType)));
72067217
}
7207-
return neverType;
7208-
}
7209-
7210-
function getLowerBoundOfConditionalType(type: ConditionalType) {
7211-
if (type.root.isDistributive) {
7212-
const constraint = getLowerBoundOfKeyType(type.checkType);
7213-
if (constraint !== type.checkType) {
7214-
const mapper = makeUnaryTypeMapper(type.root.checkType, constraint);
7215-
return getConditionalTypeInstantiation(type, combineTypeMappers(mapper, type.mapper));
7216-
}
7218+
if (isIndexType && type.flags & TypeFlags.Instantiable) {
7219+
return getLowerBoundOfKeyType(getConstraintOfType(type) || neverType, isIndexType);
72177220
}
7218-
return type;
7221+
return neverType;
72197222
}
72207223

72217224
/** Resolve the members of a mapped type { [P in K]: T } */
@@ -7246,7 +7249,7 @@ namespace ts {
72467249
}
72477250
}
72487251
else {
7249-
forEachType(getLowerBoundOfKeyType(constraintType), addMemberForKeyType);
7252+
forEachType(getLowerBoundOfKeyType(constraintType, /*isIndexType*/ false), addMemberForKeyType);
72507253
}
72517254
setStructuredTypeMembers(type, members, emptyArray, emptyArray, stringIndexInfo, numberIndexInfo);
72527255

@@ -7523,7 +7526,7 @@ namespace ts {
75237526
// a union - once negated types exist and are applied to the conditional false branch, this "constraint"
75247527
// likely doesn't need to exist.
75257528
if (type.root.isDistributive && type.restrictiveInstantiation !== type) {
7526-
const simplified = getSimplifiedType(type.checkType);
7529+
const simplified = getSimplifiedType(type.checkType, /*writing*/ false);
75277530
const constraint = simplified === type.checkType ? getConstraintOfType(simplified) : simplified;
75287531
if (constraint && constraint !== type.checkType) {
75297532
const mapper = makeUnaryTypeMapper(type.root.checkType, constraint);
@@ -7630,7 +7633,7 @@ namespace ts {
76307633
return t.immediateBaseConstraint = noConstraintType;
76317634
}
76327635
constraintDepth++;
7633-
let result = computeBaseConstraint(getSimplifiedType(t));
7636+
let result = computeBaseConstraint(getSimplifiedType(t, /*writing*/ false));
76347637
constraintDepth--;
76357638
if (!popTypeResolution()) {
76367639
if (t.flags & TypeFlags.TypeParameter) {
@@ -10003,53 +10006,68 @@ namespace ts {
1000310006
return maybeTypeOfKind(type, TypeFlags.InstantiableNonPrimitive | TypeFlags.Index);
1000410007
}
1000510008

10006-
function getSimplifiedType(type: Type): Type {
10007-
return type.flags & TypeFlags.IndexedAccess ? getSimplifiedIndexedAccessType(<IndexedAccessType>type) : type;
10009+
function getSimplifiedType(type: Type, writing: boolean): Type {
10010+
return type.flags & TypeFlags.IndexedAccess ? getSimplifiedIndexedAccessType(<IndexedAccessType>type, writing) : type;
1000810011
}
1000910012

10010-
function distributeIndexOverObjectType(objectType: Type, indexType: Type) {
10011-
// (T | U)[K] -> T[K] | U[K]
10012-
if (objectType.flags & TypeFlags.Union) {
10013-
return mapType(objectType, t => getSimplifiedType(getIndexedAccessType(t, indexType)));
10014-
}
10013+
function distributeIndexOverObjectType(objectType: Type, indexType: Type, writing: boolean) {
10014+
// (T | U)[K] -> T[K] | U[K] (reading)
10015+
// (T | U)[K] -> T[K] & U[K] (writing)
1001510016
// (T & U)[K] -> T[K] & U[K]
10016-
if (objectType.flags & TypeFlags.Intersection) {
10017-
return getIntersectionType(map((objectType as IntersectionType).types, t => getSimplifiedType(getIndexedAccessType(t, indexType))));
10017+
if (objectType.flags & TypeFlags.UnionOrIntersection) {
10018+
const types = map((objectType as UnionOrIntersectionType).types, t => getSimplifiedType(getIndexedAccessType(t, indexType), writing));
10019+
return objectType.flags & TypeFlags.Intersection || writing ? getIntersectionType(types) : getUnionType(types);
1001810020
}
1001910021
}
1002010022

10021-
// Transform an indexed access to a simpler form, if possible. Return the simpler form, or return
10022-
// the type itself if no transformation is possible.
10023-
function getSimplifiedIndexedAccessType(type: IndexedAccessType): Type {
10024-
if (type.simplified) {
10025-
return type.simplified === circularConstraintType ? type : type.simplified;
10023+
function distributeObjectOverIndexType(objectType: Type, indexType: Type, writing: boolean) {
10024+
// T[A | B] -> T[A] | T[B] (reading)
10025+
// T[A | B] -> T[A] & T[B] (writing)
10026+
if (indexType.flags & TypeFlags.Union) {
10027+
const types = map((indexType as UnionType).types, t => getSimplifiedType(getIndexedAccessType(objectType, t), writing));
10028+
return writing ? getIntersectionType(types) : getUnionType(types);
1002610029
}
10027-
type.simplified = circularConstraintType;
10030+
}
10031+
10032+
// Transform an indexed access to a simpler form, if possible. Return the simpler form, or return
10033+
// the type itself if no transformation is possible. The writing flag indicates that the type is
10034+
// the target of an assignment.
10035+
function getSimplifiedIndexedAccessType(type: IndexedAccessType, writing: boolean): Type {
10036+
const cache = writing ? "simplifiedForWriting" : "simplifiedForReading";
10037+
if (type[cache]) {
10038+
return type[cache] === circularConstraintType ? type : type[cache]!;
10039+
}
10040+
type[cache] = circularConstraintType;
1002810041
// We recursively simplify the object type as it may in turn be an indexed access type. For example, with
1002910042
// '{ [P in T]: { [Q in U]: number } }[T][U]' we want to first simplify the inner indexed access type.
10030-
const objectType = getSimplifiedType(type.objectType);
10031-
const indexType = getSimplifiedType(type.indexType);
10032-
// T[A | B] -> T[A] | T[B]
10033-
if (indexType.flags & TypeFlags.Union) {
10034-
return type.simplified = mapType(indexType, t => getSimplifiedType(getIndexedAccessType(objectType, t)));
10043+
const objectType = getSimplifiedType(type.objectType, writing);
10044+
const indexType = getSimplifiedType(type.indexType, writing);
10045+
// T[A | B] -> T[A] | T[B] (reading)
10046+
// T[A | B] -> T[A] & T[B] (writing)
10047+
const distributedOverIndex = distributeObjectOverIndexType(objectType, indexType, writing);
10048+
if (distributedOverIndex) {
10049+
return type[cache] = distributedOverIndex;
1003510050
}
1003610051
// Only do the inner distributions if the index can no longer be instantiated to cause index distribution again
1003710052
if (!(indexType.flags & TypeFlags.Instantiable)) {
10038-
const simplified = distributeIndexOverObjectType(objectType, indexType);
10039-
if (simplified) {
10040-
return type.simplified = simplified;
10053+
// (T | U)[K] -> T[K] | U[K] (reading)
10054+
// (T | U)[K] -> T[K] & U[K] (writing)
10055+
// (T & U)[K] -> T[K] & U[K]
10056+
const distributedOverObject = distributeIndexOverObjectType(objectType, indexType, writing);
10057+
if (distributedOverObject) {
10058+
return type[cache] = distributedOverObject;
1004110059
}
1004210060
}
10043-
// So ultimately:
10061+
// So ultimately (reading):
1004410062
// ((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]
1004510063

1004610064
// If the object type is a mapped type { [P in K]: E }, where K is generic, instantiate E using a mapper
1004710065
// that substitutes the index type for P. For example, for an index access { [P in K]: Box<T[P]> }[X], we
1004810066
// construct the type Box<T[X]>.
1004910067
if (isGenericMappedType(objectType)) {
10050-
return type.simplified = mapType(substituteIndexedMappedType(objectType, type.indexType), getSimplifiedType);
10068+
return type[cache] = mapType(substituteIndexedMappedType(objectType, type.indexType), t => getSimplifiedType(t, writing));
1005110069
}
10052-
return type.simplified = type;
10070+
return type[cache] = type;
1005310071
}
1005410072

1005510073
function substituteIndexedMappedType(objectType: MappedType, index: Type) {
@@ -12197,10 +12215,10 @@ namespace ts {
1219712215
target = (<SubstitutionType>target).typeVariable;
1219812216
}
1219912217
if (source.flags & TypeFlags.IndexedAccess) {
12200-
source = getSimplifiedType(source);
12218+
source = getSimplifiedType(source, /*writing*/ false);
1220112219
}
1220212220
if (target.flags & TypeFlags.IndexedAccess) {
12203-
target = getSimplifiedType(target);
12221+
target = getSimplifiedType(target, /*writing*/ true);
1220412222
}
1220512223

1220612224
// Try to see if we're relating something like `Foo` -> `Bar | null | undefined`.
@@ -12780,7 +12798,7 @@ namespace ts {
1278012798
}
1278112799
// A type S is assignable to keyof T if S is assignable to keyof C, where C is the
1278212800
// simplified form of T or, if T doesn't simplify, the constraint of T.
12783-
const simplified = getSimplifiedType((<IndexType>target).type);
12801+
const simplified = getSimplifiedType((<IndexType>target).type, /*writing*/ false);
1278412802
const constraint = simplified !== (<IndexType>target).type ? simplified : getConstraintOfType((<IndexType>target).type);
1278512803
if (constraint) {
1278612804
// We require Ternary.True here such that circular constraints don't cause
@@ -12795,12 +12813,26 @@ namespace ts {
1279512813
else if (target.flags & TypeFlags.IndexedAccess) {
1279612814
// 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,
1279712815
// where C is the base constraint of T[K]
12798-
if (relation !== identityRelation &&
12799-
!(isGenericObjectType((<IndexedAccessType>target).objectType) && isGenericIndexType((<IndexedAccessType>target).indexType))) {
12800-
const constraint = getBaseConstraintOfType(target);
12801-
if (constraint && constraint !== target) {
12802-
if (result = isRelatedTo(source, constraint, reportErrors)) {
12803-
return result;
12816+
if (relation !== identityRelation) {
12817+
const objectType = (<IndexedAccessType>target).objectType
12818+
const indexType = (<IndexedAccessType>target).indexType;
12819+
if (indexType.flags & TypeFlags.StructuredOrInstantiable) {
12820+
const keyType = getLowerBoundOfKeyType(indexType, /*isIndexType*/ true);
12821+
if (keyType !== indexType && !(keyType.flags & TypeFlags.Never)) {
12822+
const targetType = keyType.flags & TypeFlags.Union ?
12823+
getIntersectionType(map((<UnionType>keyType).types, t => getIndexedAccessType(objectType, t))) :
12824+
getIndexedAccessType(objectType, keyType);
12825+
if (result = isRelatedTo(source, targetType, reportErrors)) {
12826+
return result;
12827+
}
12828+
}
12829+
}
12830+
else {
12831+
const constraint = getConstraintOfType(objectType);
12832+
if (constraint) {
12833+
if (result = isRelatedTo(source, getIndexedAccessType(constraint, indexType), reportErrors)) {
12834+
return result;
12835+
}
1280412836
}
1280512837
}
1280612838
}
@@ -14660,16 +14692,16 @@ namespace ts {
1466014692
}
1466114693
else {
1466214694
// Infer to the simplified version of an indexed access, if possible, to (hopefully) expose more bare type parameters to the inference engine
14663-
const simplified = getSimplifiedType(target);
14695+
const simplified = getSimplifiedType(target, /*writing*/ false);
1466414696
if (simplified !== target) {
1466514697
inferFromTypesOnce(source, simplified);
1466614698
}
1466714699
else if (target.flags & TypeFlags.IndexedAccess) {
14668-
const indexType = getSimplifiedType((target as IndexedAccessType).indexType);
14700+
const indexType = getSimplifiedType((target as IndexedAccessType).indexType, /*writing*/ false);
1466914701
// Generally simplifications of instantiable indexes are avoided to keep relationship checking correct, however if our target is an access, we can consider
1467014702
// that key of that access to be "instantiated", since we're looking to find the infernce goal in any way we can.
1467114703
if (indexType.flags & TypeFlags.Instantiable) {
14672-
const simplified = distributeIndexOverObjectType(getSimplifiedType((target as IndexedAccessType).objectType), indexType);
14704+
const simplified = distributeIndexOverObjectType(getSimplifiedType((target as IndexedAccessType).objectType, /*writing*/ false), indexType, /*writing*/ false);
1467314705
if (simplified && simplified !== target) {
1467414706
inferFromTypesOnce(source, simplified);
1467514707
}
@@ -15697,24 +15729,19 @@ namespace ts {
1569715729
if (!(type.flags & TypeFlags.Union)) {
1569815730
return mapper(type);
1569915731
}
15700-
const types = (<UnionType>type).types;
15701-
let mappedType: Type | undefined;
1570215732
let mappedTypes: Type[] | undefined;
15703-
for (const current of types) {
15704-
const t = mapper(current);
15705-
if (t) {
15706-
if (!mappedType) {
15707-
mappedType = t;
15708-
}
15709-
else if (!mappedTypes) {
15710-
mappedTypes = [mappedType, t];
15733+
for (const t of (<UnionType>type).types) {
15734+
const mapped = mapper(t);
15735+
if (mapped) {
15736+
if (!mappedTypes) {
15737+
mappedTypes = [mapped];
1571115738
}
1571215739
else {
15713-
mappedTypes.push(t);
15740+
mappedTypes.push(mapped);
1571415741
}
1571515742
}
1571615743
}
15717-
return mappedTypes ? getUnionType(mappedTypes, noReductions ? UnionReduction.None : UnionReduction.Literal) : mappedType;
15744+
return mappedTypes && getUnionType(mappedTypes, noReductions ? UnionReduction.None : UnionReduction.Literal);
1571815745
}
1571915746

1572015747
function extractTypesOfKind(type: Type, kind: TypeFlags) {

src/compiler/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4279,7 +4279,8 @@ namespace ts {
42794279
objectType: Type;
42804280
indexType: Type;
42814281
constraint?: Type;
4282-
simplified?: Type;
4282+
simplifiedForReading?: Type;
4283+
simplifiedForWriting?: Type;
42834284
}
42844285

42854286
export type TypeVariable = TypeParameter | IndexedAccessType;

0 commit comments

Comments
 (0)