Skip to content

Commit

Permalink
When relating a deferred index type over a mapped type on the source …
Browse files Browse the repository at this point in the history
…side, actually compare against the mapped type's apparent keys
  • Loading branch information
weswigham committed Dec 11, 2023
1 parent 0c2dea5 commit b6e3704
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 10 deletions.
39 changes: 29 additions & 10 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof captureErrorCalculationState>): Ternary {
let result: Ternary;
let originalErrorInfo: DiagnosticMessageChain | undefined;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//// [tests/cases/compiler/declarationEmitNestedAnonymousMappedType.ts] ////

//// [declarationEmitNestedAnonymousMappedType.ts]
export function enumFromStrings<const Members extends readonly string[]>() {
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<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]; };
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//// [tests/cases/compiler/declarationEmitNestedAnonymousMappedType.ts] ////

=== declarationEmitNestedAnonymousMappedType.ts ===
export function enumFromStrings<const Members extends readonly string[]>() {
>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))
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//// [tests/cases/compiler/declarationEmitNestedAnonymousMappedType.ts] ////

=== declarationEmitNestedAnonymousMappedType.ts ===
export function enumFromStrings<const Members extends readonly string[]>() {
>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]; }

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>): any; }
>Object : ObjectConstructor
>create : { (o: object): any; (o: object, properties: PropertyDescriptorMap & ThisType<any>): any; }
}

10 changes: 10 additions & 0 deletions tests/cases/compiler/declarationEmitNestedAnonymousMappedType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// @declaration: true
export function enumFromStrings<const Members extends readonly string[]>() {
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;
}

0 comments on commit b6e3704

Please sign in to comment.