Skip to content

Commit

Permalink
Defer index types on homomorphic mapped types that change readonlyness
Browse files Browse the repository at this point in the history
  • Loading branch information
Andarist committed Dec 26, 2024
1 parent 56a0825 commit e4b5cc2
Show file tree
Hide file tree
Showing 4 changed files with 245 additions and 13 deletions.
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[]

0 comments on commit e4b5cc2

Please sign in to comment.