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 index types on homomorphic mapped types that change readonlyness #60858

Open
wants to merge 1 commit 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
44 changes: 31 additions & 13 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1367,6 +1367,12 @@ const enum MappedTypeModifiers {
ExcludeOptional = 1 << 3,
}

const enum MappedTypeModifierChange {
Stripped = -1,
Unchanged = 0,
Added = 1,
}

const enum MappedTypeNameTypeKind {
None,
Filtering,
Expand Down Expand Up @@ -14323,25 +14329,36 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
(declaration.questionToken ? declaration.questionToken.kind === SyntaxKind.MinusToken ? MappedTypeModifiers.ExcludeOptional : MappedTypeModifiers.IncludeOptional : 0);
}

// Return -1, 0, or 1, where -1 means optionality is stripped (i.e. -?), 0 means optionality is unchanged, and 1 means
// optionality is added (i.e. +?).
function getMappedTypeOptionality(type: MappedType): number {
function getMappedTypeOptionality(type: MappedType): MappedTypeModifierChange {
const modifiers = getMappedTypeModifiers(type);
return modifiers & MappedTypeModifiers.ExcludeOptional ? MappedTypeModifierChange.Stripped : modifiers & MappedTypeModifiers.IncludeOptional ? MappedTypeModifierChange.Added : MappedTypeModifierChange.Unchanged;
}

function getMappedTypeReadonlyness(type: MappedType): MappedTypeModifierChange {
const modifiers = getMappedTypeModifiers(type);
return modifiers & MappedTypeModifiers.ExcludeOptional ? -1 : modifiers & MappedTypeModifiers.IncludeOptional ? 1 : 0;
return modifiers & MappedTypeModifiers.ExcludeReadonly ? MappedTypeModifierChange.Stripped : modifiers & MappedTypeModifiers.IncludeReadonly ? MappedTypeModifierChange.Added : MappedTypeModifierChange.Unchanged;
}

// Return -1, 0, or 1, for stripped, unchanged, or added optionality respectively. When a homomorphic mapped type doesn't
// modify optionality, recursively consult the optionality of the type being mapped over to see if it strips or adds optionality.
// For intersections, return -1 or 1 when all constituents strip or add optionality, otherwise return 0.
function getCombinedMappedTypeOptionality(type: Type): number {
// Return -1, 0, or 1, for stripped, unchanged, or added modifier kind respectively. When a homomorphic mapped type doesn't
// modify a modifier, recursively consult the modifier change of the type being mapped over to see if it strips or adds it.
// For intersections, return -1 or 1 when all constituents strip or add a specific modifier, otherwise return 0.
function getCombinedMappedTypeModifierChange(type: Type, getMappedTypeModifierChange: (type: MappedType) => MappedTypeModifierChange): MappedTypeModifierChange {
if (getObjectFlags(type) & ObjectFlags.Mapped) {
return getMappedTypeOptionality(type as MappedType) || getCombinedMappedTypeOptionality(getModifiersTypeFromMappedType(type as MappedType));
return getMappedTypeModifierChange(type as MappedType) || getCombinedMappedTypeModifierChange(getModifiersTypeFromMappedType(type as MappedType), getMappedTypeModifierChange);
}
if (type.flags & TypeFlags.Intersection) {
const optionality = getCombinedMappedTypeOptionality((type as IntersectionType).types[0]);
return every((type as IntersectionType).types, (t, i) => i === 0 || getCombinedMappedTypeOptionality(t) === optionality) ? optionality : 0;
const modifierChange = getCombinedMappedTypeModifierChange((type as IntersectionType).types[0], getMappedTypeModifierChange);
return every((type as IntersectionType).types, (t, i) => i === 0 || getCombinedMappedTypeModifierChange(t, getMappedTypeModifierChange) === modifierChange) ? modifierChange : MappedTypeModifierChange.Unchanged;
}
return 0;
return MappedTypeModifierChange.Unchanged;
}

function getCombinedMappedTypeOptionality(type: Type): MappedTypeModifierChange {
return getCombinedMappedTypeModifierChange(type, getMappedTypeOptionality);
}

function getCombinedMappedTypeReadonlyness(type: Type): MappedTypeModifierChange {
return getCombinedMappedTypeModifierChange(type, getMappedTypeReadonlyness);
}

function isPartialMappedType(type: Type) {
Expand Down Expand Up @@ -18244,6 +18261,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const nameType = getNameTypeFromMappedType(type.target as MappedType || type);
if (!nameType && !(indexFlags & IndexFlags.NoIndexSignatures)) {
// no mapping and no filtering required, just quickly bail to returning the constraint in the common case
// andarist
return constraintType;
}
const keyTypes: Type[] = [];
Expand Down Expand Up @@ -18352,7 +18370,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) && (getHomomorphicTypeVariable(type.target as MappedType ?? type) && !getNameTypeFromMappedType(type) && getCombinedMappedTypeReadonlyness(type) || (!hasDistributiveNameType(type) || getMappedTypeNameTypeKind(type) === MappedTypeNameTypeKind.Remapping)) ||
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
//// [tests/cases/compiler/indexTypeOnHomomorphicMappedTypesWithReadonly1.ts] ////

=== indexTypeOnHomomorphicMappedTypesWithReadonly1.ts ===
type T1 = Readonly<string[]>;
>T1 : Symbol(T1, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 0, 0))
>Readonly : Symbol(Readonly, Decl(lib.es5.d.ts, --, --))

type R1 = keyof T1; // keyof readonly string[]
>R1 : Symbol(R1, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 0, 29))
>T1 : Symbol(T1, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 0, 0))

type KeyOfReadonly<T> = keyof Readonly<T>;
>KeyOfReadonly : Symbol(KeyOfReadonly, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 1, 19))
>T : Symbol(T, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 2, 19))
>Readonly : Symbol(Readonly, Decl(lib.es5.d.ts, --, --))
>T : Symbol(T, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 2, 19))

type R2 = KeyOfReadonly<string[]>; // keyof readonly string[]
>R2 : Symbol(R2, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 2, 42))
>KeyOfReadonly : Symbol(KeyOfReadonly, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 1, 19))

type Identity<T> = { [K in keyof T]: T[K] };
>Identity : Symbol(Identity, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 3, 34))
>T : Symbol(T, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 5, 14))
>K : Symbol(K, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 5, 22))
>T : Symbol(T, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 5, 14))
>T : Symbol(T, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 5, 14))
>K : Symbol(K, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 5, 22))

type KeyOfReadonly2<T> = keyof Identity<Readonly<T>>;
>KeyOfReadonly2 : Symbol(KeyOfReadonly2, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 5, 44))
>T : Symbol(T, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 7, 20))
>Identity : Symbol(Identity, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 3, 34))
>Readonly : Symbol(Readonly, Decl(lib.es5.d.ts, --, --))
>T : Symbol(T, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 7, 20))

type R3 = KeyOfReadonly2<string[]>; // keyof readonly string[]
>R3 : Symbol(R3, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 7, 53))
>KeyOfReadonly2 : Symbol(KeyOfReadonly2, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 5, 44))

type KeyOfReadonly3<T> = keyof Readonly<Identity<T>>;
>KeyOfReadonly3 : Symbol(KeyOfReadonly3, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 8, 35))
>T : Symbol(T, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 9, 20))
>Readonly : Symbol(Readonly, Decl(lib.es5.d.ts, --, --))
>Identity : Symbol(Identity, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 3, 34))
>T : Symbol(T, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 9, 20))

type R4 = KeyOfReadonly3<string[]>; // keyof readonly string[]
>R4 : Symbol(R4, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 9, 53))
>KeyOfReadonly3 : Symbol(KeyOfReadonly3, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 8, 35))

type Writable<T> = { -readonly [K in keyof T]: T[K] };
>Writable : Symbol(Writable, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 10, 35))
>T : Symbol(T, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 12, 14))
>K : Symbol(K, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 12, 32))
>T : Symbol(T, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 12, 14))
>T : Symbol(T, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 12, 14))
>K : Symbol(K, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 12, 32))

type KeyOfWritable<T> = keyof Writable<T>;
>KeyOfWritable : Symbol(KeyOfWritable, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 12, 54))
>T : Symbol(T, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 14, 19))
>Writable : Symbol(Writable, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 10, 35))
>T : Symbol(T, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 14, 19))

type R5 = KeyOfWritable<readonly string[]>; // keyof string[]
>R5 : Symbol(R5, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 14, 42))
>KeyOfWritable : Symbol(KeyOfWritable, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 12, 54))

type KeyOfWritable2<T> = keyof Writable<Readonly<T>>;
>KeyOfWritable2 : Symbol(KeyOfWritable2, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 15, 43))
>T : Symbol(T, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 16, 20))
>Writable : Symbol(Writable, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 10, 35))
>Readonly : Symbol(Readonly, Decl(lib.es5.d.ts, --, --))
>T : Symbol(T, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 16, 20))

type R6 = KeyOfWritable2<readonly string[]>; // keyof string[]
>R6 : Symbol(R6, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 16, 53))
>KeyOfWritable2 : Symbol(KeyOfWritable2, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 15, 43))

type KeyOfWritable3<T> = keyof Identity<Writable<Readonly<T>>>;
>KeyOfWritable3 : Symbol(KeyOfWritable3, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 17, 44))
>T : Symbol(T, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 18, 20))
>Identity : Symbol(Identity, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 3, 34))
>Writable : Symbol(Writable, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 10, 35))
>Readonly : Symbol(Readonly, Decl(lib.es5.d.ts, --, --))
>T : Symbol(T, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 18, 20))

type R7 = KeyOfWritable3<string[]>; // keyof string[]
>R7 : Symbol(R7, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 18, 63))
>KeyOfWritable3 : Symbol(KeyOfWritable3, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 17, 44))

type R8 = KeyOfWritable3<readonly string[]>; // keyof string[]
>R8 : Symbol(R8, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 19, 35))
>KeyOfWritable3 : Symbol(KeyOfWritable3, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 17, 44))

type KeyOfReadonly4<T> = keyof Identity<Readonly<Writable<T>>>;
>KeyOfReadonly4 : Symbol(KeyOfReadonly4, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 20, 44))
>T : Symbol(T, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 22, 20))
>Identity : Symbol(Identity, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 3, 34))
>Readonly : Symbol(Readonly, Decl(lib.es5.d.ts, --, --))
>Writable : Symbol(Writable, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 10, 35))
>T : Symbol(T, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 22, 20))

type R9 = KeyOfReadonly4<string[]>; // keyof readonly string[]
>R9 : Symbol(R9, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 22, 63))
>KeyOfReadonly4 : Symbol(KeyOfReadonly4, Decl(indexTypeOnHomomorphicMappedTypesWithReadonly1.ts, 20, 44))

Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//// [tests/cases/compiler/indexTypeOnHomomorphicMappedTypesWithReadonly1.ts] ////

=== indexTypeOnHomomorphicMappedTypesWithReadonly1.ts ===
type T1 = Readonly<string[]>;
>T1 : readonly string[]
> : ^^^^^^^^^^^^^^^^^

type R1 = keyof T1; // keyof readonly string[]
>R1 : keyof readonly string[]
> : ^^^^^^^^^^^^^^^^^^^^^^^

type KeyOfReadonly<T> = keyof Readonly<T>;
>KeyOfReadonly : keyof Readonly<T>
> : ^^^^^^^^^^^^^^^^^

type R2 = KeyOfReadonly<string[]>; // keyof readonly string[]
>R2 : keyof readonly string[]
> : ^^^^^^^^^^^^^^^^^^^^^^^

type Identity<T> = { [K in keyof T]: T[K] };
>Identity : Identity<T>
> : ^^^^^^^^^^^

type KeyOfReadonly2<T> = keyof Identity<Readonly<T>>;
>KeyOfReadonly2 : keyof Identity<Readonly<T>>
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^

type R3 = KeyOfReadonly2<string[]>; // keyof readonly string[]
>R3 : keyof readonly string[]
> : ^^^^^^^^^^^^^^^^^^^^^^^

type KeyOfReadonly3<T> = keyof Readonly<Identity<T>>;
>KeyOfReadonly3 : keyof Readonly<Identity<T>>
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^

type R4 = KeyOfReadonly3<string[]>; // keyof readonly string[]
>R4 : keyof readonly string[]
> : ^^^^^^^^^^^^^^^^^^^^^^^

type Writable<T> = { -readonly [K in keyof T]: T[K] };
>Writable : Writable<T>
> : ^^^^^^^^^^^

type KeyOfWritable<T> = keyof Writable<T>;
>KeyOfWritable : keyof Writable<T>
> : ^^^^^^^^^^^^^^^^^

type R5 = KeyOfWritable<readonly string[]>; // keyof string[]
>R5 : keyof string[]
> : ^^^^^^^^^^^^^^

type KeyOfWritable2<T> = keyof Writable<Readonly<T>>;
>KeyOfWritable2 : keyof Writable<Readonly<T>>
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^

type R6 = KeyOfWritable2<readonly string[]>; // keyof string[]
>R6 : keyof string[]
> : ^^^^^^^^^^^^^^

type KeyOfWritable3<T> = keyof Identity<Writable<Readonly<T>>>;
>KeyOfWritable3 : keyof Identity<Writable<Readonly<T>>>
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

type R7 = KeyOfWritable3<string[]>; // keyof string[]
>R7 : keyof string[]
> : ^^^^^^^^^^^^^^

type R8 = KeyOfWritable3<readonly string[]>; // keyof string[]
>R8 : keyof string[]
> : ^^^^^^^^^^^^^^

type KeyOfReadonly4<T> = keyof Identity<Readonly<Writable<T>>>;
>KeyOfReadonly4 : keyof Identity<Readonly<Writable<T>>>
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

type R9 = KeyOfReadonly4<string[]>; // keyof readonly string[]
>R9 : keyof readonly string[]
> : ^^^^^^^^^^^^^^^^^^^^^^^

Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// @strict: true
// @noEmit: true

type T1 = Readonly<string[]>;
type R1 = keyof T1; // keyof readonly string[]
type KeyOfReadonly<T> = keyof Readonly<T>;
type R2 = KeyOfReadonly<string[]>; // keyof readonly string[]

type Identity<T> = { [K in keyof T]: T[K] };

type KeyOfReadonly2<T> = keyof Identity<Readonly<T>>;
type R3 = KeyOfReadonly2<string[]>; // keyof readonly string[]
type KeyOfReadonly3<T> = keyof Readonly<Identity<T>>;
type R4 = KeyOfReadonly3<string[]>; // keyof readonly string[]

type Writable<T> = { -readonly [K in keyof T]: T[K] };

type KeyOfWritable<T> = keyof Writable<T>;
type R5 = KeyOfWritable<readonly string[]>; // keyof string[]
type KeyOfWritable2<T> = keyof Writable<Readonly<T>>;
type R6 = KeyOfWritable2<readonly string[]>; // keyof string[]
type KeyOfWritable3<T> = keyof Identity<Writable<Readonly<T>>>;
type R7 = KeyOfWritable3<string[]>; // keyof string[]
type R8 = KeyOfWritable3<readonly string[]>; // keyof string[]

type KeyOfReadonly4<T> = keyof Identity<Readonly<Writable<T>>>;
type R9 = KeyOfReadonly4<string[]>; // keyof readonly string[]
Loading