Skip to content

Fixed crash related to index type deferral on generic mapped types with name types #60528

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

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
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
43 changes: 17 additions & 26 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14749,6 +14749,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
undefined;
}
if (t.flags & TypeFlags.Index) {
if (isGenericMappedType((t as IndexType).type)) {
const mappedType = (t as IndexType).type as MappedType;
if (getNameTypeFromMappedType(mappedType) && !isMappedTypeWithKeyofConstraintDeclaration(mappedType)) {
return getBaseConstraint(getIndexTypeForMappedType(mappedType, IndexFlags.None));
}
}
return stringNumberSymbolType;
}
if (t.flags & TypeFlags.TemplateLiteral) {
Expand Down Expand Up @@ -18250,7 +18256,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// a circular definition. For this reason, we only eagerly manifest the keys if the constraint is non-generic.
if (isGenericIndexType(constraintType)) {
if (isMappedTypeWithKeyofConstraintDeclaration(type)) {
// We have a generic index and a homomorphic mapping (but a distributive key remapping) - we need to defer
// We have a generic index and a homomorphic mapping and a 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, indexFlags);
}
Expand Down Expand Up @@ -18280,25 +18286,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
}

// Ordinarily we reduce a keyof M, where M is a mapped type { [P in K as N<P>]: X }, to simply N<K>. This however presumes
// that N distributes over union types, i.e. that N<A | B | C> is equivalent to N<A> | N<B> | N<C>. Specifically, we only
// want to perform the reduction when the name type of a mapped type is distributive with respect to the type variable
// introduced by the 'in' clause of the mapped type. Note that non-generic types are considered to be distributive because
// they're the same type regardless of what's being distributed over.
function hasDistributiveNameType(mappedType: MappedType) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, maybe this could be kept but repurposed slightly. It would have to check if used template literal types don't refer to the type variable introduced by 'in' clause more than once. I don't think this is something the compiler would usually do in any other place so I'm hesitant to say that this would be a better solution.

const typeVariable = getTypeParameterFromMappedType(mappedType);
return isDistributive(getNameTypeFromMappedType(mappedType) || typeVariable);
function isDistributive(type: Type): boolean {
return type.flags & (TypeFlags.AnyOrUnknown | TypeFlags.Primitive | TypeFlags.Never | TypeFlags.TypeParameter | TypeFlags.Object | TypeFlags.NonPrimitive) ? true :
type.flags & TypeFlags.Conditional ? (type as ConditionalType).root.isDistributive && (type as ConditionalType).checkType === typeVariable :
type.flags & (TypeFlags.UnionOrIntersection | TypeFlags.TemplateLiteral) ? every((type as UnionOrIntersectionType | TemplateLiteralType).types, isDistributive) :
type.flags & TypeFlags.IndexedAccess ? isDistributive((type as IndexedAccessType).objectType) && isDistributive((type as IndexedAccessType).indexType) :
type.flags & TypeFlags.Substitution ? isDistributive((type as SubstitutionType).baseType) && isDistributive((type as SubstitutionType).constraint) :
type.flags & TypeFlags.StringMapping ? isDistributive((type as StringMappingType).type) :
false;
}
}

function getLiteralTypeFromPropertyName(name: PropertyName | JsxAttributeName) {
if (isPrivateIdentifier(name)) {
return neverType;
Expand Down Expand Up @@ -18350,7 +18337,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
function shouldDeferIndexType(type: Type, indexFlags = IndexFlags.None) {
return !!(type.flags & TypeFlags.InstantiableNonPrimitive ||
isGenericTupleType(type) ||
isGenericMappedType(type) && (!hasDistributiveNameType(type) || getMappedTypeNameTypeKind(type) === MappedTypeNameTypeKind.Remapping) ||
isGenericMappedType(type) && getNameTypeFromMappedType(type) ||
type.flags & TypeFlags.Union && !(indexFlags & IndexFlags.NoReducibleCheck) && isGenericReducibleType(type) ||
type.flags & TypeFlags.Intersection && maybeTypeOfKind(type, TypeFlags.Instantiable) && some((type as IntersectionType).types, isEmptyAnonymousObjectType));
}
Expand Down Expand Up @@ -18871,6 +18858,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
function getSimplifiedType(type: Type, writing: boolean): Type {
return type.flags & TypeFlags.IndexedAccess ? getSimplifiedIndexedAccessType(type as IndexedAccessType, writing) :
type.flags & TypeFlags.Conditional ? getSimplifiedConditionalType(type as ConditionalType, writing) :
type.flags & TypeFlags.Index ? getSimplifiedIndexType(type as IndexType) :
type;
}

Expand Down Expand Up @@ -18970,6 +18958,13 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return type;
}

function getSimplifiedIndexType(type: IndexType) {
if (isGenericMappedType(type.type) && getNameTypeFromMappedType(type.type) && !isMappedTypeWithKeyofConstraintDeclaration(type.type)) {
return getIndexTypeForMappedType(type.type, IndexFlags.None);
}
return type;
}

/**
* Invokes union simplification logic to determine if an intersection is considered empty as a union constituent
*/
Expand Down Expand Up @@ -42086,12 +42081,8 @@ 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;
// skip index type deferral on remapping mapped types
const objectIndexType = isGenericMappedType(objectType) && getMappedTypeNameTypeKind(objectType) === MappedTypeNameTypeKind.Remapping
? getIndexTypeForMappedType(objectType, IndexFlags.None)
: getIndexType(objectType, IndexFlags.None);
Comment on lines -42089 to -42092
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reverts my own change from #55140 . I think now the check wasn't exhaustive anyway and this is now better handled by getSimplifiedIndexType

const hasNumberIndexInfo = !!getIndexInfoOfType(objectType, numberType);
if (everyType(indexType, t => isTypeAssignableTo(t, objectIndexType) || hasNumberIndexInfo && isApplicableIndexType(t, numberType))) {
if (everyType(indexType, t => isTypeAssignableTo(t, getIndexType(objectType, IndexFlags.None)) || hasNumberIndexInfo && isApplicableIndexType(t, numberType))) {
if (
accessNode.kind === SyntaxKind.ElementAccessExpression && isAssignmentTarget(accessNode) &&
getObjectFlags(objectType) & ObjectFlags.Mapped && getMappedTypeModifiers(objectType as MappedType) & MappedTypeModifiers.IncludeReadonly
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6340,7 +6340,7 @@ export const enum TypeFlags {
/** @internal */
ObjectFlagsType = Any | Nullable | Never | Object | Union | Intersection,
/** @internal */
Simplifiable = IndexedAccess | Conditional,
Simplifiable = IndexedAccess | Conditional | Index,
/** @internal */
Singleton = Any | Unknown | String | Number | Boolean | BigInt | ESSymbol | Void | Undefined | Null | Never | NonPrimitive,
// 'Narrowable' types are types where narrowing actually narrows.
Expand Down
80 changes: 80 additions & 0 deletions tests/baselines/reference/keyRemappingKeyofResult.errors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
keyRemappingKeyofResult.ts(69,5): error TS2322: Type 'string' is not assignable to type 'keyof Remapped'.
Type '"whatever"' is not assignable to type 'unique symbol | "str" | DistributiveNonIndex<K>'.


==== keyRemappingKeyofResult.ts (1 errors) ====
const sym = Symbol("")
type Orig = { [k: string]: any, str: any, [sym]: any }

type Okay = Exclude<keyof Orig, never>
// type Okay = string | number | typeof sym

type Remapped = { [K in keyof Orig as {} extends Record<K, any> ? never : K]: any }
/* type Remapped = {
str: any;
[sym]: any;
} */
// no string index signature, right?

type Oops = Exclude<keyof Remapped, never>
declare let x: Oops;
x = sym;
x = "str";
// type Oops = typeof sym <-- what happened to "str"?

// equivalently, with an unresolved generic (no `exclude` shenanigans, since conditions won't execute):
function f<T>() {
type Orig = { [k: string]: any, str: any, [sym]: any } & T;

type Okay = keyof Orig;
let a: Okay;
a = "str";
a = sym;
a = "whatever";
// type Okay = string | number | typeof sym

type Remapped = { [K in keyof Orig as {} extends Record<K, any> ? never : K]: any }
/* type Remapped = {
str: any;
[sym]: any;
} */
// no string index signature, right?

type Oops = keyof Remapped;
let x: Oops;
x = sym;
x = "str";
}

// and another generic case with a _distributive_ mapping, to trigger a different branch in `getIndexType`
function g<T>() {
type Orig = { [k: string]: any, str: any, [sym]: any } & T;

type Okay = keyof Orig;
let a: Okay;
a = "str";
a = sym;
a = "whatever";
// type Okay = string | number | typeof sym

type NonIndex<T extends PropertyKey> = {} extends Record<T, any> ? never : T;
type DistributiveNonIndex<T extends PropertyKey> = T extends unknown ? NonIndex<T> : never;

type Remapped = { [K in keyof Orig as DistributiveNonIndex<K>]: any }
/* type Remapped = {
str: any;
[sym]: any;
} */
// no string index signature, right?

type Oops = keyof Remapped;
let x: Oops;
x = sym;
x = "str";
x = "whatever"; // error
~
!!! error TS2322: Type 'string' is not assignable to type 'keyof Remapped'.
!!! error TS2322: Type '"whatever"' is not assignable to type 'unique symbol | "str" | DistributiveNonIndex<K>'.
}

export {};
2 changes: 2 additions & 0 deletions tests/baselines/reference/keyRemappingKeyofResult.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ function g<T>() {
let x: Oops;
x = sym;
x = "str";
x = "whatever"; // error
}

export {};
Expand Down Expand Up @@ -97,5 +98,6 @@ function g() {
let x;
x = sym;
x = "str";
x = "whatever"; // error
}
export {};
3 changes: 3 additions & 0 deletions tests/baselines/reference/keyRemappingKeyofResult.symbols
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,9 @@ function g<T>() {

x = "str";
>x : Symbol(x, Decl(keyRemappingKeyofResult.ts, 65, 7))

x = "whatever"; // error
>x : Symbol(x, Decl(keyRemappingKeyofResult.ts, 65, 7))
}

export {};
14 changes: 14 additions & 0 deletions tests/baselines/reference/keyRemappingKeyofResult.types
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ type Orig = { [k: string]: any, str: any, [sym]: any }
>k : string
> : ^^^^^^
>str : any
> : ^^^
>[sym] : any
> : ^^^
>sym : unique symbol
> : ^^^^^^^^^^^^^

Expand Down Expand Up @@ -74,7 +76,9 @@ function f<T>() {
>k : string
> : ^^^^^^
>str : any
> : ^^^
>[sym] : any
> : ^^^
>sym : unique symbol
> : ^^^^^^^^^^^^^

Expand Down Expand Up @@ -158,7 +162,9 @@ function g<T>() {
>k : string
> : ^^^^^^
>str : any
> : ^^^
>[sym] : any
> : ^^^
>sym : unique symbol
> : ^^^^^^^^^^^^^

Expand Down Expand Up @@ -237,6 +243,14 @@ function g<T>() {
> : ^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>"str" : "str"
> : ^^^^^

x = "whatever"; // error
>x = "whatever" : "whatever"
> : ^^^^^^^^^^
>x : keyof { [K in keyof ({ [k: string]: any; str: any; [sym]: any; } & T) as K extends unknown ? {} extends Record<K, any> ? never : K : never]: any; }
> : ^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>"whatever" : "whatever"
> : ^^^^^^^^^^
}

export {};
84 changes: 84 additions & 0 deletions tests/baselines/reference/keyRemappingKeyofResult2.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//// [tests/cases/compiler/keyRemappingKeyofResult2.ts] ////

=== keyRemappingKeyofResult2.ts ===
// https://github.com/microsoft/TypeScript/issues/56239

type Values<T> = T[keyof T];
>Values : Symbol(Values, Decl(keyRemappingKeyofResult2.ts, 0, 0))
>T : Symbol(T, Decl(keyRemappingKeyofResult2.ts, 2, 12))
>T : Symbol(T, Decl(keyRemappingKeyofResult2.ts, 2, 12))
>T : Symbol(T, Decl(keyRemappingKeyofResult2.ts, 2, 12))

type ProvidedActor = {
>ProvidedActor : Symbol(ProvidedActor, Decl(keyRemappingKeyofResult2.ts, 2, 28))

src: string;
>src : Symbol(src, Decl(keyRemappingKeyofResult2.ts, 4, 22))

logic: unknown;
>logic : Symbol(logic, Decl(keyRemappingKeyofResult2.ts, 5, 14))

};

interface StateMachineConfig<TActors extends ProvidedActor> {
>StateMachineConfig : Symbol(StateMachineConfig, Decl(keyRemappingKeyofResult2.ts, 7, 2))
>TActors : Symbol(TActors, Decl(keyRemappingKeyofResult2.ts, 9, 29))
>ProvidedActor : Symbol(ProvidedActor, Decl(keyRemappingKeyofResult2.ts, 2, 28))

invoke: {
>invoke : Symbol(StateMachineConfig.invoke, Decl(keyRemappingKeyofResult2.ts, 9, 61))

src: TActors["src"];
>src : Symbol(src, Decl(keyRemappingKeyofResult2.ts, 10, 11))
>TActors : Symbol(TActors, Decl(keyRemappingKeyofResult2.ts, 9, 29))

};
}

declare function setup<TActors extends Record<string, unknown>>(_: {
>setup : Symbol(setup, Decl(keyRemappingKeyofResult2.ts, 13, 1))
>TActors : Symbol(TActors, Decl(keyRemappingKeyofResult2.ts, 15, 23))
>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --))
>_ : Symbol(_, Decl(keyRemappingKeyofResult2.ts, 15, 64))

actors: {
>actors : Symbol(actors, Decl(keyRemappingKeyofResult2.ts, 15, 68))

[K in keyof TActors]: TActors[K];
>K : Symbol(K, Decl(keyRemappingKeyofResult2.ts, 17, 5))
>TActors : Symbol(TActors, Decl(keyRemappingKeyofResult2.ts, 15, 23))
>TActors : Symbol(TActors, Decl(keyRemappingKeyofResult2.ts, 15, 23))
>K : Symbol(K, Decl(keyRemappingKeyofResult2.ts, 17, 5))

};
}): {
createMachine: (
>createMachine : Symbol(createMachine, Decl(keyRemappingKeyofResult2.ts, 19, 5))

config: StateMachineConfig<
>config : Symbol(config, Decl(keyRemappingKeyofResult2.ts, 20, 18))
>StateMachineConfig : Symbol(StateMachineConfig, Decl(keyRemappingKeyofResult2.ts, 7, 2))

Values<{
>Values : Symbol(Values, Decl(keyRemappingKeyofResult2.ts, 0, 0))

[K in keyof TActors as K & string]: {
>K : Symbol(K, Decl(keyRemappingKeyofResult2.ts, 23, 9))
>TActors : Symbol(TActors, Decl(keyRemappingKeyofResult2.ts, 15, 23))
>K : Symbol(K, Decl(keyRemappingKeyofResult2.ts, 23, 9))

src: K;
>src : Symbol(src, Decl(keyRemappingKeyofResult2.ts, 23, 45))
>K : Symbol(K, Decl(keyRemappingKeyofResult2.ts, 23, 9))

logic: TActors[K];
>logic : Symbol(logic, Decl(keyRemappingKeyofResult2.ts, 24, 17))
>TActors : Symbol(TActors, Decl(keyRemappingKeyofResult2.ts, 15, 23))
>K : Symbol(K, Decl(keyRemappingKeyofResult2.ts, 23, 9))

};
}>
>,
) => void;
};

Loading