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

Fixed an issue with apparent mapped type keys #57838

Open
wants to merge 5 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
19 changes: 15 additions & 4 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22895,14 +22895,14 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return result;
}

function getApparentMappedTypeKeys(nameType: Type, targetType: MappedType) {
const modifiersType = getApparentType(getModifiersTypeFromMappedType(targetType));
function getApparentMappedTypeKeys(nameType: Type, mappedType: MappedType) {
const modifiersType = getApparentType(getModifiersTypeFromMappedType(mappedType));
const mappedKeys: Type[] = [];
forEachMappedTypePropertyKeyTypeAndIndexSignatureKeyType(
modifiersType,
TypeFlags.StringOrNumberLiteralOrUnique,
/*stringsOnly*/ false,
t => void mappedKeys.push(instantiateType(nameType, appendTypeMapping(targetType.mapper, getTypeParameterFromMappedType(targetType), t))),
t => void mappedKeys.push(instantiateType(nameType, appendTypeMapping(mappedType.mapper, getTypeParameterFromMappedType(mappedType), t))),
);
return getUnionType(mappedKeys);
}
Expand Down Expand Up @@ -23277,7 +23277,18 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// allow assignments of index types of identical (or similar enough) mapped types.
// eg, `keyof {[X in keyof A]: Obj[X]}` should be assignable to `keyof {[Y in keyof A]: Tup[Y]}` because both map over the same set of keys (`keyof A`).
// Without this source-side breakdown, a `keyof {[X in keyof A]: Obj[X]}` style type won't be assignable to anything except itself, which is much too strict.
const sourceMappedKeys = nameType && isMappedTypeWithKeyofConstraintDeclaration(mappedType) ? getApparentMappedTypeKeys(nameType, mappedType) : (nameType || getConstraintTypeFromMappedType(mappedType));
let sourceMappedKeys: Type;
if (nameType && isMappedTypeWithKeyofConstraintDeclaration(mappedType)) {
sourceMappedKeys = getApparentMappedTypeKeys(nameType, mappedType);
if (sourceMappedKeys.flags & TypeFlags.Never) {
Copy link
Member

Choose a reason for hiding this comment

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

I find it a bit weird that:

  • keyof unknown is undefined.
  • the fix is looking at the result of getApparentMappedTypeKeys. I'd expect it to be inside getApparentMappedTypeKeys. Isn't there a case where getApparentMappedTypeKeys is never and sourceMappedKeys should stay as never?

This doesn't look wrong to me, but I can't yet form a good mental model of what's happening, so I don't know what to think. 😅
I'll revisit in a few days and maybe I'll have a better idea of what this all means.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

keyof unknown is undefined.

If we think about keyof T as a set of types that can be safely used to access properties of T in T[keyof T] then never makes sense here. never is the only type that can be safely used as keyof T after we instantiate T as unknown.

the fix is looking at the result of getApparentMappedTypeKeys. I'd expect it to be inside getApparentMappedTypeKeys. Isn't there a case where getApparentMappedTypeKeys is never and sourceMappedKeys should stay as never?

That's what I started with but mapping never to PropertyKey would break the other call site where getApparentMappedTypeKeys is used. In there, it's used to get the target keys. So this would remove the error below:

type StringKeys2<T> = keyof {
  [P in keyof T as T[P] extends string ? P : never]: any;
};
function h<T>(z: StringKeys2<T>) {
  z = "foo"; // error
}

Copy link
Member

Choose a reason for hiding this comment

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

This all has me thinking that the original code that uses getApparentMappedTypeKeys on the source side is just... wrong?

// modifiers type of mapped type is often `unknown`, `keyof unknown` is `never` and that's assignable to everything
// letting this through is too permissive so we use the apparent type of an index type here instead
sourceMappedKeys = stringNumberSymbolType;
}
}
else {
sourceMappedKeys = nameType || getConstraintTypeFromMappedType(mappedType);
}
if (result = isRelatedTo(sourceMappedKeys, target, RecursionFlags.Source, reportErrors)) {
return result;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
keyRemappingKeyofResult.ts(82,3): error TS2322: Type 'string' is not assignable to type 'Extract<keyof { [P in keyof T as T[P] extends string ? P : never]: any; }, string>'.
keyRemappingKeyofResult.ts(90,3): error TS2322: Type 'string' is not assignable to type 'keyof { [P in keyof T as T[P] extends string ? P : never]: any; }'.
Type 'string' is not assignable to type 'T[P] extends string ? P : never'.


==== keyRemappingKeyofResult.ts (2 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";
}

// https://github.com/microsoft/TypeScript/issues/57827

type StringKeys<T> = Extract<
keyof {
[P in keyof T as T[P] extends string ? P : never]: any;
},
string
>;

function test_57827<T>(z: StringKeys<T>) {
const f: string = z;
z = "foo"; // error
~
!!! error TS2322: Type 'string' is not assignable to type 'Extract<keyof { [P in keyof T as T[P] extends string ? P : never]: any; }, string>'.
}

type StringKeys2<T> = keyof {
[P in keyof T as T[P] extends string ? P : never]: any;
};

function h<T>(z: StringKeys2<T>) {
z = "foo"; // error
~
!!! error TS2322: Type 'string' is not assignable to type 'keyof { [P in keyof T as T[P] extends string ? P : never]: any; }'.
!!! error TS2322: Type 'string' is not assignable to type 'T[P] extends string ? P : never'.
const f: string = z; // ok
}

export {};

Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,21 @@ x = "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;
Expand All @@ -47,7 +47,7 @@ function f<T>() {
// 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";
Expand All @@ -57,21 +57,45 @@ function g<T>() {

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";
}

export {};
// https://github.com/microsoft/TypeScript/issues/57827

type StringKeys<T> = Extract<
keyof {
[P in keyof T as T[P] extends string ? P : never]: any;
},
string
>;

function test_57827<T>(z: StringKeys<T>) {
const f: string = z;
z = "foo"; // error
}

type StringKeys2<T> = keyof {
[P in keyof T as T[P] extends string ? P : never]: any;
};

function h<T>(z: StringKeys2<T>) {
z = "foo"; // error
const f: string = z; // ok
}

export {};


//// [keyRemappingKeyofResult.js]
const sym = Symbol("");
Expand All @@ -98,4 +122,12 @@ function g() {
x = sym;
x = "str";
}
function test_57827(z) {
const f = z;
z = "foo"; // error
}
function h(z) {
z = "foo"; // error
const f = z; // ok
}
export {};
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ function f<T>() {
>[sym] : Symbol([sym], Decl(keyRemappingKeyofResult.ts, 21, 45))
>sym : Symbol(sym, Decl(keyRemappingKeyofResult.ts, 0, 5))
>T : Symbol(T, Decl(keyRemappingKeyofResult.ts, 20, 11))

type Okay = keyof Orig;
>Okay : Symbol(Okay, Decl(keyRemappingKeyofResult.ts, 21, 63))
>Orig : Symbol(Orig, Decl(keyRemappingKeyofResult.ts, 20, 17))
Expand All @@ -83,7 +83,7 @@ function f<T>() {
>a : Symbol(a, Decl(keyRemappingKeyofResult.ts, 24, 7))

// type Okay = string | number | typeof sym

type Remapped = { [K in keyof Orig as {} extends Record<K, any> ? never : K]: any }
>Remapped : Symbol(Remapped, Decl(keyRemappingKeyofResult.ts, 27, 19))
>K : Symbol(K, Decl(keyRemappingKeyofResult.ts, 30, 23))
Expand All @@ -97,7 +97,7 @@ function f<T>() {
[sym]: any;
} */
// no string index signature, right?

type Oops = keyof Remapped;
>Oops : Symbol(Oops, Decl(keyRemappingKeyofResult.ts, 30, 87))
>Remapped : Symbol(Remapped, Decl(keyRemappingKeyofResult.ts, 27, 19))
Expand Down Expand Up @@ -126,7 +126,7 @@ function g<T>() {
>[sym] : Symbol([sym], Decl(keyRemappingKeyofResult.ts, 45, 45))
>sym : Symbol(sym, Decl(keyRemappingKeyofResult.ts, 0, 5))
>T : Symbol(T, Decl(keyRemappingKeyofResult.ts, 44, 11))

type Okay = keyof Orig;
>Okay : Symbol(Okay, Decl(keyRemappingKeyofResult.ts, 45, 63))
>Orig : Symbol(Orig, Decl(keyRemappingKeyofResult.ts, 44, 17))
Expand Down Expand Up @@ -162,7 +162,7 @@ function g<T>() {
>T : Symbol(T, Decl(keyRemappingKeyofResult.ts, 55, 30))
>NonIndex : Symbol(NonIndex, Decl(keyRemappingKeyofResult.ts, 51, 19))
>T : Symbol(T, Decl(keyRemappingKeyofResult.ts, 55, 30))

type Remapped = { [K in keyof Orig as DistributiveNonIndex<K>]: any }
>Remapped : Symbol(Remapped, Decl(keyRemappingKeyofResult.ts, 55, 95))
>K : Symbol(K, Decl(keyRemappingKeyofResult.ts, 57, 23))
Expand All @@ -175,7 +175,7 @@ function g<T>() {
[sym]: any;
} */
// no string index signature, right?

type Oops = keyof Remapped;
>Oops : Symbol(Oops, Decl(keyRemappingKeyofResult.ts, 57, 73))
>Remapped : Symbol(Remapped, Decl(keyRemappingKeyofResult.ts, 55, 95))
Expand All @@ -192,4 +192,67 @@ function g<T>() {
>x : Symbol(x, Decl(keyRemappingKeyofResult.ts, 65, 7))
}

// https://github.com/microsoft/TypeScript/issues/57827

type StringKeys<T> = Extract<
>StringKeys : Symbol(StringKeys, Decl(keyRemappingKeyofResult.ts, 68, 1))
>T : Symbol(T, Decl(keyRemappingKeyofResult.ts, 72, 16))
>Extract : Symbol(Extract, Decl(lib.es5.d.ts, --, --))

keyof {
[P in keyof T as T[P] extends string ? P : never]: any;
>P : Symbol(P, Decl(keyRemappingKeyofResult.ts, 74, 5))
>T : Symbol(T, Decl(keyRemappingKeyofResult.ts, 72, 16))
>T : Symbol(T, Decl(keyRemappingKeyofResult.ts, 72, 16))
>P : Symbol(P, Decl(keyRemappingKeyofResult.ts, 74, 5))
>P : Symbol(P, Decl(keyRemappingKeyofResult.ts, 74, 5))

},
string
>;

function test_57827<T>(z: StringKeys<T>) {
>test_57827 : Symbol(test_57827, Decl(keyRemappingKeyofResult.ts, 77, 2))
>T : Symbol(T, Decl(keyRemappingKeyofResult.ts, 79, 20))
>z : Symbol(z, Decl(keyRemappingKeyofResult.ts, 79, 23))
>StringKeys : Symbol(StringKeys, Decl(keyRemappingKeyofResult.ts, 68, 1))
>T : Symbol(T, Decl(keyRemappingKeyofResult.ts, 79, 20))

const f: string = z;
>f : Symbol(f, Decl(keyRemappingKeyofResult.ts, 80, 7))
>z : Symbol(z, Decl(keyRemappingKeyofResult.ts, 79, 23))

z = "foo"; // error
>z : Symbol(z, Decl(keyRemappingKeyofResult.ts, 79, 23))
}

type StringKeys2<T> = keyof {
>StringKeys2 : Symbol(StringKeys2, Decl(keyRemappingKeyofResult.ts, 82, 1))
>T : Symbol(T, Decl(keyRemappingKeyofResult.ts, 84, 17))

[P in keyof T as T[P] extends string ? P : never]: any;
>P : Symbol(P, Decl(keyRemappingKeyofResult.ts, 85, 3))
>T : Symbol(T, Decl(keyRemappingKeyofResult.ts, 84, 17))
>T : Symbol(T, Decl(keyRemappingKeyofResult.ts, 84, 17))
>P : Symbol(P, Decl(keyRemappingKeyofResult.ts, 85, 3))
>P : Symbol(P, Decl(keyRemappingKeyofResult.ts, 85, 3))

};

function h<T>(z: StringKeys2<T>) {
>h : Symbol(h, Decl(keyRemappingKeyofResult.ts, 86, 2))
>T : Symbol(T, Decl(keyRemappingKeyofResult.ts, 88, 11))
>z : Symbol(z, Decl(keyRemappingKeyofResult.ts, 88, 14))
>StringKeys2 : Symbol(StringKeys2, Decl(keyRemappingKeyofResult.ts, 82, 1))
>T : Symbol(T, Decl(keyRemappingKeyofResult.ts, 88, 11))

z = "foo"; // error
>z : Symbol(z, Decl(keyRemappingKeyofResult.ts, 88, 14))

const f: string = z; // ok
>f : Symbol(f, Decl(keyRemappingKeyofResult.ts, 90, 7))
>z : Symbol(z, Decl(keyRemappingKeyofResult.ts, 88, 14))
}

export {};

Loading