Skip to content
Closed
2 changes: 1 addition & 1 deletion src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22486,7 +22486,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
/*stringsOnly*/ false,
t => void mappedKeys.push(instantiateType(nameType, appendTypeMapping(targetType.mapper, getTypeParameterFromMappedType(targetType), t))),
);
return getUnionType(mappedKeys);
return mappedKeys.length ? getUnionType(mappedKeys) : stringNumberSymbolType;
Copy link
Copy Markdown
Contributor Author

@Andarist Andarist Mar 18, 2024

Choose a reason for hiding this comment

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

The problem here is that when modifiersType is unknown (or {}) then keyof modifiersType is never. Since never is assignable to everything, the checker assumed that keyof { [P in keyof T as T[P] extends string ? P: never]: any } is assignable to string.

Then based on that information it managed to simplify the instantiated substitution type of Extract to just keyof { [P in keyof T as T[P] extends string ? P: never]: any } (its newBaseType). Then in turn getNarrowableTypeForReference concluded that it should substitute constraints. Those were substituted with stringNumberSymbolType (for the keyof ...) and that's not assignable to string.

Before #56742 , it didn't mistakenly assume that keyof { [P in keyof T as T[P] extends string ? P: never]: any } is assignable to string so the substitution type was instantiated as (keyof { [P in keyof T as T[P] extends string ? P: never]: any }) & string and that is assignable to a string.

}

function structuredTypeRelatedToWorker(source: Type, target: Type, reportErrors: boolean, intersectionState: IntersectionState, saveErrorInfo: ReturnType<typeof captureErrorCalculationState>): Ternary {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,19 @@ function g<T>() {
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;
}

export {};

//// [keyRemappingKeyofResult.js]
Expand Down Expand Up @@ -98,4 +111,7 @@ function g() {
x = sym;
x = "str";
}
function test_57827(z) {
const f = z;
}
export {};
Original file line number Diff line number Diff line change
Expand Up @@ -192,4 +192,35 @@ 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))
}

export {};
Original file line number Diff line number Diff line change
Expand Up @@ -172,4 +172,24 @@ function g<T>() {
>"str" : "str"
}

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

type StringKeys<T> = Extract<
>StringKeys : StringKeys<T>

keyof {
[P in keyof T as T[P] extends string ? P : never]: any;
},
string
>;

function test_57827<T>(z: StringKeys<T>) {
>test_57827 : <T>(z: StringKeys<T>) => void
>z : Extract<keyof { [P in keyof T as T[P] extends string ? P : never]: any; }, string>

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

export {};
117 changes: 117 additions & 0 deletions tests/baselines/reference/keyRemappingKeyofResult(strict=true).js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
//// [tests/cases/compiler/keyRemappingKeyofResult.ts] ////

//// [keyRemappingKeyofResult.ts]
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;
}

export {};

//// [keyRemappingKeyofResult.js]
const sym = Symbol("");
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() {
let a;
a = "str";
a = sym;
a = "whatever";
let x;
x = sym;
x = "str";
}
// and another generic case with a _distributive_ mapping, to trigger a different branch in `getIndexType`
function g() {
let a;
a = "str";
a = sym;
a = "whatever";
let x;
x = sym;
x = "str";
}
function test_57827(z) {
const f = z;
}
export {};
Loading