Skip to content

Commit

Permalink
Defer resolution of indexed access types with reducible object types (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
ahejlsberg committed Mar 14, 2023
1 parent ae1b3db commit 9769421
Show file tree
Hide file tree
Showing 6 changed files with 352 additions and 53 deletions.
84 changes: 44 additions & 40 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,7 @@ import {
ImportTypeNode,
IndexedAccessType,
IndexedAccessTypeNode,
IndexFlags,
IndexInfo,
IndexKind,
indexOfNode,
Expand Down Expand Up @@ -1451,6 +1452,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 @@ -14155,6 +14157,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 @@ -16810,10 +16829,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 @@ -16823,10 +16842,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 @@ -16836,11 +16855,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 @@ -16853,12 +16872,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 @@ -16869,7 +16888,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 @@ -16938,36 +16957,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 @@ -17580,6 +17588,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 @@ -17596,7 +17605,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 @@ -19016,11 +19025,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 @@ -21299,7 +21303,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 @@ -21399,7 +21403,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 @@ -38526,7 +38530,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 @@ -6152,8 +6152,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 @@ -6442,6 +6440,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 @@ -6591,11 +6591,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

0 comments on commit 9769421

Please sign in to comment.