Skip to content

Commit 9769421

Browse files
authored
Defer resolution of indexed access types with reducible object types (#53098)
1 parent ae1b3db commit 9769421

6 files changed

+352
-53
lines changed

src/compiler/checker.ts

Lines changed: 44 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,7 @@ import {
406406
ImportTypeNode,
407407
IndexedAccessType,
408408
IndexedAccessTypeNode,
409+
IndexFlags,
409410
IndexInfo,
410411
IndexKind,
411412
indexOfNode,
@@ -1451,6 +1452,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
14511452
var noImplicitThis = getStrictOptionValue(compilerOptions, "noImplicitThis");
14521453
var useUnknownInCatchVariables = getStrictOptionValue(compilerOptions, "useUnknownInCatchVariables");
14531454
var keyofStringsOnly = !!compilerOptions.keyofStringsOnly;
1455+
var defaultIndexFlags = keyofStringsOnly ? IndexFlags.StringsOnly : IndexFlags.None;
14541456
var freshObjectLiteralFlag = compilerOptions.suppressExcessPropertyErrors ? 0 : ObjectFlags.FreshLiteral;
14551457
var exactOptionalPropertyTypes = compilerOptions.exactOptionalPropertyTypes;
14561458

@@ -14155,6 +14157,23 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1415514157
return !prop.valueDeclaration && !!(getCheckFlags(prop) & CheckFlags.ContainsPrivate);
1415614158
}
1415714159

14160+
/**
14161+
* A union type which is reducible upon instantiation (meaning some members are removed under certain instantiations)
14162+
* must be kept generic, as that instantiation information needs to flow through the type system. By replacing all
14163+
* type parameters in the union with a special never type that is treated as a literal in `getReducedType`, we can cause
14164+
* the `getReducedType` logic to reduce the resulting type if possible (since only intersections with conflicting
14165+
* literal-typed properties are reducible).
14166+
*/
14167+
function isGenericReducibleType(type: Type): boolean {
14168+
return !!(type.flags & TypeFlags.Union && (type as UnionType).objectFlags & ObjectFlags.ContainsIntersections && some((type as UnionType).types, isGenericReducibleType) ||
14169+
type.flags & TypeFlags.Intersection && isReducibleIntersection(type as IntersectionType));
14170+
}
14171+
14172+
function isReducibleIntersection(type: IntersectionType) {
14173+
const uniqueFilled = type.uniqueLiteralFilledInstantiation || (type.uniqueLiteralFilledInstantiation = instantiateType(type, uniqueLiteralMapper));
14174+
return getReducedType(uniqueFilled) !== uniqueFilled;
14175+
}
14176+
1415814177
function elaborateNeverIntersection(errorInfo: DiagnosticMessageChain | undefined, type: Type) {
1415914178
if (type.flags & TypeFlags.Intersection && getObjectFlags(type) & ObjectFlags.IsNeverIntersection) {
1416014179
const neverProp = find(getPropertiesOfUnionOrIntersectionType(type as IntersectionType), isDiscriminantWithNeverType);
@@ -16810,10 +16829,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1681016829
return links.resolvedType;
1681116830
}
1681216831

16813-
function createIndexType(type: InstantiableType | UnionOrIntersectionType, stringsOnly: boolean) {
16832+
function createIndexType(type: InstantiableType | UnionOrIntersectionType, indexFlags: IndexFlags) {
1681416833
const result = createType(TypeFlags.Index) as IndexType;
1681516834
result.type = type;
16816-
result.stringsOnly = stringsOnly;
16835+
result.indexFlags = indexFlags;
1681716836
return result;
1681816837
}
1681916838

@@ -16823,10 +16842,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1682316842
return result;
1682416843
}
1682516844

16826-
function getIndexTypeForGenericType(type: InstantiableType | UnionOrIntersectionType, stringsOnly: boolean) {
16827-
return stringsOnly ?
16828-
type.resolvedStringIndexType || (type.resolvedStringIndexType = createIndexType(type, /*stringsOnly*/ true)) :
16829-
type.resolvedIndexType || (type.resolvedIndexType = createIndexType(type, /*stringsOnly*/ false));
16845+
function getIndexTypeForGenericType(type: InstantiableType | UnionOrIntersectionType, indexFlags: IndexFlags) {
16846+
return indexFlags & IndexFlags.StringsOnly ?
16847+
type.resolvedStringIndexType || (type.resolvedStringIndexType = createIndexType(type, IndexFlags.StringsOnly)) :
16848+
type.resolvedIndexType || (type.resolvedIndexType = createIndexType(type, IndexFlags.None));
1683016849
}
1683116850

1683216851
/**
@@ -16836,11 +16855,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1683616855
* reduction in the constraintType) when possible.
1683716856
* @param noIndexSignatures Indicates if _string_ index signatures should be elided. (other index signatures are always reported)
1683816857
*/
16839-
function getIndexTypeForMappedType(type: MappedType, stringsOnly: boolean, noIndexSignatures: boolean | undefined) {
16858+
function getIndexTypeForMappedType(type: MappedType, indexFlags: IndexFlags) {
1684016859
const typeParameter = getTypeParameterFromMappedType(type);
1684116860
const constraintType = getConstraintTypeFromMappedType(type);
1684216861
const nameType = getNameTypeFromMappedType(type.target as MappedType || type);
16843-
if (!nameType && !noIndexSignatures) {
16862+
if (!nameType && !(indexFlags & IndexFlags.NoIndexSignatures)) {
1684416863
// no mapping and no filtering required, just quickly bail to returning the constraint in the common case
1684516864
return constraintType;
1684616865
}
@@ -16853,12 +16872,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1685316872
// so we only eagerly manifest the keys if the constraint is nongeneric
1685416873
if (!isGenericIndexType(constraintType)) {
1685516874
const modifiersType = getApparentType(getModifiersTypeFromMappedType(type)); // The 'T' in 'keyof T'
16856-
forEachMappedTypePropertyKeyTypeAndIndexSignatureKeyType(modifiersType, TypeFlags.StringOrNumberLiteralOrUnique, stringsOnly, addMemberForKeyType);
16875+
forEachMappedTypePropertyKeyTypeAndIndexSignatureKeyType(modifiersType, TypeFlags.StringOrNumberLiteralOrUnique, !!(indexFlags & IndexFlags.StringsOnly), addMemberForKeyType);
1685716876
}
1685816877
else {
1685916878
// we have a generic index and a homomorphic mapping (but a distributive key remapping) - we need to defer the whole `keyof whatever` for later
1686016879
// since it's not safe to resolve the shape of modifier type
16861-
return getIndexTypeForGenericType(type, stringsOnly);
16880+
return getIndexTypeForGenericType(type, indexFlags);
1686216881
}
1686316882
}
1686416883
else {
@@ -16869,7 +16888,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1686916888
}
1687016889
// we had to pick apart the constraintType to potentially map/filter it - compare the final resulting list with the original constraintType,
1687116890
// so we can return the union that preserves aliases/origin data if possible
16872-
const result = noIndexSignatures ? filterType(getUnionType(keyTypes), t => !(t.flags & (TypeFlags.Any | TypeFlags.String))) : getUnionType(keyTypes);
16891+
const result = indexFlags & IndexFlags.NoIndexSignatures ? filterType(getUnionType(keyTypes), t => !(t.flags & (TypeFlags.Any | TypeFlags.String))) : getUnionType(keyTypes);
1687316892
if (result.flags & TypeFlags.Union && constraintType.flags & TypeFlags.Union && getTypeListId((result as UnionType).types) === getTypeListId((constraintType as UnionType).types)){
1687416893
return constraintType;
1687516894
}
@@ -16938,36 +16957,25 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1693816957
/*aliasSymbol*/ undefined, /*aliasTypeArguments*/ undefined, origin);
1693916958
}
1694016959

16941-
/**
16942-
* A union type which is reducible upon instantiation (meaning some members are removed under certain instantiations)
16943-
* must be kept generic, as that instantiation information needs to flow through the type system. By replacing all
16944-
* type parameters in the union with a special never type that is treated as a literal in `getReducedType`, we can cause the `getReducedType` logic
16945-
* to reduce the resulting type if possible (since only intersections with conflicting literal-typed properties are reducible).
16946-
*/
16947-
function isPossiblyReducibleByInstantiation(type: Type): boolean {
16948-
const uniqueFilled = getUniqueLiteralFilledInstantiation(type);
16949-
return getReducedType(uniqueFilled) !== uniqueFilled;
16950-
}
16951-
16952-
function shouldDeferIndexType(type: Type) {
16960+
function shouldDeferIndexType(type: Type, indexFlags = IndexFlags.None) {
1695316961
return !!(type.flags & TypeFlags.InstantiableNonPrimitive ||
1695416962
isGenericTupleType(type) ||
1695516963
isGenericMappedType(type) && !hasDistributiveNameType(type) ||
16956-
type.flags & TypeFlags.Union && some((type as UnionType).types, isPossiblyReducibleByInstantiation) ||
16964+
type.flags & TypeFlags.Union && !(indexFlags & IndexFlags.NoReducibleCheck) && isGenericReducibleType(type) ||
1695716965
type.flags & TypeFlags.Intersection && maybeTypeOfKind(type, TypeFlags.Instantiable) && some((type as IntersectionType).types, isEmptyAnonymousObjectType));
1695816966
}
1695916967

16960-
function getIndexType(type: Type, stringsOnly = keyofStringsOnly, noIndexSignatures?: boolean): Type {
16968+
function getIndexType(type: Type, indexFlags = defaultIndexFlags): Type {
1696116969
type = getReducedType(type);
16962-
return shouldDeferIndexType(type) ? getIndexTypeForGenericType(type as InstantiableType | UnionOrIntersectionType, stringsOnly) :
16963-
type.flags & TypeFlags.Union ? getIntersectionType(map((type as UnionType).types, t => getIndexType(t, stringsOnly, noIndexSignatures))) :
16964-
type.flags & TypeFlags.Intersection ? getUnionType(map((type as IntersectionType).types, t => getIndexType(t, stringsOnly, noIndexSignatures))) :
16965-
getObjectFlags(type) & ObjectFlags.Mapped ? getIndexTypeForMappedType(type as MappedType, stringsOnly, noIndexSignatures) :
16970+
return shouldDeferIndexType(type, indexFlags) ? getIndexTypeForGenericType(type as InstantiableType | UnionOrIntersectionType, indexFlags) :
16971+
type.flags & TypeFlags.Union ? getIntersectionType(map((type as UnionType).types, t => getIndexType(t, indexFlags))) :
16972+
type.flags & TypeFlags.Intersection ? getUnionType(map((type as IntersectionType).types, t => getIndexType(t, indexFlags))) :
16973+
getObjectFlags(type) & ObjectFlags.Mapped ? getIndexTypeForMappedType(type as MappedType, indexFlags) :
1696616974
type === wildcardType ? wildcardType :
1696716975
type.flags & TypeFlags.Unknown ? neverType :
1696816976
type.flags & (TypeFlags.Any | TypeFlags.Never) ? keyofConstraintType :
16969-
getLiteralTypeFromProperties(type, (noIndexSignatures ? TypeFlags.StringLiteral : TypeFlags.StringLike) | (stringsOnly ? 0 : TypeFlags.NumberLike | TypeFlags.ESSymbolLike),
16970-
stringsOnly === keyofStringsOnly && !noIndexSignatures);
16977+
getLiteralTypeFromProperties(type, (indexFlags & IndexFlags.NoIndexSignatures ? TypeFlags.StringLiteral : TypeFlags.StringLike) | (indexFlags & IndexFlags.StringsOnly ? 0 : TypeFlags.NumberLike | TypeFlags.ESSymbolLike),
16978+
indexFlags === defaultIndexFlags);
1697116979
}
1697216980

1697316981
function getExtractStringType(type: Type) {
@@ -17580,6 +17588,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1758017588
if (objectType === wildcardType || indexType === wildcardType) {
1758117589
return wildcardType;
1758217590
}
17591+
objectType = getReducedType(objectType);
1758317592
// If the object type has a string index signature and no other members we know that the result will
1758417593
// always be the type of that index signature and we can simplify accordingly.
1758517594
if (isStringIndexSignatureOnlyType(objectType) && !(indexType.flags & TypeFlags.Nullable) && isTypeAssignableToKind(indexType, TypeFlags.String | TypeFlags.Number)) {
@@ -17596,7 +17605,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1759617605
// eagerly using the constraint type of 'this' at the given location.
1759717606
if (isGenericIndexType(indexType) || (accessNode && accessNode.kind !== SyntaxKind.IndexedAccessType ?
1759817607
isGenericTupleType(objectType) && !indexTypeLessThan(indexType, objectType.target.fixedLength) :
17599-
isGenericObjectType(objectType) && !(isTupleType(objectType) && indexTypeLessThan(indexType, objectType.target.fixedLength)))) {
17608+
isGenericObjectType(objectType) && !(isTupleType(objectType) && indexTypeLessThan(indexType, objectType.target.fixedLength)) || isGenericReducibleType(objectType))) {
1760017609
if (objectType.flags & TypeFlags.AnyOrUnknown) {
1760117610
return objectType;
1760217611
}
@@ -19016,11 +19025,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1901619025
return type; // Nested invocation of `inferTypeForHomomorphicMappedType` or the `source` instantiated into something unmappable
1901719026
}
1901819027

19019-
function getUniqueLiteralFilledInstantiation(type: Type) {
19020-
return type.flags & (TypeFlags.Primitive | TypeFlags.AnyOrUnknown | TypeFlags.Never) ? type :
19021-
type.uniqueLiteralFilledInstantiation || (type.uniqueLiteralFilledInstantiation = instantiateType(type, uniqueLiteralMapper));
19022-
}
19023-
1902419028
function getPermissiveInstantiation(type: Type) {
1902519029
return type.flags & (TypeFlags.Primitive | TypeFlags.AnyOrUnknown | TypeFlags.Never) ? type :
1902619030
type.permissiveInstantiation || (type.permissiveInstantiation = instantiateType(type, permissiveMapper));
@@ -21299,7 +21303,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2129921303
// false positives. For example, given 'T extends { [K in keyof T]: string }',
2130021304
// 'keyof T' has itself as its constraint and produces a Ternary.Maybe when
2130121305
// related to other types.
21302-
if (isRelatedTo(source, getIndexType(constraint, (target as IndexType).stringsOnly), RecursionFlags.Target, reportErrors) === Ternary.True) {
21306+
if (isRelatedTo(source, getIndexType(constraint, (target as IndexType).indexFlags | IndexFlags.NoReducibleCheck), RecursionFlags.Target, reportErrors) === Ternary.True) {
2130321307
return Ternary.True;
2130421308
}
2130521309
}
@@ -21399,7 +21403,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2139921403
// If target has shape `{ [P in Q]: T }`, then its keys have type `Q`.
2140021404
const targetKeys = keysRemapped ? getNameTypeFromMappedType(target)! : getConstraintTypeFromMappedType(target);
2140121405
// Type of the keys of source type `S`, i.e. `keyof S`.
21402-
const sourceKeys = getIndexType(source, /*stringsOnly*/ undefined, /*noIndexSignatures*/ true);
21406+
const sourceKeys = getIndexType(source, IndexFlags.NoIndexSignatures);
2140321407
const includeOptional = modifiers & MappedTypeModifiers.IncludeOptional;
2140421408
const filteredByApplicability = includeOptional ? intersectTypes(targetKeys, sourceKeys) : undefined;
2140521409
// A source type `S` is related to a target type `{ [P in Q]: T }` if `Q` is related to `keyof S` and `S[Q]` is related to `T`.
@@ -38526,7 +38530,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3852638530
// Check if the index type is assignable to 'keyof T' for the object type.
3852738531
const objectType = (type as IndexedAccessType).objectType;
3852838532
const indexType = (type as IndexedAccessType).indexType;
38529-
if (isTypeAssignableTo(indexType, getIndexType(objectType, /*stringsOnly*/ false))) {
38533+
if (isTypeAssignableTo(indexType, getIndexType(objectType, IndexFlags.None))) {
3853038534
if (accessNode.kind === SyntaxKind.ElementAccessExpression && isAssignmentTarget(accessNode) &&
3853138535
getObjectFlags(objectType) & ObjectFlags.Mapped && getMappedTypeModifiers(objectType as MappedType) & MappedTypeModifiers.IncludeReadonly) {
3853238536
error(accessNode, Diagnostics.Index_signature_in_type_0_only_permits_reading, typeToString(objectType));

src/compiler/types.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6152,8 +6152,6 @@ export interface Type {
61526152
/** @internal */
61536153
restrictiveInstantiation?: Type; // Instantiation with type parameters mapped to unconstrained form
61546154
/** @internal */
6155-
uniqueLiteralFilledInstantiation?: Type; // Instantiation with type parameters mapped to never type
6156-
/** @internal */
61576155
immediateBaseConstraint?: Type; // Immediate base constraint cache
61586156
/** @internal */
61596157
widened?: Type; // Cached widened form of the type
@@ -6442,6 +6440,8 @@ export interface UnionType extends UnionOrIntersectionType {
64426440
export interface IntersectionType extends UnionOrIntersectionType {
64436441
/** @internal */
64446442
resolvedApparentType: Type;
6443+
/** @internal */
6444+
uniqueLiteralFilledInstantiation?: Type; // Instantiation with type parameters mapped to never type
64456445
}
64466446

64476447
export type StructuredType = ObjectType | UnionType | IntersectionType;
@@ -6591,11 +6591,19 @@ export interface IndexedAccessType extends InstantiableType {
65916591

65926592
export type TypeVariable = TypeParameter | IndexedAccessType;
65936593

6594+
/** @internal */
6595+
export const enum IndexFlags {
6596+
None = 0,
6597+
StringsOnly = 1 << 0,
6598+
NoIndexSignatures = 1 << 1,
6599+
NoReducibleCheck = 1 << 2,
6600+
}
6601+
65946602
// keyof T types (TypeFlags.Index)
65956603
export interface IndexType extends InstantiableType {
65966604
type: InstantiableType | UnionOrIntersectionType;
65976605
/** @internal */
6598-
stringsOnly: boolean;
6606+
indexFlags: IndexFlags;
65996607
}
66006608

66016609
export interface ConditionalRoot {

0 commit comments

Comments
 (0)