diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index c56560671c304..bdc4cefd9a1dd 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -22142,6 +22142,18 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return result; } + function getApparentMappedTypeKeys(nameType: Type, targetType: MappedType) { + 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))), + ); + return getUnionType(mappedKeys); + } + function structuredTypeRelatedToWorker(source: Type, target: Type, reportErrors: boolean, intersectionState: IntersectionState, saveErrorInfo: ReturnType): Ternary { let result: Ternary; let originalErrorInfo: DiagnosticMessageChain | undefined; @@ -22305,16 +22317,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { 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))), - ); + const mappedKeys = getApparentMappedTypeKeys(nameType, targetType); // 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]); + targetKeys = getUnionType([mappedKeys, nameType]); } else { targetKeys = nameType || constraintType; @@ -22507,9 +22512,23 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } } else if (sourceFlags & TypeFlags.Index) { - if (result = isRelatedTo(keyofConstraintType, target, RecursionFlags.Source, reportErrors)) { + const isDeferredMappedIndex = shouldDeferIndexType((source as IndexType).type, (source as IndexType).indexFlags) && getObjectFlags((source as IndexType).type) & ObjectFlags.Mapped; + if (result = isRelatedTo(keyofConstraintType, target, RecursionFlags.Source, reportErrors && !isDeferredMappedIndex)) { return result; } + if (isDeferredMappedIndex) { + const mappedType = (source as IndexType).type as MappedType; + const nameType = getNameTypeFromMappedType(mappedType); + // Unlike on the target side, on the source side we do *not* include the generic part of the `nameType`, since that comes from a + // (potentially anonymous) mapped type local type parameter, so that'd never assign outside the mapped type body, but we still want to + // 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)); + if (result = isRelatedTo(sourceMappedKeys, target, RecursionFlags.Source, reportErrors)) { + return result; + } + } } else if (sourceFlags & TypeFlags.TemplateLiteral && !(targetFlags & TypeFlags.Object)) { if (!(targetFlags & TypeFlags.TemplateLiteral)) { diff --git a/tests/baselines/reference/declarationEmitNestedAnonymousMappedType.js b/tests/baselines/reference/declarationEmitNestedAnonymousMappedType.js new file mode 100644 index 0000000000000..990be7d639417 --- /dev/null +++ b/tests/baselines/reference/declarationEmitNestedAnonymousMappedType.js @@ -0,0 +1,26 @@ +//// [tests/cases/compiler/declarationEmitNestedAnonymousMappedType.ts] //// + +//// [declarationEmitNestedAnonymousMappedType.ts] +export function enumFromStrings() { + type Part1 = { + [key in keyof Members as Members[key] extends string + ? Members[key] + : never]: Members[key]; + }; + type Part2 = { [Property in keyof Part1]: Part1[Property] }; + return Object.create(null) as Part2; +} + + +//// [declarationEmitNestedAnonymousMappedType.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.enumFromStrings = void 0; +function enumFromStrings() { + return Object.create(null); +} +exports.enumFromStrings = enumFromStrings; + + +//// [declarationEmitNestedAnonymousMappedType.d.ts] +export declare function enumFromStrings(): { [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]; }; diff --git a/tests/baselines/reference/declarationEmitNestedAnonymousMappedType.symbols b/tests/baselines/reference/declarationEmitNestedAnonymousMappedType.symbols new file mode 100644 index 0000000000000..14ac1faaa8487 --- /dev/null +++ b/tests/baselines/reference/declarationEmitNestedAnonymousMappedType.symbols @@ -0,0 +1,39 @@ +//// [tests/cases/compiler/declarationEmitNestedAnonymousMappedType.ts] //// + +=== declarationEmitNestedAnonymousMappedType.ts === +export function enumFromStrings() { +>enumFromStrings : Symbol(enumFromStrings, Decl(declarationEmitNestedAnonymousMappedType.ts, 0, 0)) +>Members : Symbol(Members, Decl(declarationEmitNestedAnonymousMappedType.ts, 0, 32)) + + type Part1 = { +>Part1 : Symbol(Part1, Decl(declarationEmitNestedAnonymousMappedType.ts, 0, 76)) + + [key in keyof Members as Members[key] extends string +>key : Symbol(key, Decl(declarationEmitNestedAnonymousMappedType.ts, 2, 9)) +>Members : Symbol(Members, Decl(declarationEmitNestedAnonymousMappedType.ts, 0, 32)) +>Members : Symbol(Members, Decl(declarationEmitNestedAnonymousMappedType.ts, 0, 32)) +>key : Symbol(key, Decl(declarationEmitNestedAnonymousMappedType.ts, 2, 9)) + + ? Members[key] +>Members : Symbol(Members, Decl(declarationEmitNestedAnonymousMappedType.ts, 0, 32)) +>key : Symbol(key, Decl(declarationEmitNestedAnonymousMappedType.ts, 2, 9)) + + : never]: Members[key]; +>Members : Symbol(Members, Decl(declarationEmitNestedAnonymousMappedType.ts, 0, 32)) +>key : Symbol(key, Decl(declarationEmitNestedAnonymousMappedType.ts, 2, 9)) + + }; + type Part2 = { [Property in keyof Part1]: Part1[Property] }; +>Part2 : Symbol(Part2, Decl(declarationEmitNestedAnonymousMappedType.ts, 5, 6)) +>Property : Symbol(Property, Decl(declarationEmitNestedAnonymousMappedType.ts, 6, 20)) +>Part1 : Symbol(Part1, Decl(declarationEmitNestedAnonymousMappedType.ts, 0, 76)) +>Part1 : Symbol(Part1, Decl(declarationEmitNestedAnonymousMappedType.ts, 0, 76)) +>Property : Symbol(Property, Decl(declarationEmitNestedAnonymousMappedType.ts, 6, 20)) + + return Object.create(null) as Part2; +>Object.create : Symbol(ObjectConstructor.create, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>Object : Symbol(Object, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>create : Symbol(ObjectConstructor.create, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>Part2 : Symbol(Part2, Decl(declarationEmitNestedAnonymousMappedType.ts, 5, 6)) +} + diff --git a/tests/baselines/reference/declarationEmitNestedAnonymousMappedType.types b/tests/baselines/reference/declarationEmitNestedAnonymousMappedType.types new file mode 100644 index 0000000000000..393f26ed21c44 --- /dev/null +++ b/tests/baselines/reference/declarationEmitNestedAnonymousMappedType.types @@ -0,0 +1,24 @@ +//// [tests/cases/compiler/declarationEmitNestedAnonymousMappedType.ts] //// + +=== declarationEmitNestedAnonymousMappedType.ts === +export function enumFromStrings() { +>enumFromStrings : () => { [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]; } + + type Part1 = { +>Part1 : { [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]; + }; + type Part2 = { [Property in keyof Part1]: Part1[Property] }; +>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]; } + + return Object.create(null) as Part2; +>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]; } +>Object.create(null) : any +>Object.create : { (o: object): any; (o: object, properties: PropertyDescriptorMap & ThisType): any; } +>Object : ObjectConstructor +>create : { (o: object): any; (o: object, properties: PropertyDescriptorMap & ThisType): any; } +} + diff --git a/tests/cases/compiler/declarationEmitNestedAnonymousMappedType.ts b/tests/cases/compiler/declarationEmitNestedAnonymousMappedType.ts new file mode 100644 index 0000000000000..29db810629b13 --- /dev/null +++ b/tests/cases/compiler/declarationEmitNestedAnonymousMappedType.ts @@ -0,0 +1,10 @@ +// @declaration: true +export function enumFromStrings() { + type Part1 = { + [key in keyof Members as Members[key] extends string + ? Members[key] + : never]: Members[key]; + }; + type Part2 = { [Property in keyof Part1]: Part1[Property] }; + return Object.create(null) as Part2; +}