keyof should always include remapped keys (#45923)

* Loosen check in getIndexTypeForMappedType to directly map property names when any indexy type is present

* Handle homomorphic mappings better in keyof, add specific relationship rule for relating generic keyof MappedType to handle remapped keys

* Remove trailing whitespace
This commit is contained in:
Wesley Wigham
2021-09-27 19:10:02 -07:00
committed by GitHub
parent 530b0e2e10
commit 8d5c197f99
7 changed files with 642 additions and 33 deletions
+98 -26
View File
@@ -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 }
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;
// `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 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<P>]: X }, to simply N<K>. 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) {
@@ -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"
@@ -0,0 +1,99 @@
//// [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";
}
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 {};
@@ -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<keyof Orig, never>
>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<K, any> ? 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<keyof Remapped, never>
>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<T>() {
>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<K, any> ? 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<T>() {
>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<T extends PropertyKey> = {} extends Record<T, any> ? 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 PropertyKey> = T extends unknown ? NonIndex<T> : 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<K>]: 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 {};
@@ -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<keyof Orig, never>
>Okay : Okay
// type Okay = string | number | typeof sym
type Remapped = { [K in keyof Orig as {} extends Record<K, any> ? never : K]: any }
>Remapped : Remapped
/* type Remapped = {
str: any;
[sym]: any;
} */
// no string index signature, right?
type Oops = Exclude<keyof Remapped, never>
>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<T>() {
>f : <T>() => 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<K, any> ? never : K]: any }
>Remapped : { [K in keyof ({ [k: string]: any; str: any; [sym]: any; } & T) as {} extends Record<K, any> ? 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<K, any> ? never : K]: any; }
let x: Oops;
>x : keyof { [K in keyof ({ [k: string]: any; str: any; [sym]: any; } & T) as {} extends Record<K, any> ? 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<K, any> ? 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<K, any> ? never : K]: any; }
>"str" : "str"
}
// and another generic case with a _distributive_ mapping, to trigger a different branch in `getIndexType`
function g<T>() {
>g : <T>() => 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<T extends PropertyKey> = {} extends Record<T, any> ? never : T;
>NonIndex : {} extends Record<T, any> ? never : T
type DistributiveNonIndex<T extends PropertyKey> = T extends unknown ? NonIndex<T> : never;
>DistributiveNonIndex : T extends unknown ? {} extends Record<T, any> ? never : T : never
type Remapped = { [K in keyof Orig as DistributiveNonIndex<K>]: any }
>Remapped : { [K in keyof ({ [k: string]: any; str: any; [sym]: any; } & T) as K extends unknown ? {} extends Record<K, any> ? 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<K, any> ? 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<K, any> ? 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<K, any> ? 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<K, any> ? never : K : never]: any; }
>"str" : "str"
}
export {};
@@ -260,25 +260,25 @@ type NameMap = { 'a': 'x', 'b': 'y', 'c': 'z' };
// Distributive, will be simplified
type TS0<T> = keyof { [P in keyof T as keyof Record<P, number>]: string };
>TS0 : keyof T
>TS0 : keyof { [P in keyof T as P]: string; }
type TS1<T> = keyof { [P in keyof T as Extract<P, 'a' | 'b' | 'c'>]: string };
>TS1 : Extract<keyof T, "a" | "b" | "c">
>TS1 : keyof { [P in keyof T as Extract<P, "a" | "b" | "c">]: string; }
type TS2<T> = 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<T> = keyof { [P in keyof T as Exclude<P, 'a' | 'b' | 'c'>]: string };
>TS3 : Exclude<keyof T, "a" | "b" | "c">
>TS3 : keyof { [P in keyof T as Exclude<P, "a" | "b" | "c">]: string; }
type TS4<T> = 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<T> = 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<T, U, V> = 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
@@ -0,0 +1,72 @@
// @target: es6
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";
}
export {};