Skip to content

Declaration emit for inlined mapped types preserves modifier-preserving behavior #55054

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
25 changes: 25 additions & 0 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6641,6 +6641,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const questionToken = type.declaration.questionToken ? factory.createToken(type.declaration.questionToken.kind) as QuestionToken | PlusToken | MinusToken : undefined;
let appropriateConstraintTypeNode: TypeNode;
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// If the mapped type isn't `keyof` constraint-declared, _but_ still has modifiers preserved, and its' naive instantiation won't preserve modifiers because its' constraint isn't `keyof` constrained, we have work to do
// If the mapped type isn't `keyof` constraint-declared, _but_ still has modifiers preserved, and its naive instantiation won't preserve modifiers because its constraint isn't `keyof` constrained, we have work to do

let newTypeVariable: TypeReferenceNode | undefined;
// If the mapped type isn't `keyof` constraint-declared, _but_ still has modifiers preserved, and its naive instantiation won't preserve modifiers because its constraint isn't `keyof` constrained, we have work to do
Copy link
Member

Choose a reason for hiding this comment

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

dprint when (this is hard to read with 2 predicates per line, each of which are so long that they wrap on my screen)

const needsModifierPreservingWrapper = !isMappedTypeWithKeyofConstraintDeclaration(type)
&& !(getModifiersTypeFromMappedType(type).flags & TypeFlags.Unknown)
&& context.flags & NodeBuilderFlags.GenerateNamesForShadowedTypeParams
&& !(getConstraintTypeFromMappedType(type).flags & TypeFlags.TypeParameter && getConstraintOfTypeParameter(getConstraintTypeFromMappedType(type))?.flags! & TypeFlags.Index);
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`
Expand All @@ -6651,6 +6656,14 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
appropriateConstraintTypeNode = factory.createTypeOperatorNode(SyntaxKind.KeyOfKeyword, newTypeVariable || typeToTypeNodeHelper(getModifiersTypeFromMappedType(type), context));
}
else if (needsModifierPreservingWrapper) {
// So, step 1: new type variable
const newParam = createTypeParameter(createSymbol(SymbolFlags.TypeParameter, "T" as __String));
const name = typeParameterToName(newParam, context);
newTypeVariable = factory.createTypeReferenceNode(name);
// step 2: make that new type variable itself the constraint node, making the mapped type `{[K in T_1]: Template}`
appropriateConstraintTypeNode = newTypeVariable;
}
else {
appropriateConstraintTypeNode = typeToTypeNodeHelper(getConstraintTypeFromMappedType(type), context);
}
Expand All @@ -6672,6 +6685,18 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
factory.createKeywordTypeNode(SyntaxKind.NeverKeyword)
);
}
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// constrained to a `keyof` type to preserve it's modifier-preserving behavior. This is all basically because we preserve modifiers for a wider set of mapped types than
// constrained to a `keyof` type to preserve its modifier-preserving behavior. This is all basically because we preserve modifiers for a wider set of mapped types than

double-checking: is the effect of this PR that we now preserve modifiers for these mapped types, and didn't before?

Copy link
Contributor

Choose a reason for hiding this comment

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

without reading the code of the fix itself - yes, the issue that I reported is specifically about the fact that modifiers are lost but they shouldn't be so the fix for that issue should preserve them

else if (needsModifierPreservingWrapper) {
// and step 3: once the mapped type is reconstructed, create a `ConstraintType extends infer T_1 extends keyof ModifiersType ? {[K in T_1]: Template} : never`
// subtly different from the `keyof` constraint case, by including the `keyof` constraint on the `infer` type parameter, it doesn't rely on the constraint type being itself
// constrained to a `keyof` type to preserve its modifier-preserving behavior. This is all basically because we preserve modifiers for a wider set of mapped types than
// just homomorphic ones.
return factory.createConditionalTypeNode(
typeToTypeNodeHelper(getConstraintTypeFromMappedType(type), context),
factory.createInferTypeNode(factory.createTypeParameterDeclaration(/*modifiers*/ undefined, factory.cloneNode(newTypeVariable!.typeName) as Identifier, factory.createTypeOperatorNode(SyntaxKind.KeyOfKeyword, typeToTypeNodeHelper(getModifiersTypeFromMappedType(type), context)))),
result,
factory.createKeywordTypeNode(SyntaxKind.NeverKeyword)
);
}
return result;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
//// [tests/cases/compiler/inlineMappedTypeModifierDeclarationEmit.ts] ////

//// [index.ts]
import { test1, test2 } from "./other";

export function wrappedTest1<T, K extends string>(obj: T, k: K) {
return test1(obj, k);
}

export function wrappedTest2<T, K extends string>(obj: T, k: K) {
return test2(obj, k);
}

export type Obj = {
a: number;
readonly foo: string;
};

export const processedInternally1 = wrappedTest1({} as Obj, "a");
export const processedInternally2 = wrappedTest2({} as Obj, "a");
//// [other.ts]
// how Omit from lib is defined
type OmitReal<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
// what we see when we hover it
type OmitUnveiled<T, K extends string | number | symbol> = {
[P in Exclude<keyof T, K>]: T[P];
};

export function test1<T, K extends string>(obj: T, k: K): OmitReal<T, K> {
return {} as any;
}

export function test2<T, K extends string>(obj: T, k: K): OmitUnveiled<T, K> {
return {} as any;
}

//// [other.js]
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.test2 = exports.test1 = void 0;
function test1(obj, k) {
return {};
}
exports.test1 = test1;
function test2(obj, k) {
return {};
}
exports.test2 = test2;
//// [index.js]
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.processedInternally2 = exports.processedInternally1 = exports.wrappedTest2 = exports.wrappedTest1 = void 0;
var other_1 = require("./other");
function wrappedTest1(obj, k) {
return (0, other_1.test1)(obj, k);
}
exports.wrappedTest1 = wrappedTest1;
function wrappedTest2(obj, k) {
return (0, other_1.test2)(obj, k);
}
exports.wrappedTest2 = wrappedTest2;
exports.processedInternally1 = wrappedTest1({}, "a");
exports.processedInternally2 = wrappedTest2({}, "a");


//// [other.d.ts]
type OmitReal<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
type OmitUnveiled<T, K extends string | number | symbol> = {
[P in Exclude<keyof T, K>]: T[P];
};
export declare function test1<T, K extends string>(obj: T, k: K): OmitReal<T, K>;
export declare function test2<T, K extends string>(obj: T, k: K): OmitUnveiled<T, K>;
export {};
//// [index.d.ts]
export declare function wrappedTest1<T, K extends string>(obj: T, k: K): Exclude<keyof T, K> extends infer T_1 extends keyof T ? { [P in T_1]: T[P]; } : never;
export declare function wrappedTest2<T, K extends string>(obj: T, k: K): { [P in Exclude<keyof T, K>]: T[P]; };
export type Obj = {
a: number;
readonly foo: string;
};
export declare const processedInternally1: {
readonly foo: string;
};
export declare const processedInternally2: {
foo: string;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
//// [tests/cases/compiler/inlineMappedTypeModifierDeclarationEmit.ts] ////

=== index.ts ===
import { test1, test2 } from "./other";
>test1 : Symbol(test1, Decl(index.ts, 0, 8))
>test2 : Symbol(test2, Decl(index.ts, 0, 15))

export function wrappedTest1<T, K extends string>(obj: T, k: K) {
>wrappedTest1 : Symbol(wrappedTest1, Decl(index.ts, 0, 39))
>T : Symbol(T, Decl(index.ts, 2, 29))
>K : Symbol(K, Decl(index.ts, 2, 31))
>obj : Symbol(obj, Decl(index.ts, 2, 50))
>T : Symbol(T, Decl(index.ts, 2, 29))
>k : Symbol(k, Decl(index.ts, 2, 57))
>K : Symbol(K, Decl(index.ts, 2, 31))

return test1(obj, k);
>test1 : Symbol(test1, Decl(index.ts, 0, 8))
>obj : Symbol(obj, Decl(index.ts, 2, 50))
>k : Symbol(k, Decl(index.ts, 2, 57))
}

export function wrappedTest2<T, K extends string>(obj: T, k: K) {
>wrappedTest2 : Symbol(wrappedTest2, Decl(index.ts, 4, 1))
>T : Symbol(T, Decl(index.ts, 6, 29))
>K : Symbol(K, Decl(index.ts, 6, 31))
>obj : Symbol(obj, Decl(index.ts, 6, 50))
>T : Symbol(T, Decl(index.ts, 6, 29))
>k : Symbol(k, Decl(index.ts, 6, 57))
>K : Symbol(K, Decl(index.ts, 6, 31))

return test2(obj, k);
>test2 : Symbol(test2, Decl(index.ts, 0, 15))
>obj : Symbol(obj, Decl(index.ts, 6, 50))
>k : Symbol(k, Decl(index.ts, 6, 57))
}

export type Obj = {
>Obj : Symbol(Obj, Decl(index.ts, 8, 1))

a: number;
>a : Symbol(a, Decl(index.ts, 10, 19))

readonly foo: string;
>foo : Symbol(foo, Decl(index.ts, 11, 12))

};

export const processedInternally1 = wrappedTest1({} as Obj, "a");
>processedInternally1 : Symbol(processedInternally1, Decl(index.ts, 15, 12))
>wrappedTest1 : Symbol(wrappedTest1, Decl(index.ts, 0, 39))
>Obj : Symbol(Obj, Decl(index.ts, 8, 1))

export const processedInternally2 = wrappedTest2({} as Obj, "a");
>processedInternally2 : Symbol(processedInternally2, Decl(index.ts, 16, 12))
>wrappedTest2 : Symbol(wrappedTest2, Decl(index.ts, 4, 1))
>Obj : Symbol(Obj, Decl(index.ts, 8, 1))

=== other.ts ===
// how Omit from lib is defined
type OmitReal<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
>OmitReal : Symbol(OmitReal, Decl(other.ts, 0, 0))
>T : Symbol(T, Decl(other.ts, 1, 14))
>K : Symbol(K, Decl(other.ts, 1, 16))
>Pick : Symbol(Pick, Decl(lib.es5.d.ts, --, --))
>T : Symbol(T, Decl(other.ts, 1, 14))
>Exclude : Symbol(Exclude, Decl(lib.es5.d.ts, --, --))
>T : Symbol(T, Decl(other.ts, 1, 14))
>K : Symbol(K, Decl(other.ts, 1, 16))

// what we see when we hover it
type OmitUnveiled<T, K extends string | number | symbol> = {
>OmitUnveiled : Symbol(OmitUnveiled, Decl(other.ts, 1, 69))
>T : Symbol(T, Decl(other.ts, 3, 18))
>K : Symbol(K, Decl(other.ts, 3, 20))

[P in Exclude<keyof T, K>]: T[P];
>P : Symbol(P, Decl(other.ts, 4, 3))
>Exclude : Symbol(Exclude, Decl(lib.es5.d.ts, --, --))
>T : Symbol(T, Decl(other.ts, 3, 18))
>K : Symbol(K, Decl(other.ts, 3, 20))
>T : Symbol(T, Decl(other.ts, 3, 18))
>P : Symbol(P, Decl(other.ts, 4, 3))

};

export function test1<T, K extends string>(obj: T, k: K): OmitReal<T, K> {
>test1 : Symbol(test1, Decl(other.ts, 5, 2))
>T : Symbol(T, Decl(other.ts, 7, 22))
>K : Symbol(K, Decl(other.ts, 7, 24))
>obj : Symbol(obj, Decl(other.ts, 7, 43))
>T : Symbol(T, Decl(other.ts, 7, 22))
>k : Symbol(k, Decl(other.ts, 7, 50))
>K : Symbol(K, Decl(other.ts, 7, 24))
>OmitReal : Symbol(OmitReal, Decl(other.ts, 0, 0))
>T : Symbol(T, Decl(other.ts, 7, 22))
>K : Symbol(K, Decl(other.ts, 7, 24))

return {} as any;
}

export function test2<T, K extends string>(obj: T, k: K): OmitUnveiled<T, K> {
>test2 : Symbol(test2, Decl(other.ts, 9, 1))
>T : Symbol(T, Decl(other.ts, 11, 22))
>K : Symbol(K, Decl(other.ts, 11, 24))
>obj : Symbol(obj, Decl(other.ts, 11, 43))
>T : Symbol(T, Decl(other.ts, 11, 22))
>k : Symbol(k, Decl(other.ts, 11, 50))
>K : Symbol(K, Decl(other.ts, 11, 24))
>OmitUnveiled : Symbol(OmitUnveiled, Decl(other.ts, 1, 69))
>T : Symbol(T, Decl(other.ts, 11, 22))
>K : Symbol(K, Decl(other.ts, 11, 24))

return {} as any;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//// [tests/cases/compiler/inlineMappedTypeModifierDeclarationEmit.ts] ////

=== index.ts ===
import { test1, test2 } from "./other";
>test1 : <T, K extends string>(obj: T, k: K) => { [P in Exclude<keyof T, K>]: T[P]; }
>test2 : <T, K extends string>(obj: T, k: K) => { [P in Exclude<keyof T, K>]: T[P]; }

export function wrappedTest1<T, K extends string>(obj: T, k: K) {
>wrappedTest1 : <T, K extends string>(obj: T, k: K) => { [P in Exclude<keyof T, K>]: T[P]; }
>obj : T
>k : K

return test1(obj, k);
>test1(obj, k) : { [P in Exclude<keyof T, K>]: T[P]; }
>test1 : <T, K extends string>(obj: T, k: K) => { [P in Exclude<keyof T, K>]: T[P]; }
>obj : T
>k : K
}

export function wrappedTest2<T, K extends string>(obj: T, k: K) {
>wrappedTest2 : <T, K extends string>(obj: T, k: K) => { [P in Exclude<keyof T, K>]: T[P]; }
>obj : T
>k : K

return test2(obj, k);
>test2(obj, k) : { [P in Exclude<keyof T, K>]: T[P]; }
>test2 : <T, K extends string>(obj: T, k: K) => { [P in Exclude<keyof T, K>]: T[P]; }
>obj : T
>k : K
}

export type Obj = {
>Obj : { a: number; readonly foo: string; }

a: number;
>a : number

readonly foo: string;
>foo : string

};

export const processedInternally1 = wrappedTest1({} as Obj, "a");
>processedInternally1 : { readonly foo: string; }
>wrappedTest1({} as Obj, "a") : { readonly foo: string; }
>wrappedTest1 : <T, K extends string>(obj: T, k: K) => { [P in Exclude<keyof T, K>]: T[P]; }
>{} as Obj : Obj
>{} : {}
>"a" : "a"

export const processedInternally2 = wrappedTest2({} as Obj, "a");
>processedInternally2 : { foo: string; }
>wrappedTest2({} as Obj, "a") : { foo: string; }
>wrappedTest2 : <T, K extends string>(obj: T, k: K) => { [P in Exclude<keyof T, K>]: T[P]; }
>{} as Obj : Obj
>{} : {}
>"a" : "a"

=== other.ts ===
// how Omit from lib is defined
type OmitReal<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
>OmitReal : OmitReal<T, K>

// what we see when we hover it
type OmitUnveiled<T, K extends string | number | symbol> = {
>OmitUnveiled : OmitUnveiled<T, K>

[P in Exclude<keyof T, K>]: T[P];
};

export function test1<T, K extends string>(obj: T, k: K): OmitReal<T, K> {
>test1 : <T, K extends string>(obj: T, k: K) => OmitReal<T, K>
>obj : T
>k : K

return {} as any;
>{} as any : any
>{} : {}
}

export function test2<T, K extends string>(obj: T, k: K): OmitUnveiled<T, K> {
>test2 : <T, K extends string>(obj: T, k: K) => OmitUnveiled<T, K>
>obj : T
>k : K

return {} as any;
>{} as any : any
>{} : {}
}
34 changes: 34 additions & 0 deletions tests/cases/compiler/inlineMappedTypeModifierDeclarationEmit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// @declaration: true
// @filename: index.ts
import { test1, test2 } from "./other";

export function wrappedTest1<T, K extends string>(obj: T, k: K) {
return test1(obj, k);
}

export function wrappedTest2<T, K extends string>(obj: T, k: K) {
return test2(obj, k);
}

export type Obj = {
a: number;
readonly foo: string;
};

export const processedInternally1 = wrappedTest1({} as Obj, "a");
export const processedInternally2 = wrappedTest2({} as Obj, "a");
// @filename: other.ts
// how Omit from lib is defined
type OmitReal<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
// what we see when we hover it
type OmitUnveiled<T, K extends string | number | symbol> = {
[P in Exclude<keyof T, K>]: T[P];
};

export function test1<T, K extends string>(obj: T, k: K): OmitReal<T, K> {
return {} as any;
}

export function test2<T, K extends string>(obj: T, k: K): OmitUnveiled<T, K> {
return {} as any;
}