diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts
index 3340ad1bbc6a4..77d83a04d2579 100644
--- a/src/compiler/checker.ts
+++ b/src/compiler/checker.ts
@@ -11265,6 +11265,22 @@ namespace ts {
return getCheckFlags(s) & CheckFlags.Late;
}
+ function forEachMappedTypePropertyKeyTypeAndIndexSignatureKeyType(type: Type, include: TypeFlags, stringsOnly: boolean, cb: (keyType: Type) => void) {
+ for (const prop of getPropertiesOfType(type)) {
+ cb(getLiteralTypeFromProperty(prop, include));
+ }
+ if (type.flags & TypeFlags.Any) {
+ cb(stringType);
+ }
+ else {
+ for (const info of getIndexInfosOfType(type)) {
+ if (!stringsOnly || info.keyType.flags & (TypeFlags.String | TypeFlags.TemplateLiteral)) {
+ cb(info.keyType);
+ }
+ }
+ }
+ }
+
/** Resolve the members of a mapped type { [P in K]: T } */
function resolveMappedTypeMembers(type: MappedType) {
const members: SymbolTable = createSymbolTable();
@@ -11282,19 +11298,7 @@ namespace ts {
const include = keyofStringsOnly ? TypeFlags.StringLiteral : TypeFlags.StringOrNumberLiteralOrUnique;
if (isMappedTypeWithKeyofConstraintDeclaration(type)) {
// We have a { [P in keyof T]: X }
- for (const prop of getPropertiesOfType(modifiersType)) {
- addMemberForKeyType(getLiteralTypeFromProperty(prop, include));
- }
- if (modifiersType.flags & TypeFlags.Any) {
- addMemberForKeyType(stringType);
- }
- else {
- for (const info of getIndexInfosOfType(modifiersType)) {
- if (!keyofStringsOnly || info.keyType.flags & (TypeFlags.String | TypeFlags.TemplateLiteral)) {
- addMemberForKeyType(info.keyType);
- }
- }
- }
+ forEachMappedTypePropertyKeyTypeAndIndexSignatureKeyType(modifiersType, include, keyofStringsOnly, addMemberForKeyType);
}
else {
forEachType(getLowerBoundOfKeyType(constraintType), addMemberForKeyType);
@@ -14653,19 +14657,58 @@ namespace ts {
type.resolvedIndexType || (type.resolvedIndexType = createIndexType(type, /*stringsOnly*/ false));
}
- function instantiateTypeAsMappedNameType(nameType: Type, type: MappedType, t: Type) {
- return instantiateType(nameType, appendTypeMapping(type.mapper, getTypeParameterFromMappedType(type), t));
- }
+ /**
+ * This roughly mirrors `resolveMappedTypeMembers` in the nongeneric case, except only reports a union of the keys calculated,
+ * rather than manufacturing the properties. We can't just fetch the `constraintType` since that would ignore mappings
+ * and mapping the `constraintType` directly ignores how mapped types map _properties_ and not keys (thus ignoring subtype
+ * reduction in the constraintType) when possible.
+ * @param noIndexSignatures Indicates if _string_ index signatures should be elided. (other index signatures are always reported)
+ */
+ function getIndexTypeForMappedType(type: MappedType, stringsOnly: boolean, noIndexSignatures: boolean | undefined) {
+ const typeParameter = getTypeParameterFromMappedType(type);
+ const constraintType = getConstraintTypeFromMappedType(type);
+ const nameType = getNameTypeFromMappedType(type.target as MappedType || type);
+ if (!nameType && !noIndexSignatures) {
+ // no mapping and no filtering required, just quickly bail to returning the constraint in the common case
+ return constraintType;
+ }
+ const keyTypes: Type[] = [];
+ if (isMappedTypeWithKeyofConstraintDeclaration(type)) {
+ // We have a { [P in keyof T]: X }
+
+ // `getApparentType` on the T in a generic mapped type can trigger a circularity
+ // (conditionals and `infer` types create a circular dependency in the constraint resolution)
+ // so we only eagerly manifest the keys if the constraint is nongeneric
+ if (!isGenericIndexType(constraintType)) {
+ const modifiersType = getApparentType(getModifiersTypeFromMappedType(type)); // The 'T' in 'keyof T'
+ forEachMappedTypePropertyKeyTypeAndIndexSignatureKeyType(modifiersType, TypeFlags.StringOrNumberLiteralOrUnique, stringsOnly, addMemberForKeyType);
+ }
+ else {
+ // we have a generic index and a homomorphic mapping (but a distributive key remapping) - we need to defer the whole `keyof whatever` for later
+ // since it's not safe to resolve the shape of modifier type
+ return getIndexTypeForGenericType(type, stringsOnly);
+ }
+ }
+ else {
+ forEachType(getLowerBoundOfKeyType(constraintType), addMemberForKeyType);
+ }
+ if (isGenericIndexType(constraintType)) { // include the generic component in the resulting type
+ forEachType(constraintType, addMemberForKeyType);
+ }
+ // we had to pick apart the constraintType to potentially map/filter it - compare the final resulting list with the original constraintType,
+ // so we can return the union that preserves aliases/origin data if possible
+ const result = noIndexSignatures ? filterType(getUnionType(keyTypes), t => !(t.flags & (TypeFlags.Any | TypeFlags.String))) : getUnionType(keyTypes);
+ if (result.flags & TypeFlags.Union && constraintType.flags & TypeFlags.Union && getTypeListId((result as UnionType).types) === getTypeListId((constraintType as UnionType).types)){
+ return constraintType;
+ }
+ return result;
- function getIndexTypeForMappedType(type: MappedType, noIndexSignatures: boolean | undefined) {
- const constraint = filterType(getConstraintTypeFromMappedType(type), t => !(noIndexSignatures && t.flags & (TypeFlags.Any | TypeFlags.String)));
- const nameType = type.declaration.nameType && getTypeFromTypeNode(type.declaration.nameType);
- // If the constraint is exclusively string/number/never type(s), we need to pull the property names from the modified type and run them through the `nameType` mapper as well
- // since they won't appear in the constraint, due to subtype reducing with the string/number index types
- const properties = nameType && everyType(constraint, t => !!(t.flags & (TypeFlags.String | TypeFlags.Number | TypeFlags.Never))) && getPropertiesOfType(getApparentType(getModifiersTypeFromMappedType(type)));
- return nameType ?
- getUnionType([mapType(constraint, t => instantiateTypeAsMappedNameType(nameType, type, t)), mapType(getUnionType(map(properties || emptyArray, p => getLiteralTypeFromProperty(p, TypeFlags.StringOrNumberLiteralOrUnique))), t => instantiateTypeAsMappedNameType(nameType, type, t))]):
- constraint;
+ function addMemberForKeyType(keyType: Type) {
+ const propNameType = nameType ? instantiateType(nameType, appendTypeMapping(type.mapper, typeParameter, keyType)) : keyType;
+ // `keyof` currently always returns `string | number` for concrete `string` index signatures - the below ternary keeps that behavior for mapped types
+ // See `getLiteralTypeFromProperties` where there's a similar ternary to cause the same behavior.
+ keyTypes.push(propNameType === stringType ? stringOrNumberType : propNameType);
+ }
}
// Ordinarily we reduce a keyof M, where M is a mapped type { [P in K as N
]: X }, to simply N. This however presumes
@@ -14728,7 +14771,7 @@ namespace ts {
return type.flags & TypeFlags.Union ? getIntersectionType(map((type as UnionType).types, t => getIndexType(t, stringsOnly, noIndexSignatures))) :
type.flags & TypeFlags.Intersection ? getUnionType(map((type as IntersectionType).types, t => getIndexType(t, stringsOnly, noIndexSignatures))) :
type.flags & TypeFlags.InstantiableNonPrimitive || isGenericTupleType(type) || isGenericMappedType(type) && !hasDistributiveNameType(type) ? getIndexTypeForGenericType(type as InstantiableType | UnionOrIntersectionType, stringsOnly) :
- getObjectFlags(type) & ObjectFlags.Mapped ? getIndexTypeForMappedType(type as MappedType, noIndexSignatures) :
+ getObjectFlags(type) & ObjectFlags.Mapped ? getIndexTypeForMappedType(type as MappedType, stringsOnly, noIndexSignatures) :
type === wildcardType ? wildcardType :
type.flags & TypeFlags.Unknown ? neverType :
type.flags & (TypeFlags.Any | TypeFlags.Never) ? keyofConstraintType :
@@ -18793,6 +18836,35 @@ namespace ts {
return Ternary.True;
}
}
+ else if (isGenericMappedType(targetType)) {
+ // generic mapped types that don't simplify or have a constraint still have a very simple set of keys we can compare against
+ // - their nameType or constraintType.
+ // In many ways, this comparison is a deferred version of what `getIndexTypeForMappedType` does to actually resolve the keys for _non_-generic types
+
+ const nameType = getNameTypeFromMappedType(targetType);
+ const constraintType = getConstraintTypeFromMappedType(targetType);
+ let targetKeys;
+ if (nameType && isMappedTypeWithKeyofConstraintDeclaration(targetType)) {
+ // we need to get the apparent mappings and union them with the generic mappings, since some properties may be
+ // missing from the `constraintType` which will otherwise be mapped in the object
+ const modifiersType = getApparentType(getModifiersTypeFromMappedType(targetType));
+ const mappedKeys: Type[] = [];
+ forEachMappedTypePropertyKeyTypeAndIndexSignatureKeyType(
+ modifiersType,
+ TypeFlags.StringOrNumberLiteralOrUnique,
+ /*stringsOnly*/ false,
+ t => void mappedKeys.push(instantiateType(nameType, appendTypeMapping(targetType.mapper, getTypeParameterFromMappedType(targetType), t)))
+ );
+ // We still need to include the non-apparent (and thus still generic) keys in the target side of the comparison (in case they're in the source side)
+ targetKeys = getUnionType([...mappedKeys, nameType]);
+ }
+ else {
+ targetKeys = nameType || constraintType;
+ }
+ if (isRelatedTo(source, targetKeys, reportErrors) === Ternary.True) {
+ return Ternary.True;
+ }
+ }
}
}
else if (target.flags & TypeFlags.IndexedAccess) {
diff --git a/tests/baselines/reference/computedTypesKeyofNoIndexSignatureType.types b/tests/baselines/reference/computedTypesKeyofNoIndexSignatureType.types
index 4723797638850..6adec9195f253 100644
--- a/tests/baselines/reference/computedTypesKeyofNoIndexSignatureType.types
+++ b/tests/baselines/reference/computedTypesKeyofNoIndexSignatureType.types
@@ -39,5 +39,5 @@ type WithIndexKey = keyof WithIndex; // string | number <-- Expected: stri
>WithIndexKey : string | number
type WithoutIndexKey = keyof WithoutIndex; // number <-- Expected: "foo" | "bar"
->WithoutIndexKey : number | "foo" | "bar"
+>WithoutIndexKey : "foo" | "bar"
diff --git a/tests/baselines/reference/keyRemappingKeyofResult.js b/tests/baselines/reference/keyRemappingKeyofResult.js
new file mode 100644
index 0000000000000..0b725e05b5710
--- /dev/null
+++ b/tests/baselines/reference/keyRemappingKeyofResult.js
@@ -0,0 +1,99 @@
+//// [keyRemappingKeyofResult.ts]
+const sym = Symbol("")
+type Orig = { [k: string]: any, str: any, [sym]: any }
+
+type Okay = Exclude
+// type Okay = string | number | typeof sym
+
+type Remapped = { [K in keyof Orig as {} extends Record ? never : K]: any }
+/* type Remapped = {
+ str: any;
+ [sym]: any;
+} */
+// no string index signature, right?
+
+type Oops = Exclude
+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() {
+ 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 ? 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() {
+ 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 = {} extends Record ? never : T;
+ type DistributiveNonIndex = T extends unknown ? NonIndex : never;
+
+ type Remapped = { [K in keyof Orig as DistributiveNonIndex]: any }
+ /* type Remapped = {
+ str: any;
+ [sym]: any;
+ } */
+ // no string index signature, right?
+
+ type Oops = keyof Remapped;
+ let x: Oops;
+ x = sym;
+ x = "str";
+}
+
+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";
+}
+export {};
diff --git a/tests/baselines/reference/keyRemappingKeyofResult.symbols b/tests/baselines/reference/keyRemappingKeyofResult.symbols
new file mode 100644
index 0000000000000..fb306f96b72d0
--- /dev/null
+++ b/tests/baselines/reference/keyRemappingKeyofResult.symbols
@@ -0,0 +1,193 @@
+=== tests/cases/compiler/keyRemappingKeyofResult.ts ===
+const sym = Symbol("")
+>sym : Symbol(sym, Decl(keyRemappingKeyofResult.ts, 0, 5))
+>Symbol : Symbol(Symbol, Decl(lib.es5.d.ts, --, --), Decl(lib.es2015.symbol.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --))
+
+type Orig = { [k: string]: any, str: any, [sym]: any }
+>Orig : Symbol(Orig, Decl(keyRemappingKeyofResult.ts, 0, 22))
+>k : Symbol(k, Decl(keyRemappingKeyofResult.ts, 1, 15))
+>str : Symbol(str, Decl(keyRemappingKeyofResult.ts, 1, 31))
+>[sym] : Symbol([sym], Decl(keyRemappingKeyofResult.ts, 1, 41))
+>sym : Symbol(sym, Decl(keyRemappingKeyofResult.ts, 0, 5))
+
+type Okay = Exclude
+>Okay : Symbol(Okay, Decl(keyRemappingKeyofResult.ts, 1, 54))
+>Exclude : Symbol(Exclude, Decl(lib.es5.d.ts, --, --))
+>Orig : Symbol(Orig, Decl(keyRemappingKeyofResult.ts, 0, 22))
+
+// type Okay = string | number | typeof sym
+
+type Remapped = { [K in keyof Orig as {} extends Record ? never : K]: any }
+>Remapped : Symbol(Remapped, Decl(keyRemappingKeyofResult.ts, 3, 38))
+>K : Symbol(K, Decl(keyRemappingKeyofResult.ts, 6, 19))
+>Orig : Symbol(Orig, Decl(keyRemappingKeyofResult.ts, 0, 22))
+>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --))
+>K : Symbol(K, Decl(keyRemappingKeyofResult.ts, 6, 19))
+>K : Symbol(K, Decl(keyRemappingKeyofResult.ts, 6, 19))
+
+/* type Remapped = {
+ str: any;
+ [sym]: any;
+} */
+// no string index signature, right?
+
+type Oops = Exclude
+>Oops : Symbol(Oops, Decl(keyRemappingKeyofResult.ts, 6, 83))
+>Exclude : Symbol(Exclude, Decl(lib.es5.d.ts, --, --))
+>Remapped : Symbol(Remapped, Decl(keyRemappingKeyofResult.ts, 3, 38))
+
+declare let x: Oops;
+>x : Symbol(x, Decl(keyRemappingKeyofResult.ts, 14, 11))
+>Oops : Symbol(Oops, Decl(keyRemappingKeyofResult.ts, 6, 83))
+
+x = sym;
+>x : Symbol(x, Decl(keyRemappingKeyofResult.ts, 14, 11))
+>sym : Symbol(sym, Decl(keyRemappingKeyofResult.ts, 0, 5))
+
+x = "str";
+>x : Symbol(x, Decl(keyRemappingKeyofResult.ts, 14, 11))
+
+// type Oops = typeof sym <-- what happened to "str"?
+
+// equivalently, with an unresolved generic (no `exclude` shenanigans, since conditions won't execute):
+function f() {
+>f : Symbol(f, Decl(keyRemappingKeyofResult.ts, 16, 10))
+>T : Symbol(T, Decl(keyRemappingKeyofResult.ts, 20, 11))
+
+ type Orig = { [k: string]: any, str: any, [sym]: any } & T;
+>Orig : Symbol(Orig, Decl(keyRemappingKeyofResult.ts, 20, 17))
+>k : Symbol(k, Decl(keyRemappingKeyofResult.ts, 21, 19))
+>str : Symbol(str, Decl(keyRemappingKeyofResult.ts, 21, 35))
+>[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))
+
+ let a: Okay;
+>a : Symbol(a, Decl(keyRemappingKeyofResult.ts, 24, 7))
+>Okay : Symbol(Okay, Decl(keyRemappingKeyofResult.ts, 21, 63))
+
+ a = "str";
+>a : Symbol(a, Decl(keyRemappingKeyofResult.ts, 24, 7))
+
+ a = sym;
+>a : Symbol(a, Decl(keyRemappingKeyofResult.ts, 24, 7))
+>sym : Symbol(sym, Decl(keyRemappingKeyofResult.ts, 0, 5))
+
+ a = "whatever";
+>a : Symbol(a, Decl(keyRemappingKeyofResult.ts, 24, 7))
+
+ // type Okay = string | number | typeof sym
+
+ type Remapped = { [K in keyof Orig as {} extends Record ? never : K]: any }
+>Remapped : Symbol(Remapped, Decl(keyRemappingKeyofResult.ts, 27, 19))
+>K : Symbol(K, Decl(keyRemappingKeyofResult.ts, 30, 23))
+>Orig : Symbol(Orig, Decl(keyRemappingKeyofResult.ts, 20, 17))
+>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --))
+>K : Symbol(K, Decl(keyRemappingKeyofResult.ts, 30, 23))
+>K : Symbol(K, Decl(keyRemappingKeyofResult.ts, 30, 23))
+
+ /* type Remapped = {
+ str: any;
+ [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))
+
+ let x: Oops;
+>x : Symbol(x, Decl(keyRemappingKeyofResult.ts, 38, 7))
+>Oops : Symbol(Oops, Decl(keyRemappingKeyofResult.ts, 30, 87))
+
+ x = sym;
+>x : Symbol(x, Decl(keyRemappingKeyofResult.ts, 38, 7))
+>sym : Symbol(sym, Decl(keyRemappingKeyofResult.ts, 0, 5))
+
+ x = "str";
+>x : Symbol(x, Decl(keyRemappingKeyofResult.ts, 38, 7))
+}
+
+// and another generic case with a _distributive_ mapping, to trigger a different branch in `getIndexType`
+function g() {
+>g : Symbol(g, Decl(keyRemappingKeyofResult.ts, 41, 1))
+>T : Symbol(T, Decl(keyRemappingKeyofResult.ts, 44, 11))
+
+ type Orig = { [k: string]: any, str: any, [sym]: any } & T;
+>Orig : Symbol(Orig, Decl(keyRemappingKeyofResult.ts, 44, 17))
+>k : Symbol(k, Decl(keyRemappingKeyofResult.ts, 45, 19))
+>str : Symbol(str, Decl(keyRemappingKeyofResult.ts, 45, 35))
+>[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))
+
+ let a: Okay;
+>a : Symbol(a, Decl(keyRemappingKeyofResult.ts, 48, 7))
+>Okay : Symbol(Okay, Decl(keyRemappingKeyofResult.ts, 45, 63))
+
+ a = "str";
+>a : Symbol(a, Decl(keyRemappingKeyofResult.ts, 48, 7))
+
+ a = sym;
+>a : Symbol(a, Decl(keyRemappingKeyofResult.ts, 48, 7))
+>sym : Symbol(sym, Decl(keyRemappingKeyofResult.ts, 0, 5))
+
+ a = "whatever";
+>a : Symbol(a, Decl(keyRemappingKeyofResult.ts, 48, 7))
+
+ // type Okay = string | number | typeof sym
+
+ type NonIndex = {} extends Record ? never : T;
+>NonIndex : Symbol(NonIndex, Decl(keyRemappingKeyofResult.ts, 51, 19))
+>T : Symbol(T, Decl(keyRemappingKeyofResult.ts, 54, 18))
+>PropertyKey : Symbol(PropertyKey, Decl(lib.es5.d.ts, --, --))
+>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --))
+>T : Symbol(T, Decl(keyRemappingKeyofResult.ts, 54, 18))
+>T : Symbol(T, Decl(keyRemappingKeyofResult.ts, 54, 18))
+
+ type DistributiveNonIndex = T extends unknown ? NonIndex : never;
+>DistributiveNonIndex : Symbol(DistributiveNonIndex, Decl(keyRemappingKeyofResult.ts, 54, 81))
+>T : Symbol(T, Decl(keyRemappingKeyofResult.ts, 55, 30))
+>PropertyKey : Symbol(PropertyKey, Decl(lib.es5.d.ts, --, --))
+>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]: any }
+>Remapped : Symbol(Remapped, Decl(keyRemappingKeyofResult.ts, 55, 95))
+>K : Symbol(K, Decl(keyRemappingKeyofResult.ts, 57, 23))
+>Orig : Symbol(Orig, Decl(keyRemappingKeyofResult.ts, 44, 17))
+>DistributiveNonIndex : Symbol(DistributiveNonIndex, Decl(keyRemappingKeyofResult.ts, 54, 81))
+>K : Symbol(K, Decl(keyRemappingKeyofResult.ts, 57, 23))
+
+ /* type Remapped = {
+ str: any;
+ [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))
+
+ let x: Oops;
+>x : Symbol(x, Decl(keyRemappingKeyofResult.ts, 65, 7))
+>Oops : Symbol(Oops, Decl(keyRemappingKeyofResult.ts, 57, 73))
+
+ x = sym;
+>x : Symbol(x, Decl(keyRemappingKeyofResult.ts, 65, 7))
+>sym : Symbol(sym, Decl(keyRemappingKeyofResult.ts, 0, 5))
+
+ x = "str";
+>x : Symbol(x, Decl(keyRemappingKeyofResult.ts, 65, 7))
+}
+
+export {};
diff --git a/tests/baselines/reference/keyRemappingKeyofResult.types b/tests/baselines/reference/keyRemappingKeyofResult.types
new file mode 100644
index 0000000000000..5d8aef034be42
--- /dev/null
+++ b/tests/baselines/reference/keyRemappingKeyofResult.types
@@ -0,0 +1,173 @@
+=== tests/cases/compiler/keyRemappingKeyofResult.ts ===
+const sym = Symbol("")
+>sym : unique symbol
+>Symbol("") : unique symbol
+>Symbol : SymbolConstructor
+>"" : ""
+
+type Orig = { [k: string]: any, str: any, [sym]: any }
+>Orig : Orig
+>k : string
+>str : any
+>[sym] : any
+>sym : unique symbol
+
+type Okay = Exclude
+>Okay : Okay
+
+// type Okay = string | number | typeof sym
+
+type Remapped = { [K in keyof Orig as {} extends Record ? never : K]: any }
+>Remapped : Remapped
+
+/* type Remapped = {
+ str: any;
+ [sym]: any;
+} */
+// no string index signature, right?
+
+type Oops = Exclude
+>Oops : Oops
+
+declare let x: Oops;
+>x : Oops
+
+x = sym;
+>x = sym : unique symbol
+>x : Oops
+>sym : unique symbol
+
+x = "str";
+>x = "str" : "str"
+>x : Oops
+>"str" : "str"
+
+// type Oops = typeof sym <-- what happened to "str"?
+
+// equivalently, with an unresolved generic (no `exclude` shenanigans, since conditions won't execute):
+function f() {
+>f : () => void
+
+ type Orig = { [k: string]: any, str: any, [sym]: any } & T;
+>Orig : { [k: string]: any; str: any; [sym]: any; } & T
+>k : string
+>str : any
+>[sym] : any
+>sym : unique symbol
+
+ type Okay = keyof Orig;
+>Okay : string | number | unique symbol | keyof T
+
+ let a: Okay;
+>a : string | number | unique symbol | keyof T
+
+ a = "str";
+>a = "str" : "str"
+>a : string | number | unique symbol | keyof T
+>"str" : "str"
+
+ a = sym;
+>a = sym : unique symbol
+>a : string | number | unique symbol | keyof T
+>sym : unique symbol
+
+ a = "whatever";
+>a = "whatever" : "whatever"
+>a : string | number | unique symbol | keyof T
+>"whatever" : "whatever"
+
+ // type Okay = string | number | typeof sym
+
+ type Remapped = { [K in keyof Orig as {} extends Record ? never : K]: any }
+>Remapped : { [K in keyof ({ [k: string]: any; str: any; [sym]: any; } & T) as {} extends Record ? never : K]: any; }
+
+ /* type Remapped = {
+ str: any;
+ [sym]: any;
+ } */
+ // no string index signature, right?
+
+ type Oops = keyof Remapped;
+>Oops : keyof { [K in keyof ({ [k: string]: any; str: any; [sym]: any; } & T) as {} extends Record ? never : K]: any; }
+
+ let x: Oops;
+>x : keyof { [K in keyof ({ [k: string]: any; str: any; [sym]: any; } & T) as {} extends Record ? never : K]: any; }
+
+ x = sym;
+>x = sym : unique symbol
+>x : keyof { [K in keyof ({ [k: string]: any; str: any; [sym]: any; } & T) as {} extends Record ? never : K]: any; }
+>sym : unique symbol
+
+ x = "str";
+>x = "str" : "str"
+>x : keyof { [K in keyof ({ [k: string]: any; str: any; [sym]: any; } & T) as {} extends Record ? never : K]: any; }
+>"str" : "str"
+}
+
+// and another generic case with a _distributive_ mapping, to trigger a different branch in `getIndexType`
+function g() {
+>g : () => void
+
+ type Orig = { [k: string]: any, str: any, [sym]: any } & T;
+>Orig : { [k: string]: any; str: any; [sym]: any; } & T
+>k : string
+>str : any
+>[sym] : any
+>sym : unique symbol
+
+ type Okay = keyof Orig;
+>Okay : string | number | unique symbol | keyof T
+
+ let a: Okay;
+>a : string | number | unique symbol | keyof T
+
+ a = "str";
+>a = "str" : "str"
+>a : string | number | unique symbol | keyof T
+>"str" : "str"
+
+ a = sym;
+>a = sym : unique symbol
+>a : string | number | unique symbol | keyof T
+>sym : unique symbol
+
+ a = "whatever";
+>a = "whatever" : "whatever"
+>a : string | number | unique symbol | keyof T
+>"whatever" : "whatever"
+
+ // type Okay = string | number | typeof sym
+
+ type NonIndex = {} extends Record ? never : T;
+>NonIndex : {} extends Record ? never : T
+
+ type DistributiveNonIndex = T extends unknown ? NonIndex : never;
+>DistributiveNonIndex : T extends unknown ? {} extends Record ? never : T : never
+
+ type Remapped = { [K in keyof Orig as DistributiveNonIndex]: any }
+>Remapped : { [K in keyof ({ [k: string]: any; str: any; [sym]: any; } & T) as K extends unknown ? {} extends Record ? never : K : never]: any; }
+
+ /* type Remapped = {
+ str: any;
+ [sym]: any;
+ } */
+ // no string index signature, right?
+
+ type Oops = keyof Remapped;
+>Oops : keyof { [K in keyof ({ [k: string]: any; str: any; [sym]: any; } & T) as K extends unknown ? {} extends Record ? never : K : never]: any; }
+
+ let x: Oops;
+>x : keyof { [K in keyof ({ [k: string]: any; str: any; [sym]: any; } & T) as K extends unknown ? {} extends Record ? never : K : never]: any; }
+
+ x = sym;
+>x = sym : unique symbol
+>x : keyof { [K in keyof ({ [k: string]: any; str: any; [sym]: any; } & T) as K extends unknown ? {} extends Record ? never : K : never]: any; }
+>sym : unique symbol
+
+ x = "str";
+>x = "str" : "str"
+>x : keyof { [K in keyof ({ [k: string]: any; str: any; [sym]: any; } & T) as K extends unknown ? {} extends Record ? never : K : never]: any; }
+>"str" : "str"
+}
+
+export {};
diff --git a/tests/baselines/reference/mappedTypeAsClauses.types b/tests/baselines/reference/mappedTypeAsClauses.types
index 803fd0bef5a23..f77042a71e5c1 100644
--- a/tests/baselines/reference/mappedTypeAsClauses.types
+++ b/tests/baselines/reference/mappedTypeAsClauses.types
@@ -260,25 +260,25 @@ type NameMap = { 'a': 'x', 'b': 'y', 'c': 'z' };
// Distributive, will be simplified
type TS0 = keyof { [P in keyof T as keyof Record]: string };
->TS0 : keyof T
+>TS0 : keyof { [P in keyof T as P]: string; }
type TS1 = keyof { [P in keyof T as Extract]: string };
->TS1 : Extract
+>TS1 : keyof { [P in keyof T as Extract]: string; }
type TS2 = keyof { [P in keyof T as P & ('a' | 'b' | 'c')]: string };
->TS2 : keyof T & ("a" | "b" | "c")
+>TS2 : keyof { [P in keyof T as P & ("a" | "b" | "c")]: string; }
type TS3 = keyof { [P in keyof T as Exclude]: string };
->TS3 : Exclude
+>TS3 : keyof { [P in keyof T as Exclude]: string; }
type TS4 = keyof { [P in keyof T as NameMap[P & keyof NameMap]]: string };
->TS4 : NameMap[keyof T & keyof NameMap]
+>TS4 : keyof { [P in keyof T as NameMap[P & keyof NameMap]]: string; }
type TS5 = keyof { [P in keyof T & keyof NameMap as NameMap[P]]: string };
>TS5 : NameMap[keyof T & "a"] | NameMap[keyof T & "b"] | NameMap[keyof T & "c"]
type TS6 = keyof { [ K in keyof T as V & (K extends U ? K : never)]: string };
->TS6 : V & (keyof T extends U ? U & keyof T : never)
+>TS6 : keyof { [K in keyof T as V & (K extends U ? K : never)]: string; }
// Non-distributive, won't be simplified
diff --git a/tests/cases/compiler/keyRemappingKeyofResult.ts b/tests/cases/compiler/keyRemappingKeyofResult.ts
new file mode 100644
index 0000000000000..fcf3f835ac691
--- /dev/null
+++ b/tests/cases/compiler/keyRemappingKeyofResult.ts
@@ -0,0 +1,72 @@
+// @target: es6
+const sym = Symbol("")
+type Orig = { [k: string]: any, str: any, [sym]: any }
+
+type Okay = Exclude
+// type Okay = string | number | typeof sym
+
+type Remapped = { [K in keyof Orig as {} extends Record ? never : K]: any }
+/* type Remapped = {
+ str: any;
+ [sym]: any;
+} */
+// no string index signature, right?
+
+type Oops = Exclude
+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() {
+ 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 ? 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() {
+ 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 = {} extends Record ? never : T;
+ type DistributiveNonIndex = T extends unknown ? NonIndex : never;
+
+ type Remapped = { [K in keyof Orig as DistributiveNonIndex]: any }
+ /* type Remapped = {
+ str: any;
+ [sym]: any;
+ } */
+ // no string index signature, right?
+
+ type Oops = keyof Remapped;
+ let x: Oops;
+ x = sym;
+ x = "str";
+}
+
+export {};
\ No newline at end of file