Skip to content

Fixed const reverse mapped types themselves to be treated as const #55794

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

Merged
merged 3 commits into from
Nov 27, 2023
Merged
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
6 changes: 6 additions & 0 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14055,13 +14055,19 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return hasNonCircularBaseConstraint(typeParameter) ? getConstraintFromTypeParameter(typeParameter) : undefined;
}

function isConstMappedType(type: MappedType, depth: number): boolean {
const typeVariable = getHomomorphicTypeVariable(type);
return !!typeVariable && isConstTypeVariable(typeVariable, depth);
}

function isConstTypeVariable(type: Type | undefined, depth = 0): boolean {
return depth < 5 && !!(type && (
type.flags & TypeFlags.TypeParameter && some((type as TypeParameter).symbol?.declarations, d => hasSyntacticModifier(d, ModifierFlags.Const)) ||
type.flags & TypeFlags.UnionOrIntersection && some((type as UnionOrIntersectionType).types, t => isConstTypeVariable(t, depth)) ||
type.flags & TypeFlags.IndexedAccess && isConstTypeVariable((type as IndexedAccessType).objectType, depth + 1) ||
type.flags & TypeFlags.Conditional && isConstTypeVariable(getConstraintOfConditionalType(type as ConditionalType), depth + 1) ||
type.flags & TypeFlags.Substitution && isConstTypeVariable((type as SubstitutionType).baseType, depth) ||
getObjectFlags(type) & ObjectFlags.Mapped && isConstMappedType(type as MappedType, depth) ||
isGenericTupleType(type) && findIndex(getElementTypes(type), (t, i) => !!(type.target.elementFlags[i] & ElementFlags.Variadic) && isConstTypeVariable(t, depth)) >= 0
));
}
Expand Down
4 changes: 2 additions & 2 deletions tests/baselines/reference/typeParameterConstModifiers.types
Original file line number Diff line number Diff line change
Expand Up @@ -402,8 +402,8 @@ const thingMapped = <const O extends Record<string, any>>(o: NotEmptyMapped<O>)
>o : NotEmptyMapped<O>

const tMapped = thingMapped({ foo: '' }); // { foo: "" }
>tMapped : { foo: ""; }
>thingMapped({ foo: '' }) : { foo: ""; }
>tMapped : { readonly foo: ""; }
>thingMapped({ foo: '' }) : { readonly foo: ""; }
>thingMapped : <const O extends Record<string, any>>(o: NotEmptyMapped<O>) => NotEmptyMapped<O>
>{ foo: '' } : { foo: ""; }
>foo : ""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
//// [tests/cases/conformance/types/typeParameters/typeParameterLists/typeParameterConstModifiersReverseMappedTypes.ts] ////

=== typeParameterConstModifiersReverseMappedTypes.ts ===
declare function test1<const T>(obj: {
>test1 : Symbol(test1, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 0, 0))
>T : Symbol(T, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 0, 23))
>obj : Symbol(obj, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 0, 32))

[K in keyof T]: T[K];
>K : Symbol(K, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 1, 3))
>T : Symbol(T, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 0, 23))
>T : Symbol(T, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 0, 23))
>K : Symbol(K, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 1, 3))

}): [T, typeof obj];
>T : Symbol(T, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 0, 23))
>obj : Symbol(obj, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 0, 32))

const result1 = test1({
>result1 : Symbol(result1, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 4, 5))
>test1 : Symbol(test1, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 0, 0))

prop: "foo",
>prop : Symbol(prop, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 4, 23))

nested: {
>nested : Symbol(nested, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 5, 14))

nestedProp: "bar",
>nestedProp : Symbol(nestedProp, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 6, 11))

},
});

declare function test2<const T>(obj: {
>test2 : Symbol(test2, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 9, 3))
>T : Symbol(T, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 11, 23))
>obj : Symbol(obj, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 11, 32))

readonly [K in keyof T]: T[K];
>K : Symbol(K, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 12, 12))
>T : Symbol(T, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 11, 23))
>T : Symbol(T, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 11, 23))
>K : Symbol(K, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 12, 12))

}): [T, typeof obj];
>T : Symbol(T, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 11, 23))
>obj : Symbol(obj, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 11, 32))

const result2 = test2({
>result2 : Symbol(result2, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 15, 5))
>test2 : Symbol(test2, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 9, 3))

prop: "foo",
>prop : Symbol(prop, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 15, 23))

nested: {
>nested : Symbol(nested, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 16, 14))

nestedProp: "bar",
>nestedProp : Symbol(nestedProp, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 17, 11))

},
});

declare function test3<const T>(obj: {
>test3 : Symbol(test3, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 20, 3))
>T : Symbol(T, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 22, 23))
>obj : Symbol(obj, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 22, 32))

-readonly [K in keyof T]: T[K];
>K : Symbol(K, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 23, 13))
>T : Symbol(T, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 22, 23))
>T : Symbol(T, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 22, 23))
>K : Symbol(K, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 23, 13))

}): [T, typeof obj];
>T : Symbol(T, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 22, 23))
>obj : Symbol(obj, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 22, 32))

const result3 = test3({
>result3 : Symbol(result3, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 26, 5))
>test3 : Symbol(test3, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 20, 3))

prop: "foo",
>prop : Symbol(prop, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 26, 23))

nested: {
>nested : Symbol(nested, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 27, 14))

nestedProp: "bar",
>nestedProp : Symbol(nestedProp, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 28, 11))

},
});

declare function test4<const T extends readonly unknown[]>(arr: {
>test4 : Symbol(test4, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 31, 3))
>T : Symbol(T, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 33, 23))
>arr : Symbol(arr, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 33, 59))

[K in keyof T]: T[K];
>K : Symbol(K, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 34, 3))
>T : Symbol(T, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 33, 23))
>T : Symbol(T, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 33, 23))
>K : Symbol(K, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 34, 3))

}): T;
>T : Symbol(T, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 33, 23))

const result4 = test4(["1", 2]);
>result4 : Symbol(result4, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 37, 5))
>test4 : Symbol(test4, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 31, 3))

declare function test5<const T extends readonly unknown[]>(
>test5 : Symbol(test5, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 37, 32))
>T : Symbol(T, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 39, 23))

...args: {
>args : Symbol(args, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 39, 59))

[K in keyof T]: T[K];
>K : Symbol(K, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 41, 5))
>T : Symbol(T, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 39, 23))
>T : Symbol(T, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 39, 23))
>K : Symbol(K, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 41, 5))
}
): T;
>T : Symbol(T, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 39, 23))

const result5 = test5({ a: "foo" });
>result5 : Symbol(result5, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 45, 5))
>test5 : Symbol(test5, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 37, 32))
>a : Symbol(a, Decl(typeParameterConstModifiersReverseMappedTypes.ts, 45, 23))

Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
//// [tests/cases/conformance/types/typeParameters/typeParameterLists/typeParameterConstModifiersReverseMappedTypes.ts] ////

=== typeParameterConstModifiersReverseMappedTypes.ts ===
declare function test1<const T>(obj: {
>test1 : <const T>(obj: { [K in keyof T]: T[K]; }) => [T, typeof obj]
>obj : { [K in keyof T]: T[K]; }

[K in keyof T]: T[K];
}): [T, typeof obj];
>obj : { [K in keyof T]: T[K]; }

const result1 = test1({
>result1 : [{ readonly prop: "foo"; readonly nested: { readonly nestedProp: "bar"; }; }, { readonly prop: "foo"; readonly nested: { readonly nestedProp: "bar"; }; }]
>test1({ prop: "foo", nested: { nestedProp: "bar", },}) : [{ readonly prop: "foo"; readonly nested: { readonly nestedProp: "bar"; }; }, { readonly prop: "foo"; readonly nested: { readonly nestedProp: "bar"; }; }]
>test1 : <const T>(obj: { [K in keyof T]: T[K]; }) => [T, { [K in keyof T]: T[K]; }]
>{ prop: "foo", nested: { nestedProp: "bar", },} : { prop: "foo"; nested: { nestedProp: "bar"; }; }

prop: "foo",
>prop : "foo"
>"foo" : "foo"

nested: {
>nested : { nestedProp: "bar"; }
>{ nestedProp: "bar", } : { nestedProp: "bar"; }

nestedProp: "bar",
>nestedProp : "bar"
>"bar" : "bar"

},
});

declare function test2<const T>(obj: {
>test2 : <const T>(obj: { readonly [K in keyof T]: T[K]; }) => [T, typeof obj]
>obj : { readonly [K in keyof T]: T[K]; }

readonly [K in keyof T]: T[K];
}): [T, typeof obj];
>obj : { readonly [K in keyof T]: T[K]; }

const result2 = test2({
>result2 : [{ prop: "foo"; nested: { readonly nestedProp: "bar"; }; }, { readonly prop: "foo"; readonly nested: { readonly nestedProp: "bar"; }; }]
>test2({ prop: "foo", nested: { nestedProp: "bar", },}) : [{ prop: "foo"; nested: { readonly nestedProp: "bar"; }; }, { readonly prop: "foo"; readonly nested: { readonly nestedProp: "bar"; }; }]
>test2 : <const T>(obj: { readonly [K in keyof T]: T[K]; }) => [T, { readonly [K in keyof T]: T[K]; }]
>{ prop: "foo", nested: { nestedProp: "bar", },} : { prop: "foo"; nested: { nestedProp: "bar"; }; }

prop: "foo",
>prop : "foo"
>"foo" : "foo"

nested: {
>nested : { nestedProp: "bar"; }
>{ nestedProp: "bar", } : { nestedProp: "bar"; }

nestedProp: "bar",
>nestedProp : "bar"
>"bar" : "bar"

},
});

declare function test3<const T>(obj: {
>test3 : <const T>(obj: { -readonly [K in keyof T]: T[K]; }) => [T, typeof obj]
>obj : { -readonly [K in keyof T]: T[K]; }

-readonly [K in keyof T]: T[K];
}): [T, typeof obj];
>obj : { -readonly [K in keyof T]: T[K]; }

const result3 = test3({
>result3 : [{ readonly prop: "foo"; readonly nested: { readonly nestedProp: "bar"; }; }, { prop: "foo"; nested: { readonly nestedProp: "bar"; }; }]
>test3({ prop: "foo", nested: { nestedProp: "bar", },}) : [{ readonly prop: "foo"; readonly nested: { readonly nestedProp: "bar"; }; }, { prop: "foo"; nested: { readonly nestedProp: "bar"; }; }]
>test3 : <const T>(obj: { -readonly [K in keyof T]: T[K]; }) => [T, { -readonly [K in keyof T]: T[K]; }]
>{ prop: "foo", nested: { nestedProp: "bar", },} : { prop: "foo"; nested: { nestedProp: "bar"; }; }

prop: "foo",
>prop : "foo"
>"foo" : "foo"

nested: {
>nested : { nestedProp: "bar"; }
>{ nestedProp: "bar", } : { nestedProp: "bar"; }

nestedProp: "bar",
>nestedProp : "bar"
>"bar" : "bar"

},
});

declare function test4<const T extends readonly unknown[]>(arr: {
>test4 : <const T extends readonly unknown[]>(arr: { [K in keyof T]: T[K]; }) => T
>arr : { [K in keyof T]: T[K]; }

[K in keyof T]: T[K];
}): T;

const result4 = test4(["1", 2]);
>result4 : readonly ["1", 2]
>test4(["1", 2]) : readonly ["1", 2]
>test4 : <const T extends readonly unknown[]>(arr: { [K in keyof T]: T[K]; }) => T
>["1", 2] : ["1", 2]
>"1" : "1"
>2 : 2

declare function test5<const T extends readonly unknown[]>(
>test5 : <const T extends readonly unknown[]>(...args: { [K in keyof T]: T[K]; }) => T

...args: {
>args : { [K in keyof T]: T[K]; }

[K in keyof T]: T[K];
}
): T;

const result5 = test5({ a: "foo" });
>result5 : readonly [{ readonly a: "foo"; }]
>test5({ a: "foo" }) : readonly [{ readonly a: "foo"; }]
>test5 : <const T extends readonly unknown[]>(...args: { [K in keyof T]: T[K]; }) => T
>{ a: "foo" } : { a: "foo"; }
>a : "foo"
>"foo" : "foo"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There are also some other test cases that I'd like to add here but I'm not entirely sure what should be the expected results for them

  1. const T, P extends keyof T - this might iterate over P which might be the subset of keyof T. Should we still assign constness to T in such a case? See the playground TS playground. Note that it's not really possible to gather P across from different mapped types and somehow different mapped type to build up a "combined" reverse mapped type: TS playground
  2. K in keyof T | "extra" - what should happen with a non-homomorphic case like this? TS playground

Copy link
Member

Choose a reason for hiding this comment

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

  1. Probably? I'm not a huge fan of overuse of const type variables, but if they're in-use, there a strong signal that const-ness is desired somewhere related to that type parameter. The workflow is usually "does this do what I want? No? Add a const.", so the const normally only gets added by people if it's meant to be meaningful to inference.
  2. Eh, I'd leave that as whatever it falls out as from being not homomorphic. It'll get unified if and when we ever adjust the logic to consider something like that as homomorphic-enough to get the homomorphic type variable treatment.

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

declare function test1<const T>(obj: {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The first 3 test cases are testing objects and how readonly, -readonly, and lack of them impact the T and the fresh object that is created by mapping over T.

Those objects today would be constified for the most part but their properties wouldn't always be marked as readonly when they should be. When readonly modifier is used on the mapped type then that result (typeof obj) has readonly properties but the const type parameter itself doesn't have them. This is consistent with #12589 . You might also want to recheck when/how readonly is preserved/stripped on members of the reverse mapped type here

[K in keyof T]: T[K];
}): [T, typeof obj];

const result1 = test1({
prop: "foo",
nested: {
nestedProp: "bar",
},
});

declare function test2<const T>(obj: {
readonly [K in keyof T]: T[K];
}): [T, typeof obj];

const result2 = test2({
prop: "foo",
nested: {
nestedProp: "bar",
},
});

declare function test3<const T>(obj: {
-readonly [K in keyof T]: T[K];
}): [T, typeof obj];

const result3 = test3({
prop: "foo",
nested: {
nestedProp: "bar",
},
});

declare function test4<const T extends readonly unknown[]>(arr: {
[K in keyof T]: T[K];
}): T;

const result4 = test4(["1", 2]);
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 is an especially important part of the fix. Currently T is inferred as (2 | "1")[] but it really should be readonly ["1", 2]


declare function test5<const T extends readonly unknown[]>(
...args: {
[K in keyof T]: T[K];
}
): T;

const result5 = test5({ a: "foo" });