Skip to content

Commit b6e3704

Browse files
committed
When relating a deferred index type over a mapped type on the source side, actually compare against the mapped type's apparent keys
1 parent 0c2dea5 commit b6e3704

5 files changed

+128
-10
lines changed

src/compiler/checker.ts

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22142,6 +22142,18 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2214222142
return result;
2214322143
}
2214422144

22145+
function getApparentMappedTypeKeys(nameType: Type, targetType: MappedType) {
22146+
const modifiersType = getApparentType(getModifiersTypeFromMappedType(targetType));
22147+
const mappedKeys: Type[] = [];
22148+
forEachMappedTypePropertyKeyTypeAndIndexSignatureKeyType(
22149+
modifiersType,
22150+
TypeFlags.StringOrNumberLiteralOrUnique,
22151+
/*stringsOnly*/ false,
22152+
t => void mappedKeys.push(instantiateType(nameType, appendTypeMapping(targetType.mapper, getTypeParameterFromMappedType(targetType), t))),
22153+
);
22154+
return getUnionType(mappedKeys);
22155+
}
22156+
2214522157
function structuredTypeRelatedToWorker(source: Type, target: Type, reportErrors: boolean, intersectionState: IntersectionState, saveErrorInfo: ReturnType<typeof captureErrorCalculationState>): Ternary {
2214622158
let result: Ternary;
2214722159
let originalErrorInfo: DiagnosticMessageChain | undefined;
@@ -22305,16 +22317,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2230522317
if (nameType && isMappedTypeWithKeyofConstraintDeclaration(targetType)) {
2230622318
// we need to get the apparent mappings and union them with the generic mappings, since some properties may be
2230722319
// missing from the `constraintType` which will otherwise be mapped in the object
22308-
const modifiersType = getApparentType(getModifiersTypeFromMappedType(targetType));
22309-
const mappedKeys: Type[] = [];
22310-
forEachMappedTypePropertyKeyTypeAndIndexSignatureKeyType(
22311-
modifiersType,
22312-
TypeFlags.StringOrNumberLiteralOrUnique,
22313-
/*stringsOnly*/ false,
22314-
t => void mappedKeys.push(instantiateType(nameType, appendTypeMapping(targetType.mapper, getTypeParameterFromMappedType(targetType), t))),
22315-
);
22320+
const mappedKeys = getApparentMappedTypeKeys(nameType, targetType);
2231622321
// 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)
22317-
targetKeys = getUnionType([...mappedKeys, nameType]);
22322+
targetKeys = getUnionType([mappedKeys, nameType]);
2231822323
}
2231922324
else {
2232022325
targetKeys = nameType || constraintType;
@@ -22507,9 +22512,23 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2250722512
}
2250822513
}
2250922514
else if (sourceFlags & TypeFlags.Index) {
22510-
if (result = isRelatedTo(keyofConstraintType, target, RecursionFlags.Source, reportErrors)) {
22515+
const isDeferredMappedIndex = shouldDeferIndexType((source as IndexType).type, (source as IndexType).indexFlags) && getObjectFlags((source as IndexType).type) & ObjectFlags.Mapped;
22516+
if (result = isRelatedTo(keyofConstraintType, target, RecursionFlags.Source, reportErrors && !isDeferredMappedIndex)) {
2251122517
return result;
2251222518
}
22519+
if (isDeferredMappedIndex) {
22520+
const mappedType = (source as IndexType).type as MappedType;
22521+
const nameType = getNameTypeFromMappedType(mappedType)
22522+
// Unlike on the target side, on the source side we do *not* include the generic part of the `nameType`, since that comes from a
22523+
// (potentially anonymous) mapped type local type parameter, so that'd never assign outside the mapped type body, but we still want to
22524+
// allow assignments of index types of identical (or similar enough) mapped types.
22525+
// 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`).
22526+
// 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.
22527+
const sourceMappedKeys = nameType && isMappedTypeWithKeyofConstraintDeclaration(mappedType) ? getApparentMappedTypeKeys(nameType, mappedType) : (nameType || getConstraintTypeFromMappedType(mappedType));
22528+
if (result = isRelatedTo(sourceMappedKeys, target, RecursionFlags.Source, reportErrors)) {
22529+
return result;
22530+
}
22531+
}
2251322532
}
2251422533
else if (sourceFlags & TypeFlags.TemplateLiteral && !(targetFlags & TypeFlags.Object)) {
2251522534
if (!(targetFlags & TypeFlags.TemplateLiteral)) {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//// [tests/cases/compiler/declarationEmitNestedAnonymousMappedType.ts] ////
2+
3+
//// [declarationEmitNestedAnonymousMappedType.ts]
4+
export function enumFromStrings<const Members extends readonly string[]>() {
5+
type Part1 = {
6+
[key in keyof Members as Members[key] extends string
7+
? Members[key]
8+
: never]: Members[key];
9+
};
10+
type Part2 = { [Property in keyof Part1]: Part1[Property] };
11+
return Object.create(null) as Part2;
12+
}
13+
14+
15+
//// [declarationEmitNestedAnonymousMappedType.js]
16+
"use strict";
17+
Object.defineProperty(exports, "__esModule", { value: true });
18+
exports.enumFromStrings = void 0;
19+
function enumFromStrings() {
20+
return Object.create(null);
21+
}
22+
exports.enumFromStrings = enumFromStrings;
23+
24+
25+
//// [declarationEmitNestedAnonymousMappedType.d.ts]
26+
export declare function enumFromStrings<const Members extends readonly string[]>(): { [Property in keyof { [key in keyof Members as Members[key] extends string ? Members[key] : never]: Members[key]; }]: { [key in keyof Members as Members[key] extends string ? Members[key] : never]: Members[key]; }[Property]; };
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//// [tests/cases/compiler/declarationEmitNestedAnonymousMappedType.ts] ////
2+
3+
=== declarationEmitNestedAnonymousMappedType.ts ===
4+
export function enumFromStrings<const Members extends readonly string[]>() {
5+
>enumFromStrings : Symbol(enumFromStrings, Decl(declarationEmitNestedAnonymousMappedType.ts, 0, 0))
6+
>Members : Symbol(Members, Decl(declarationEmitNestedAnonymousMappedType.ts, 0, 32))
7+
8+
type Part1 = {
9+
>Part1 : Symbol(Part1, Decl(declarationEmitNestedAnonymousMappedType.ts, 0, 76))
10+
11+
[key in keyof Members as Members[key] extends string
12+
>key : Symbol(key, Decl(declarationEmitNestedAnonymousMappedType.ts, 2, 9))
13+
>Members : Symbol(Members, Decl(declarationEmitNestedAnonymousMappedType.ts, 0, 32))
14+
>Members : Symbol(Members, Decl(declarationEmitNestedAnonymousMappedType.ts, 0, 32))
15+
>key : Symbol(key, Decl(declarationEmitNestedAnonymousMappedType.ts, 2, 9))
16+
17+
? Members[key]
18+
>Members : Symbol(Members, Decl(declarationEmitNestedAnonymousMappedType.ts, 0, 32))
19+
>key : Symbol(key, Decl(declarationEmitNestedAnonymousMappedType.ts, 2, 9))
20+
21+
: never]: Members[key];
22+
>Members : Symbol(Members, Decl(declarationEmitNestedAnonymousMappedType.ts, 0, 32))
23+
>key : Symbol(key, Decl(declarationEmitNestedAnonymousMappedType.ts, 2, 9))
24+
25+
};
26+
type Part2 = { [Property in keyof Part1]: Part1[Property] };
27+
>Part2 : Symbol(Part2, Decl(declarationEmitNestedAnonymousMappedType.ts, 5, 6))
28+
>Property : Symbol(Property, Decl(declarationEmitNestedAnonymousMappedType.ts, 6, 20))
29+
>Part1 : Symbol(Part1, Decl(declarationEmitNestedAnonymousMappedType.ts, 0, 76))
30+
>Part1 : Symbol(Part1, Decl(declarationEmitNestedAnonymousMappedType.ts, 0, 76))
31+
>Property : Symbol(Property, Decl(declarationEmitNestedAnonymousMappedType.ts, 6, 20))
32+
33+
return Object.create(null) as Part2;
34+
>Object.create : Symbol(ObjectConstructor.create, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
35+
>Object : Symbol(Object, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
36+
>create : Symbol(ObjectConstructor.create, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
37+
>Part2 : Symbol(Part2, Decl(declarationEmitNestedAnonymousMappedType.ts, 5, 6))
38+
}
39+
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//// [tests/cases/compiler/declarationEmitNestedAnonymousMappedType.ts] ////
2+
3+
=== declarationEmitNestedAnonymousMappedType.ts ===
4+
export function enumFromStrings<const Members extends readonly string[]>() {
5+
>enumFromStrings : <const Members extends readonly string[]>() => { [Property in keyof { [key in keyof Members as Members[key] extends string ? Members[key] : never]: Members[key]; }]: { [key in keyof Members as Members[key] extends string ? Members[key] : never]: Members[key]; }[Property]; }
6+
7+
type Part1 = {
8+
>Part1 : { [key in keyof Members as Members[key] extends string ? Members[key] : never]: Members[key]; }
9+
10+
[key in keyof Members as Members[key] extends string
11+
? Members[key]
12+
: never]: Members[key];
13+
};
14+
type Part2 = { [Property in keyof Part1]: Part1[Property] };
15+
>Part2 : { [Property in keyof { [key in keyof Members as Members[key] extends string ? Members[key] : never]: Members[key]; }]: { [key in keyof Members as Members[key] extends string ? Members[key] : never]: Members[key]; }[Property]; }
16+
17+
return Object.create(null) as Part2;
18+
>Object.create(null) as Part2 : { [Property in keyof { [key in keyof Members as Members[key] extends string ? Members[key] : never]: Members[key]; }]: { [key in keyof Members as Members[key] extends string ? Members[key] : never]: Members[key]; }[Property]; }
19+
>Object.create(null) : any
20+
>Object.create : { (o: object): any; (o: object, properties: PropertyDescriptorMap & ThisType<any>): any; }
21+
>Object : ObjectConstructor
22+
>create : { (o: object): any; (o: object, properties: PropertyDescriptorMap & ThisType<any>): any; }
23+
}
24+
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// @declaration: true
2+
export function enumFromStrings<const Members extends readonly string[]>() {
3+
type Part1 = {
4+
[key in keyof Members as Members[key] extends string
5+
? Members[key]
6+
: never]: Members[key];
7+
};
8+
type Part2 = { [Property in keyof Part1]: Part1[Property] };
9+
return Object.create(null) as Part2;
10+
}

0 commit comments

Comments
 (0)