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 6 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
121 changes: 121 additions & 0 deletions tests/baselines/reference/mappedTypeAsClauseRecursiveNoCrash1.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
//// [tests/cases/conformance/types/mapped/mappedTypeAsClauseRecursiveNoCrash1.ts] ////

=== mappedTypeAsClauseRecursiveNoCrash1.ts ===
// https://github.com/microsoft/TypeScript/issues/60476

export type FlattenType<Source extends object, Target> = {
>FlattenType : Symbol(FlattenType, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 0, 0))
>Source : Symbol(Source, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 2, 24))
>Target : Symbol(Target, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 2, 46))

[Key in keyof Source as Key extends string
>Key : Symbol(Key, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 3, 3))
>Source : Symbol(Source, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 2, 24))
>Key : Symbol(Key, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 3, 3))

? Source[Key] extends object
>Source : Symbol(Source, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 2, 24))
>Key : Symbol(Key, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 3, 3))

? `${Key}.${keyof FlattenType<Source[Key], Target> & string}`
>Key : Symbol(Key, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 3, 3))
>FlattenType : Symbol(FlattenType, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 0, 0))
>Source : Symbol(Source, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 2, 24))
>Key : Symbol(Key, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 3, 3))
>Target : Symbol(Target, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 2, 46))

: Key
>Key : Symbol(Key, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 3, 3))

: never]-?: Target;
>Target : Symbol(Target, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 2, 46))

};

type FieldSelect = {
>FieldSelect : Symbol(FieldSelect, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 8, 2))

table: string;
>table : Symbol(table, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 10, 20))

field: string;
>field : Symbol(field, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 11, 16))

};

type Address = {
>Address : Symbol(Address, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 13, 2))

postCode: string;
>postCode : Symbol(postCode, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 15, 16))

description: string;
>description : Symbol(description, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 16, 19))

address: string;
>address : Symbol(address, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 17, 22))

};

type User = {
>User : Symbol(User, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 19, 2))

id: number;
>id : Symbol(id, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 21, 13))

name: string;
>name : Symbol(name, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 22, 13))

address: Address;
>address : Symbol(address, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 23, 15))
>Address : Symbol(Address, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 13, 2))

};

type FlattenedUser = FlattenType<User, FieldSelect>;
>FlattenedUser : Symbol(FlattenedUser, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 25, 2))
>FlattenType : Symbol(FlattenType, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 0, 0))
>User : Symbol(User, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 19, 2))
>FieldSelect : Symbol(FieldSelect, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 8, 2))

type FlattenedUserKeys = keyof FlattenType<User, FieldSelect>;
>FlattenedUserKeys : Symbol(FlattenedUserKeys, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 27, 52))
>FlattenType : Symbol(FlattenType, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 0, 0))
>User : Symbol(User, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 19, 2))
>FieldSelect : Symbol(FieldSelect, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 8, 2))

export type FlattenTypeKeys<Source extends object, Target> = keyof {
>FlattenTypeKeys : Symbol(FlattenTypeKeys, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 28, 62))
>Source : Symbol(Source, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 30, 28))
>Target : Symbol(Target, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 30, 50))

[Key in keyof Source as Key extends string
>Key : Symbol(Key, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 31, 3))
>Source : Symbol(Source, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 30, 28))
>Key : Symbol(Key, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 31, 3))

? Source[Key] extends object
>Source : Symbol(Source, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 30, 28))
>Key : Symbol(Key, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 31, 3))

? `${Key}.${keyof FlattenType<Source[Key], Target> & string}`
>Key : Symbol(Key, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 31, 3))
>FlattenType : Symbol(FlattenType, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 0, 0))
>Source : Symbol(Source, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 30, 28))
>Key : Symbol(Key, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 31, 3))
>Target : Symbol(Target, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 30, 50))

: Key
>Key : Symbol(Key, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 31, 3))

: never]-?: Target;
>Target : Symbol(Target, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 30, 50))

};

type FlattenedUserKeys2 = FlattenTypeKeys<User, FieldSelect>;
>FlattenedUserKeys2 : Symbol(FlattenedUserKeys2, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 36, 2))
>FlattenTypeKeys : Symbol(FlattenTypeKeys, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 28, 62))
>User : Symbol(User, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 19, 2))
>FieldSelect : Symbol(FieldSelect, Decl(mappedTypeAsClauseRecursiveNoCrash1.ts, 8, 2))

Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//// [tests/cases/conformance/types/mapped/mappedTypeAsClauseRecursiveNoCrash1.ts] ////

=== mappedTypeAsClauseRecursiveNoCrash1.ts ===
// https://github.com/microsoft/TypeScript/issues/60476

export type FlattenType<Source extends object, Target> = {
>FlattenType : FlattenType<Source, Target>
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^

[Key in keyof Source as Key extends string
? Source[Key] extends object
? `${Key}.${keyof FlattenType<Source[Key], Target> & string}`
: Key
: never]-?: Target;
};

type FieldSelect = {
>FieldSelect : FieldSelect
> : ^^^^^^^^^^^

table: string;
>table : string
> : ^^^^^^

field: string;
>field : string
> : ^^^^^^

};

type Address = {
>Address : Address
> : ^^^^^^^

postCode: string;
>postCode : string
> : ^^^^^^

description: string;
>description : string
> : ^^^^^^

address: string;
>address : string
> : ^^^^^^

};

type User = {
>User : User
> : ^^^^

id: number;
>id : number
> : ^^^^^^

name: string;
>name : string
> : ^^^^^^

address: Address;
>address : Address
> : ^^^^^^^

};

type FlattenedUser = FlattenType<User, FieldSelect>;
>FlattenedUser : FlattenType<User, FieldSelect>
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

type FlattenedUserKeys = keyof FlattenType<User, FieldSelect>;
>FlattenedUserKeys : "id" | "name" | "address.address" | "address.postCode" | "address.description"
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

export type FlattenTypeKeys<Source extends object, Target> = keyof {
>FlattenTypeKeys : keyof { [Key in keyof Source as Key extends string ? Source[Key] extends object ? `${Key}.${keyof FlattenType<Source[Key], Target> & string}` : Key : never]-?: Target; }
> : ^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

[Key in keyof Source as Key extends string
? Source[Key] extends object
? `${Key}.${keyof FlattenType<Source[Key], Target> & string}`
: Key
: never]-?: Target;
};

type FlattenedUserKeys2 = FlattenTypeKeys<User, FieldSelect>;
>FlattenedUserKeys2 : "id" | "name" | "address.address" | "address.postCode" | "address.description"
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

20 changes: 14 additions & 6 deletions tests/baselines/reference/mappedTypeConstraints2.errors.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ mappedTypeConstraints2.ts(16,11): error TS2322: Type 'Mapped3<K>[Uppercase<K>]'
Type 'Mapped3<K>[Uppercase<string>]' is not assignable to type '{ a: K; }'.
Type 'Mapped3<K>[string]' is not assignable to type '{ a: K; }'.
mappedTypeConstraints2.ts(42,7): error TS2322: Type 'Mapped6<K>[keyof Mapped6<K>]' is not assignable to type '`_${string}`'.
Type 'Mapped6<K>[string] | Mapped6<K>[number] | Mapped6<K>[symbol]' is not assignable to type '`_${string}`'.
Type 'Mapped6<K>[string]' is not assignable to type '`_${string}`'.
mappedTypeConstraints2.ts(51,57): error TS2322: Type 'Foo<T>[`get${T}`]' is not assignable to type 'T'.
Type 'Mapped6<K>[`_${K}`]' is not assignable to type '`_${string}`'.
Type 'Mapped6<K>[`_${string}`]' is not assignable to type '`_${string}`'.
mappedTypeConstraints2.ts(59,57): error TS2322: Type 'Foo<T>[`get${T}`]' is not assignable to type 'T'.
'T' could be instantiated with an arbitrary type which could be unrelated to 'Foo<T>[`get${T}`]'.
mappedTypeConstraints2.ts(82,9): error TS2322: Type 'ObjectWithUnderscoredKeys<K>[`_${K}`]' is not assignable to type 'true'.
mappedTypeConstraints2.ts(90,9): error TS2322: Type 'ObjectWithUnderscoredKeys<K>[`_${K}`]' is not assignable to type 'true'.
Type 'ObjectWithUnderscoredKeys<K>[`_${string}`]' is not assignable to type 'true'.


Expand Down Expand Up @@ -64,8 +64,16 @@ mappedTypeConstraints2.ts(82,9): error TS2322: Type 'ObjectWithUnderscoredKeys<K
let s: `_${string}` = obj[key]; // Error
~
!!! error TS2322: Type 'Mapped6<K>[keyof Mapped6<K>]' is not assignable to type '`_${string}`'.
!!! error TS2322: Type 'Mapped6<K>[string] | Mapped6<K>[number] | Mapped6<K>[symbol]' is not assignable to type '`_${string}`'.
!!! error TS2322: Type 'Mapped6<K>[string]' is not assignable to type '`_${string}`'.
!!! error TS2322: Type 'Mapped6<K>[`_${K}`]' is not assignable to type '`_${string}`'.
!!! error TS2322: Type 'Mapped6<K>[`_${string}`]' is not assignable to type '`_${string}`'.
Comment on lines +67 to +68
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I believe this change is a fix. The new error matches what was reported by TS 5.0: TS playground

}

type Mapped7<K extends string> = {
[P in K as [P] extends [`_${string}`] ? P : never]: P;
};

function f7<K extends string>(obj: Mapped7<K>, key: keyof Mapped7<K>) {
let s: `_${string}` = obj[key];
}

// Repro from #47794
Expand Down
15 changes: 15 additions & 0 deletions tests/baselines/reference/mappedTypeConstraints2.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ function f6<K extends string>(obj: Mapped6<K>, key: keyof Mapped6<K>) {
let s: `_${string}` = obj[key]; // Error
}

type Mapped7<K extends string> = {
[P in K as [P] extends [`_${string}`] ? P : never]: P;
};

function f7<K extends string>(obj: Mapped7<K>, key: keyof Mapped7<K>) {
let s: `_${string}` = obj[key];
}

// Repro from #47794

type Foo<T extends string> = {
Expand Down Expand Up @@ -106,6 +114,9 @@ function f5(obj, key) {
function f6(obj, key) {
let s = obj[key]; // Error
}
function f7(obj, key) {
let s = obj[key];
}
const get = (t, foo) => foo[`get${t}`]; // Type 'Foo<T>[`get${T}`]' is not assignable to type 'T'
function validate(obj, bounds) {
for (const [key, val] of Object.entries(obj)) {
Expand Down Expand Up @@ -154,6 +165,10 @@ type Mapped6<K extends string> = {
[P in K as `_${P}`]: P;
};
declare function f6<K extends string>(obj: Mapped6<K>, key: keyof Mapped6<K>): void;
type Mapped7<K extends string> = {
[P in K as [P] extends [`_${string}`] ? P : never]: P;
};
declare function f7<K extends string>(obj: Mapped7<K>, key: keyof Mapped7<K>): void;
type Foo<T extends string> = {
[RemappedT in T as `get${RemappedT}`]: RemappedT;
};
Expand Down
Loading