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

Defer resolution of indexed access types with reducible object types #53098

Merged
merged 4 commits into from
Mar 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 44 additions & 40 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,7 @@ import {
ImportTypeNode,
IndexedAccessType,
IndexedAccessTypeNode,
IndexFlags,
IndexInfo,
IndexKind,
indexOfNode,
Expand Down Expand Up @@ -1448,6 +1449,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
var noImplicitThis = getStrictOptionValue(compilerOptions, "noImplicitThis");
var useUnknownInCatchVariables = getStrictOptionValue(compilerOptions, "useUnknownInCatchVariables");
var keyofStringsOnly = !!compilerOptions.keyofStringsOnly;
var defaultIndexFlags = keyofStringsOnly ? IndexFlags.StringsOnly : IndexFlags.None;
var freshObjectLiteralFlag = compilerOptions.suppressExcessPropertyErrors ? 0 : ObjectFlags.FreshLiteral;
var exactOptionalPropertyTypes = compilerOptions.exactOptionalPropertyTypes;

Expand Down Expand Up @@ -14123,6 +14125,23 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return !prop.valueDeclaration && !!(getCheckFlags(prop) & CheckFlags.ContainsPrivate);
}

/**
* A union type which is reducible upon instantiation (meaning some members are removed under certain instantiations)
* must be kept generic, as that instantiation information needs to flow through the type system. By replacing all
* type parameters in the union with a special never type that is treated as a literal in `getReducedType`, we can cause
* the `getReducedType` logic to reduce the resulting type if possible (since only intersections with conflicting
* literal-typed properties are reducible).
*/
function isGenericReducibleType(type: Type): boolean {
return !!(type.flags & TypeFlags.Union && (type as UnionType).objectFlags & ObjectFlags.ContainsIntersections && some((type as UnionType).types, isGenericReducibleType) ||
type.flags & TypeFlags.Intersection && isReducibleIntersection(type as IntersectionType));
}

function isReducibleIntersection(type: IntersectionType) {
const uniqueFilled = type.uniqueLiteralFilledInstantiation || (type.uniqueLiteralFilledInstantiation = instantiateType(type, uniqueLiteralMapper));
return getReducedType(uniqueFilled) !== uniqueFilled;
}

function elaborateNeverIntersection(errorInfo: DiagnosticMessageChain | undefined, type: Type) {
if (type.flags & TypeFlags.Intersection && getObjectFlags(type) & ObjectFlags.IsNeverIntersection) {
const neverProp = find(getPropertiesOfUnionOrIntersectionType(type as IntersectionType), isDiscriminantWithNeverType);
Expand Down Expand Up @@ -16774,10 +16793,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return links.resolvedType;
}

function createIndexType(type: InstantiableType | UnionOrIntersectionType, stringsOnly: boolean) {
function createIndexType(type: InstantiableType | UnionOrIntersectionType, indexFlags: IndexFlags) {
const result = createType(TypeFlags.Index) as IndexType;
result.type = type;
result.stringsOnly = stringsOnly;
result.indexFlags = indexFlags;
return result;
}

Expand All @@ -16787,10 +16806,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return result;
}

function getIndexTypeForGenericType(type: InstantiableType | UnionOrIntersectionType, stringsOnly: boolean) {
return stringsOnly ?
type.resolvedStringIndexType || (type.resolvedStringIndexType = createIndexType(type, /*stringsOnly*/ true)) :
type.resolvedIndexType || (type.resolvedIndexType = createIndexType(type, /*stringsOnly*/ false));
function getIndexTypeForGenericType(type: InstantiableType | UnionOrIntersectionType, indexFlags: IndexFlags) {
return indexFlags & IndexFlags.StringsOnly ?
type.resolvedStringIndexType || (type.resolvedStringIndexType = createIndexType(type, IndexFlags.StringsOnly)) :
type.resolvedIndexType || (type.resolvedIndexType = createIndexType(type, IndexFlags.None));
}

/**
Expand All @@ -16800,11 +16819,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
* reduction in the constraintType) when possible.
* @param noIndexSignatures Indicates if _string_ index signatures should be elided. (other index signatures are always reported)
*/
function getIndexTypeForMappedType(type: MappedType, stringsOnly: boolean, noIndexSignatures: boolean | undefined) {
function getIndexTypeForMappedType(type: MappedType, indexFlags: IndexFlags) {
const typeParameter = getTypeParameterFromMappedType(type);
const constraintType = getConstraintTypeFromMappedType(type);
const nameType = getNameTypeFromMappedType(type.target as MappedType || type);
if (!nameType && !noIndexSignatures) {
if (!nameType && !(indexFlags & IndexFlags.NoIndexSignatures)) {
// no mapping and no filtering required, just quickly bail to returning the constraint in the common case
return constraintType;
}
Expand All @@ -16817,12 +16836,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// so we only eagerly manifest the keys if the constraint is nongeneric
if (!isGenericIndexType(constraintType)) {
const modifiersType = getApparentType(getModifiersTypeFromMappedType(type)); // The 'T' in 'keyof T'
forEachMappedTypePropertyKeyTypeAndIndexSignatureKeyType(modifiersType, TypeFlags.StringOrNumberLiteralOrUnique, stringsOnly, addMemberForKeyType);
forEachMappedTypePropertyKeyTypeAndIndexSignatureKeyType(modifiersType, TypeFlags.StringOrNumberLiteralOrUnique, !!(indexFlags & IndexFlags.StringsOnly), addMemberForKeyType);
}
else {
// we have a generic index and a homomorphic mapping (but a distributive key remapping) - we need to defer the whole `keyof whatever` for later
// since it's not safe to resolve the shape of modifier type
return getIndexTypeForGenericType(type, stringsOnly);
return getIndexTypeForGenericType(type, indexFlags);
}
}
else {
Expand All @@ -16833,7 +16852,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
// we had to pick apart the constraintType to potentially map/filter it - compare the final resulting list with the original constraintType,
// so we can return the union that preserves aliases/origin data if possible
const result = noIndexSignatures ? filterType(getUnionType(keyTypes), t => !(t.flags & (TypeFlags.Any | TypeFlags.String))) : getUnionType(keyTypes);
const result = indexFlags & IndexFlags.NoIndexSignatures ? filterType(getUnionType(keyTypes), t => !(t.flags & (TypeFlags.Any | TypeFlags.String))) : getUnionType(keyTypes);
if (result.flags & TypeFlags.Union && constraintType.flags & TypeFlags.Union && getTypeListId((result as UnionType).types) === getTypeListId((constraintType as UnionType).types)){
return constraintType;
}
Expand Down Expand Up @@ -16902,36 +16921,25 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
/*aliasSymbol*/ undefined, /*aliasTypeArguments*/ undefined, origin);
}

/**
* A union type which is reducible upon instantiation (meaning some members are removed under certain instantiations)
* must be kept generic, as that instantiation information needs to flow through the type system. By replacing all
* type parameters in the union with a special never type that is treated as a literal in `getReducedType`, we can cause the `getReducedType` logic
* to reduce the resulting type if possible (since only intersections with conflicting literal-typed properties are reducible).
*/
function isPossiblyReducibleByInstantiation(type: Type): boolean {
const uniqueFilled = getUniqueLiteralFilledInstantiation(type);
return getReducedType(uniqueFilled) !== uniqueFilled;
}

function shouldDeferIndexType(type: Type) {
function shouldDeferIndexType(type: Type, indexFlags = IndexFlags.None) {
return !!(type.flags & TypeFlags.InstantiableNonPrimitive ||
isGenericTupleType(type) ||
isGenericMappedType(type) && !hasDistributiveNameType(type) ||
type.flags & TypeFlags.Union && some((type as UnionType).types, isPossiblyReducibleByInstantiation) ||
type.flags & TypeFlags.Union && !(indexFlags & IndexFlags.NoReducibleCheck) && isGenericReducibleType(type) ||
type.flags & TypeFlags.Intersection && maybeTypeOfKind(type, TypeFlags.Instantiable) && some((type as IntersectionType).types, isEmptyAnonymousObjectType));
}

function getIndexType(type: Type, stringsOnly = keyofStringsOnly, noIndexSignatures?: boolean): Type {
function getIndexType(type: Type, indexFlags = defaultIndexFlags): Type {
type = getReducedType(type);
return shouldDeferIndexType(type) ? getIndexTypeForGenericType(type as InstantiableType | UnionOrIntersectionType, stringsOnly) :
type.flags & TypeFlags.Union ? getIntersectionType(map((type as UnionType).types, t => getIndexType(t, stringsOnly, noIndexSignatures))) :
type.flags & TypeFlags.Intersection ? getUnionType(map((type as IntersectionType).types, t => getIndexType(t, stringsOnly, noIndexSignatures))) :
getObjectFlags(type) & ObjectFlags.Mapped ? getIndexTypeForMappedType(type as MappedType, stringsOnly, noIndexSignatures) :
return shouldDeferIndexType(type, indexFlags) ? getIndexTypeForGenericType(type as InstantiableType | UnionOrIntersectionType, indexFlags) :
type.flags & TypeFlags.Union ? getIntersectionType(map((type as UnionType).types, t => getIndexType(t, indexFlags))) :
type.flags & TypeFlags.Intersection ? getUnionType(map((type as IntersectionType).types, t => getIndexType(t, indexFlags))) :
getObjectFlags(type) & ObjectFlags.Mapped ? getIndexTypeForMappedType(type as MappedType, indexFlags) :
type === wildcardType ? wildcardType :
type.flags & TypeFlags.Unknown ? neverType :
type.flags & (TypeFlags.Any | TypeFlags.Never) ? keyofConstraintType :
getLiteralTypeFromProperties(type, (noIndexSignatures ? TypeFlags.StringLiteral : TypeFlags.StringLike) | (stringsOnly ? 0 : TypeFlags.NumberLike | TypeFlags.ESSymbolLike),
stringsOnly === keyofStringsOnly && !noIndexSignatures);
getLiteralTypeFromProperties(type, (indexFlags & IndexFlags.NoIndexSignatures ? TypeFlags.StringLiteral : TypeFlags.StringLike) | (indexFlags & IndexFlags.StringsOnly ? 0 : TypeFlags.NumberLike | TypeFlags.ESSymbolLike),
indexFlags === defaultIndexFlags);
}

function getExtractStringType(type: Type) {
Expand Down Expand Up @@ -17544,6 +17552,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (objectType === wildcardType || indexType === wildcardType) {
return wildcardType;
}
objectType = getReducedType(objectType);
// If the object type has a string index signature and no other members we know that the result will
// always be the type of that index signature and we can simplify accordingly.
if (isStringIndexSignatureOnlyType(objectType) && !(indexType.flags & TypeFlags.Nullable) && isTypeAssignableToKind(indexType, TypeFlags.String | TypeFlags.Number)) {
Expand All @@ -17560,7 +17569,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// eagerly using the constraint type of 'this' at the given location.
if (isGenericIndexType(indexType) || (accessNode && accessNode.kind !== SyntaxKind.IndexedAccessType ?
isGenericTupleType(objectType) && !indexTypeLessThan(indexType, objectType.target.fixedLength) :
isGenericObjectType(objectType) && !(isTupleType(objectType) && indexTypeLessThan(indexType, objectType.target.fixedLength)))) {
isGenericObjectType(objectType) && !(isTupleType(objectType) && indexTypeLessThan(indexType, objectType.target.fixedLength)) || isGenericReducibleType(objectType))) {
if (objectType.flags & TypeFlags.AnyOrUnknown) {
return objectType;
}
Expand Down Expand Up @@ -18980,11 +18989,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return type; // Nested invocation of `inferTypeForHomomorphicMappedType` or the `source` instantiated into something unmappable
}

function getUniqueLiteralFilledInstantiation(type: Type) {
return type.flags & (TypeFlags.Primitive | TypeFlags.AnyOrUnknown | TypeFlags.Never) ? type :
type.uniqueLiteralFilledInstantiation || (type.uniqueLiteralFilledInstantiation = instantiateType(type, uniqueLiteralMapper));
}

function getPermissiveInstantiation(type: Type) {
return type.flags & (TypeFlags.Primitive | TypeFlags.AnyOrUnknown | TypeFlags.Never) ? type :
type.permissiveInstantiation || (type.permissiveInstantiation = instantiateType(type, permissiveMapper));
Expand Down Expand Up @@ -21273,7 +21277,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// false positives. For example, given 'T extends { [K in keyof T]: string }',
// 'keyof T' has itself as its constraint and produces a Ternary.Maybe when
// related to other types.
if (isRelatedTo(source, getIndexType(constraint, (target as IndexType).stringsOnly), RecursionFlags.Target, reportErrors) === Ternary.True) {
if (isRelatedTo(source, getIndexType(constraint, (target as IndexType).indexFlags | IndexFlags.NoReducibleCheck), RecursionFlags.Target, reportErrors) === Ternary.True) {
return Ternary.True;
}
}
Expand Down Expand Up @@ -21373,7 +21377,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// If target has shape `{ [P in Q]: T }`, then its keys have type `Q`.
const targetKeys = keysRemapped ? getNameTypeFromMappedType(target)! : getConstraintTypeFromMappedType(target);
// Type of the keys of source type `S`, i.e. `keyof S`.
const sourceKeys = getIndexType(source, /*stringsOnly*/ undefined, /*noIndexSignatures*/ true);
const sourceKeys = getIndexType(source, IndexFlags.NoIndexSignatures);
const includeOptional = modifiers & MappedTypeModifiers.IncludeOptional;
const filteredByApplicability = includeOptional ? intersectTypes(targetKeys, sourceKeys) : undefined;
// 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`.
Expand Down Expand Up @@ -38486,7 +38490,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// Check if the index type is assignable to 'keyof T' for the object type.
const objectType = (type as IndexedAccessType).objectType;
const indexType = (type as IndexedAccessType).indexType;
if (isTypeAssignableTo(indexType, getIndexType(objectType, /*stringsOnly*/ false))) {
if (isTypeAssignableTo(indexType, getIndexType(objectType, IndexFlags.None))) {
if (accessNode.kind === SyntaxKind.ElementAccessExpression && isAssignmentTarget(accessNode) &&
getObjectFlags(objectType) & ObjectFlags.Mapped && getMappedTypeModifiers(objectType as MappedType) & MappedTypeModifiers.IncludeReadonly) {
error(accessNode, Diagnostics.Index_signature_in_type_0_only_permits_reading, typeToString(objectType));
Expand Down
14 changes: 11 additions & 3 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6118,8 +6118,6 @@ export interface Type {
/** @internal */
restrictiveInstantiation?: Type; // Instantiation with type parameters mapped to unconstrained form
/** @internal */
uniqueLiteralFilledInstantiation?: Type; // Instantiation with type parameters mapped to never type
/** @internal */
immediateBaseConstraint?: Type; // Immediate base constraint cache
/** @internal */
widened?: Type; // Cached widened form of the type
Expand Down Expand Up @@ -6408,6 +6406,8 @@ export interface UnionType extends UnionOrIntersectionType {
export interface IntersectionType extends UnionOrIntersectionType {
/** @internal */
resolvedApparentType: Type;
/** @internal */
uniqueLiteralFilledInstantiation?: Type; // Instantiation with type parameters mapped to never type
}

export type StructuredType = ObjectType | UnionType | IntersectionType;
Expand Down Expand Up @@ -6557,11 +6557,19 @@ export interface IndexedAccessType extends InstantiableType {

export type TypeVariable = TypeParameter | IndexedAccessType;

/** @internal */
export const enum IndexFlags {
None = 0,
StringsOnly = 1 << 0,
NoIndexSignatures = 1 << 1,
NoReducibleCheck = 1 << 2,
}

// keyof T types (TypeFlags.Index)
export interface IndexType extends InstantiableType {
type: InstantiableType | UnionOrIntersectionType;
/** @internal */
stringsOnly: boolean;
indexFlags: IndexFlags;
}

export interface ConditionalRoot {
Expand Down
Loading