Skip to content

Preserve the homomorphism of inlined mapped types in declaration emit #48091

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5079,10 +5079,16 @@ namespace ts {
const readonlyToken = type.declaration.readonlyToken ? factory.createToken(type.declaration.readonlyToken.kind) as ReadonlyKeyword | PlusToken | MinusToken : undefined;
const questionToken = type.declaration.questionToken ? factory.createToken(type.declaration.questionToken.kind) as QuestionToken | PlusToken | MinusToken : undefined;
let appropriateConstraintTypeNode: TypeNode;
let newTypeVariable: TypeReferenceNode | undefined;
if (isMappedTypeWithKeyofConstraintDeclaration(type)) {
// We have a { [P in keyof T]: X }
// We do this to ensure we retain the toplevel keyof-ness of the type which may be lost due to keyof distribution during `getConstraintTypeFromMappedType`
appropriateConstraintTypeNode = factory.createTypeOperatorNode(SyntaxKind.KeyOfKeyword, typeToTypeNodeHelper(getModifiersTypeFromMappedType(type), context));
if (!(getModifiersTypeFromMappedType(type).flags & TypeFlags.TypeParameter) && context.flags & NodeBuilderFlags.GenerateNamesForShadowedTypeParams) {
const newParam = createTypeParameter(createSymbol(SymbolFlags.TypeParameter, "T" as __String));
const name = typeParameterToName(newParam, context);
newTypeVariable = factory.createTypeReferenceNode(name);
}
appropriateConstraintTypeNode = factory.createTypeOperatorNode(SyntaxKind.KeyOfKeyword, newTypeVariable || typeToTypeNodeHelper(getModifiersTypeFromMappedType(type), context));
}
else {
appropriateConstraintTypeNode = typeToTypeNodeHelper(getConstraintTypeFromMappedType(type), context);
Expand All @@ -5092,7 +5098,19 @@ namespace ts {
const templateTypeNode = typeToTypeNodeHelper(removeMissingType(getTemplateTypeFromMappedType(type), !!(getMappedTypeModifiers(type) & MappedTypeModifiers.IncludeOptional)), context);
const mappedTypeNode = factory.createMappedTypeNode(readonlyToken, typeParameterNode, nameTypeNode, questionToken, templateTypeNode, /*members*/ undefined);
context.approximateLength += 10;
return setEmitFlags(mappedTypeNode, EmitFlags.SingleLine);
const result = setEmitFlags(mappedTypeNode, EmitFlags.SingleLine);
if (isMappedTypeWithKeyofConstraintDeclaration(type) && !(getModifiersTypeFromMappedType(type).flags & TypeFlags.TypeParameter) && context.flags & NodeBuilderFlags.GenerateNamesForShadowedTypeParams) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it probably doesn't make sense to precalculate this predicate; on the other hand, it's confusing to read as-is and a name would help.

In particular, I don't understand why the modifiers type needs to be not-a-type-parameter. Can you give an example?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the modifiers type is a type parameter, it'll preserve the homomorphism of the mapped type on its own - we don't need to substitute in a temporary type parameter.

// homomorphic mapped type with a non-homomorphic naive inlining
// wrap it with a conditional like `SomeModifiersType extends infer U ? {..the mapped type...} : never` to ensure the resulting
// type stays homomorphic
return factory.createConditionalTypeNode(
typeToTypeNodeHelper(getModifiersTypeFromMappedType(type), context),
factory.createInferTypeNode(factory.createTypeParameterDeclaration(factory.cloneNode(newTypeVariable!.typeName) as Identifier)),
result,
factory.createKeywordTypeNode(SyntaxKind.NeverKeyword)
);
}
return result;
}

function createAnonymousTypeNode(type: ObjectType): TypeNode {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//// [tests/cases/compiler/mappedTypeGenericInstantiationPreservesHomomorphism.ts] ////

//// [internal.ts]
export declare function usePrivateType<T extends unknown[]>(...args: T): PrivateMapped<T[any]>;

type PrivateMapped<Obj> = {[K in keyof Obj]: Obj[K]};

//// [api.ts]
import {usePrivateType} from './internal';
export const mappedUnionWithPrivateType = <T extends unknown[]>(...args: T) => usePrivateType(...args);


//// [internal.js]
"use strict";
exports.__esModule = true;
//// [api.js]
"use strict";
exports.__esModule = true;
exports.mappedUnionWithPrivateType = void 0;
var internal_1 = require("./internal");
var mappedUnionWithPrivateType = function () {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
return internal_1.usePrivateType.apply(void 0, args);
};
exports.mappedUnionWithPrivateType = mappedUnionWithPrivateType;


//// [internal.d.ts]
export declare function usePrivateType<T extends unknown[]>(...args: T): PrivateMapped<T[any]>;
declare type PrivateMapped<Obj> = {
[K in keyof Obj]: Obj[K];
};
export {};
//// [api.d.ts]
export declare const mappedUnionWithPrivateType: <T extends unknown[]>(...args: T) => T[any] extends infer T_1 ? { [K in keyof T_1]: T[any][K]; } : never;
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
=== tests/cases/compiler/internal.ts ===
export declare function usePrivateType<T extends unknown[]>(...args: T): PrivateMapped<T[any]>;
>usePrivateType : Symbol(usePrivateType, Decl(internal.ts, 0, 0))
>T : Symbol(T, Decl(internal.ts, 0, 39))
>args : Symbol(args, Decl(internal.ts, 0, 60))
>T : Symbol(T, Decl(internal.ts, 0, 39))
>PrivateMapped : Symbol(PrivateMapped, Decl(internal.ts, 0, 95))
>T : Symbol(T, Decl(internal.ts, 0, 39))

type PrivateMapped<Obj> = {[K in keyof Obj]: Obj[K]};
>PrivateMapped : Symbol(PrivateMapped, Decl(internal.ts, 0, 95))
>Obj : Symbol(Obj, Decl(internal.ts, 2, 19))
>K : Symbol(K, Decl(internal.ts, 2, 28))
>Obj : Symbol(Obj, Decl(internal.ts, 2, 19))
>Obj : Symbol(Obj, Decl(internal.ts, 2, 19))
>K : Symbol(K, Decl(internal.ts, 2, 28))

=== tests/cases/compiler/api.ts ===
import {usePrivateType} from './internal';
>usePrivateType : Symbol(usePrivateType, Decl(api.ts, 0, 8))

export const mappedUnionWithPrivateType = <T extends unknown[]>(...args: T) => usePrivateType(...args);
>mappedUnionWithPrivateType : Symbol(mappedUnionWithPrivateType, Decl(api.ts, 1, 12))
>T : Symbol(T, Decl(api.ts, 1, 43))
>args : Symbol(args, Decl(api.ts, 1, 64))
>T : Symbol(T, Decl(api.ts, 1, 43))
>usePrivateType : Symbol(usePrivateType, Decl(api.ts, 0, 8))
>args : Symbol(args, Decl(api.ts, 1, 64))

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
=== tests/cases/compiler/internal.ts ===
export declare function usePrivateType<T extends unknown[]>(...args: T): PrivateMapped<T[any]>;
>usePrivateType : <T extends unknown[]>(...args: T) => PrivateMapped<T[any]>
>args : T

type PrivateMapped<Obj> = {[K in keyof Obj]: Obj[K]};
>PrivateMapped : PrivateMapped<Obj>

=== tests/cases/compiler/api.ts ===
import {usePrivateType} from './internal';
>usePrivateType : <T extends unknown[]>(...args: T) => { [K in keyof T[any]]: T[any][K]; }

export const mappedUnionWithPrivateType = <T extends unknown[]>(...args: T) => usePrivateType(...args);
>mappedUnionWithPrivateType : <T extends unknown[]>(...args: T) => { [K in keyof T[any]]: T[any][K]; }
><T extends unknown[]>(...args: T) => usePrivateType(...args) : <T extends unknown[]>(...args: T) => { [K in keyof T[any]]: T[any][K]; }
>args : T
>usePrivateType(...args) : { [K in keyof T[any]]: T[any][K]; }
>usePrivateType : <T extends unknown[]>(...args: T) => { [K in keyof T[any]]: T[any][K]; }
>...args : unknown
>args : T

Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export declare type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
export declare type PartialProperties<T, K extends keyof T> = Partial<Pick<T, K>> & Omit<T, K>;
export declare function doSomething_Actual<T extends {
prop: string;
}>(a: T): { [P in keyof PartialProperties<T, "prop">]: PartialProperties<T, "prop">[P]; };
}>(a: T): PartialProperties<T, "prop"> extends infer T_1 ? { [P in keyof T_1]: PartialProperties<T, "prop">[P]; } : never;
export declare function doSomething_Expected<T extends {
prop: string;
}>(a: T): {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// @declaration: true
// @filename: internal.ts
export declare function usePrivateType<T extends unknown[]>(...args: T): PrivateMapped<T[any]>;

type PrivateMapped<Obj> = {[K in keyof Obj]: Obj[K]};

// @filename: api.ts
import {usePrivateType} from './internal';
export const mappedUnionWithPrivateType = <T extends unknown[]>(...args: T) => usePrivateType(...args);