Skip to content

Commit 9707336

Browse files
authored
Declaration emit for inlined mapped types preserves modifier-preserving behavior (#55054)
1 parent 34c144d commit 9707336

5 files changed

+349
-0
lines changed

src/compiler/checker.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6641,6 +6641,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
66416641
const questionToken = type.declaration.questionToken ? factory.createToken(type.declaration.questionToken.kind) as QuestionToken | PlusToken | MinusToken : undefined;
66426642
let appropriateConstraintTypeNode: TypeNode;
66436643
let newTypeVariable: TypeReferenceNode | undefined;
6644+
// 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
6645+
const needsModifierPreservingWrapper = !isMappedTypeWithKeyofConstraintDeclaration(type)
6646+
&& !(getModifiersTypeFromMappedType(type).flags & TypeFlags.Unknown)
6647+
&& context.flags & NodeBuilderFlags.GenerateNamesForShadowedTypeParams
6648+
&& !(getConstraintTypeFromMappedType(type).flags & TypeFlags.TypeParameter && getConstraintOfTypeParameter(getConstraintTypeFromMappedType(type))?.flags! & TypeFlags.Index);
66446649
if (isMappedTypeWithKeyofConstraintDeclaration(type)) {
66456650
// We have a { [P in keyof T]: X }
66466651
// We do this to ensure we retain the toplevel keyof-ness of the type which may be lost due to keyof distribution during `getConstraintTypeFromMappedType`
@@ -6651,6 +6656,14 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
66516656
}
66526657
appropriateConstraintTypeNode = factory.createTypeOperatorNode(SyntaxKind.KeyOfKeyword, newTypeVariable || typeToTypeNodeHelper(getModifiersTypeFromMappedType(type), context));
66536658
}
6659+
else if (needsModifierPreservingWrapper) {
6660+
// So, step 1: new type variable
6661+
const newParam = createTypeParameter(createSymbol(SymbolFlags.TypeParameter, "T" as __String));
6662+
const name = typeParameterToName(newParam, context);
6663+
newTypeVariable = factory.createTypeReferenceNode(name);
6664+
// step 2: make that new type variable itself the constraint node, making the mapped type `{[K in T_1]: Template}`
6665+
appropriateConstraintTypeNode = newTypeVariable;
6666+
}
66546667
else {
66556668
appropriateConstraintTypeNode = typeToTypeNodeHelper(getConstraintTypeFromMappedType(type), context);
66566669
}
@@ -6672,6 +6685,18 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
66726685
factory.createKeywordTypeNode(SyntaxKind.NeverKeyword)
66736686
);
66746687
}
6688+
else if (needsModifierPreservingWrapper) {
6689+
// 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`
6690+
// 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
6691+
// 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
6692+
// just homomorphic ones.
6693+
return factory.createConditionalTypeNode(
6694+
typeToTypeNodeHelper(getConstraintTypeFromMappedType(type), context),
6695+
factory.createInferTypeNode(factory.createTypeParameterDeclaration(/*modifiers*/ undefined, factory.cloneNode(newTypeVariable!.typeName) as Identifier, factory.createTypeOperatorNode(SyntaxKind.KeyOfKeyword, typeToTypeNodeHelper(getModifiersTypeFromMappedType(type), context)))),
6696+
result,
6697+
factory.createKeywordTypeNode(SyntaxKind.NeverKeyword)
6698+
);
6699+
}
66756700
return result;
66766701
}
66776702

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
//// [tests/cases/compiler/inlineMappedTypeModifierDeclarationEmit.ts] ////
2+
3+
//// [index.ts]
4+
import { test1, test2 } from "./other";
5+
6+
export function wrappedTest1<T, K extends string>(obj: T, k: K) {
7+
return test1(obj, k);
8+
}
9+
10+
export function wrappedTest2<T, K extends string>(obj: T, k: K) {
11+
return test2(obj, k);
12+
}
13+
14+
export type Obj = {
15+
a: number;
16+
readonly foo: string;
17+
};
18+
19+
export const processedInternally1 = wrappedTest1({} as Obj, "a");
20+
export const processedInternally2 = wrappedTest2({} as Obj, "a");
21+
//// [other.ts]
22+
// how Omit from lib is defined
23+
type OmitReal<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
24+
// what we see when we hover it
25+
type OmitUnveiled<T, K extends string | number | symbol> = {
26+
[P in Exclude<keyof T, K>]: T[P];
27+
};
28+
29+
export function test1<T, K extends string>(obj: T, k: K): OmitReal<T, K> {
30+
return {} as any;
31+
}
32+
33+
export function test2<T, K extends string>(obj: T, k: K): OmitUnveiled<T, K> {
34+
return {} as any;
35+
}
36+
37+
//// [other.js]
38+
"use strict";
39+
Object.defineProperty(exports, "__esModule", { value: true });
40+
exports.test2 = exports.test1 = void 0;
41+
function test1(obj, k) {
42+
return {};
43+
}
44+
exports.test1 = test1;
45+
function test2(obj, k) {
46+
return {};
47+
}
48+
exports.test2 = test2;
49+
//// [index.js]
50+
"use strict";
51+
Object.defineProperty(exports, "__esModule", { value: true });
52+
exports.processedInternally2 = exports.processedInternally1 = exports.wrappedTest2 = exports.wrappedTest1 = void 0;
53+
var other_1 = require("./other");
54+
function wrappedTest1(obj, k) {
55+
return (0, other_1.test1)(obj, k);
56+
}
57+
exports.wrappedTest1 = wrappedTest1;
58+
function wrappedTest2(obj, k) {
59+
return (0, other_1.test2)(obj, k);
60+
}
61+
exports.wrappedTest2 = wrappedTest2;
62+
exports.processedInternally1 = wrappedTest1({}, "a");
63+
exports.processedInternally2 = wrappedTest2({}, "a");
64+
65+
66+
//// [other.d.ts]
67+
type OmitReal<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
68+
type OmitUnveiled<T, K extends string | number | symbol> = {
69+
[P in Exclude<keyof T, K>]: T[P];
70+
};
71+
export declare function test1<T, K extends string>(obj: T, k: K): OmitReal<T, K>;
72+
export declare function test2<T, K extends string>(obj: T, k: K): OmitUnveiled<T, K>;
73+
export {};
74+
//// [index.d.ts]
75+
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;
76+
export declare function wrappedTest2<T, K extends string>(obj: T, k: K): { [P in Exclude<keyof T, K>]: T[P]; };
77+
export type Obj = {
78+
a: number;
79+
readonly foo: string;
80+
};
81+
export declare const processedInternally1: {
82+
readonly foo: string;
83+
};
84+
export declare const processedInternally2: {
85+
foo: string;
86+
};
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
//// [tests/cases/compiler/inlineMappedTypeModifierDeclarationEmit.ts] ////
2+
3+
=== index.ts ===
4+
import { test1, test2 } from "./other";
5+
>test1 : Symbol(test1, Decl(index.ts, 0, 8))
6+
>test2 : Symbol(test2, Decl(index.ts, 0, 15))
7+
8+
export function wrappedTest1<T, K extends string>(obj: T, k: K) {
9+
>wrappedTest1 : Symbol(wrappedTest1, Decl(index.ts, 0, 39))
10+
>T : Symbol(T, Decl(index.ts, 2, 29))
11+
>K : Symbol(K, Decl(index.ts, 2, 31))
12+
>obj : Symbol(obj, Decl(index.ts, 2, 50))
13+
>T : Symbol(T, Decl(index.ts, 2, 29))
14+
>k : Symbol(k, Decl(index.ts, 2, 57))
15+
>K : Symbol(K, Decl(index.ts, 2, 31))
16+
17+
return test1(obj, k);
18+
>test1 : Symbol(test1, Decl(index.ts, 0, 8))
19+
>obj : Symbol(obj, Decl(index.ts, 2, 50))
20+
>k : Symbol(k, Decl(index.ts, 2, 57))
21+
}
22+
23+
export function wrappedTest2<T, K extends string>(obj: T, k: K) {
24+
>wrappedTest2 : Symbol(wrappedTest2, Decl(index.ts, 4, 1))
25+
>T : Symbol(T, Decl(index.ts, 6, 29))
26+
>K : Symbol(K, Decl(index.ts, 6, 31))
27+
>obj : Symbol(obj, Decl(index.ts, 6, 50))
28+
>T : Symbol(T, Decl(index.ts, 6, 29))
29+
>k : Symbol(k, Decl(index.ts, 6, 57))
30+
>K : Symbol(K, Decl(index.ts, 6, 31))
31+
32+
return test2(obj, k);
33+
>test2 : Symbol(test2, Decl(index.ts, 0, 15))
34+
>obj : Symbol(obj, Decl(index.ts, 6, 50))
35+
>k : Symbol(k, Decl(index.ts, 6, 57))
36+
}
37+
38+
export type Obj = {
39+
>Obj : Symbol(Obj, Decl(index.ts, 8, 1))
40+
41+
a: number;
42+
>a : Symbol(a, Decl(index.ts, 10, 19))
43+
44+
readonly foo: string;
45+
>foo : Symbol(foo, Decl(index.ts, 11, 12))
46+
47+
};
48+
49+
export const processedInternally1 = wrappedTest1({} as Obj, "a");
50+
>processedInternally1 : Symbol(processedInternally1, Decl(index.ts, 15, 12))
51+
>wrappedTest1 : Symbol(wrappedTest1, Decl(index.ts, 0, 39))
52+
>Obj : Symbol(Obj, Decl(index.ts, 8, 1))
53+
54+
export const processedInternally2 = wrappedTest2({} as Obj, "a");
55+
>processedInternally2 : Symbol(processedInternally2, Decl(index.ts, 16, 12))
56+
>wrappedTest2 : Symbol(wrappedTest2, Decl(index.ts, 4, 1))
57+
>Obj : Symbol(Obj, Decl(index.ts, 8, 1))
58+
59+
=== other.ts ===
60+
// how Omit from lib is defined
61+
type OmitReal<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
62+
>OmitReal : Symbol(OmitReal, Decl(other.ts, 0, 0))
63+
>T : Symbol(T, Decl(other.ts, 1, 14))
64+
>K : Symbol(K, Decl(other.ts, 1, 16))
65+
>Pick : Symbol(Pick, Decl(lib.es5.d.ts, --, --))
66+
>T : Symbol(T, Decl(other.ts, 1, 14))
67+
>Exclude : Symbol(Exclude, Decl(lib.es5.d.ts, --, --))
68+
>T : Symbol(T, Decl(other.ts, 1, 14))
69+
>K : Symbol(K, Decl(other.ts, 1, 16))
70+
71+
// what we see when we hover it
72+
type OmitUnveiled<T, K extends string | number | symbol> = {
73+
>OmitUnveiled : Symbol(OmitUnveiled, Decl(other.ts, 1, 69))
74+
>T : Symbol(T, Decl(other.ts, 3, 18))
75+
>K : Symbol(K, Decl(other.ts, 3, 20))
76+
77+
[P in Exclude<keyof T, K>]: T[P];
78+
>P : Symbol(P, Decl(other.ts, 4, 3))
79+
>Exclude : Symbol(Exclude, Decl(lib.es5.d.ts, --, --))
80+
>T : Symbol(T, Decl(other.ts, 3, 18))
81+
>K : Symbol(K, Decl(other.ts, 3, 20))
82+
>T : Symbol(T, Decl(other.ts, 3, 18))
83+
>P : Symbol(P, Decl(other.ts, 4, 3))
84+
85+
};
86+
87+
export function test1<T, K extends string>(obj: T, k: K): OmitReal<T, K> {
88+
>test1 : Symbol(test1, Decl(other.ts, 5, 2))
89+
>T : Symbol(T, Decl(other.ts, 7, 22))
90+
>K : Symbol(K, Decl(other.ts, 7, 24))
91+
>obj : Symbol(obj, Decl(other.ts, 7, 43))
92+
>T : Symbol(T, Decl(other.ts, 7, 22))
93+
>k : Symbol(k, Decl(other.ts, 7, 50))
94+
>K : Symbol(K, Decl(other.ts, 7, 24))
95+
>OmitReal : Symbol(OmitReal, Decl(other.ts, 0, 0))
96+
>T : Symbol(T, Decl(other.ts, 7, 22))
97+
>K : Symbol(K, Decl(other.ts, 7, 24))
98+
99+
return {} as any;
100+
}
101+
102+
export function test2<T, K extends string>(obj: T, k: K): OmitUnveiled<T, K> {
103+
>test2 : Symbol(test2, Decl(other.ts, 9, 1))
104+
>T : Symbol(T, Decl(other.ts, 11, 22))
105+
>K : Symbol(K, Decl(other.ts, 11, 24))
106+
>obj : Symbol(obj, Decl(other.ts, 11, 43))
107+
>T : Symbol(T, Decl(other.ts, 11, 22))
108+
>k : Symbol(k, Decl(other.ts, 11, 50))
109+
>K : Symbol(K, Decl(other.ts, 11, 24))
110+
>OmitUnveiled : Symbol(OmitUnveiled, Decl(other.ts, 1, 69))
111+
>T : Symbol(T, Decl(other.ts, 11, 22))
112+
>K : Symbol(K, Decl(other.ts, 11, 24))
113+
114+
return {} as any;
115+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
//// [tests/cases/compiler/inlineMappedTypeModifierDeclarationEmit.ts] ////
2+
3+
=== index.ts ===
4+
import { test1, test2 } from "./other";
5+
>test1 : <T, K extends string>(obj: T, k: K) => { [P in Exclude<keyof T, K>]: T[P]; }
6+
>test2 : <T, K extends string>(obj: T, k: K) => { [P in Exclude<keyof T, K>]: T[P]; }
7+
8+
export function wrappedTest1<T, K extends string>(obj: T, k: K) {
9+
>wrappedTest1 : <T, K extends string>(obj: T, k: K) => { [P in Exclude<keyof T, K>]: T[P]; }
10+
>obj : T
11+
>k : K
12+
13+
return test1(obj, k);
14+
>test1(obj, k) : { [P in Exclude<keyof T, K>]: T[P]; }
15+
>test1 : <T, K extends string>(obj: T, k: K) => { [P in Exclude<keyof T, K>]: T[P]; }
16+
>obj : T
17+
>k : K
18+
}
19+
20+
export function wrappedTest2<T, K extends string>(obj: T, k: K) {
21+
>wrappedTest2 : <T, K extends string>(obj: T, k: K) => { [P in Exclude<keyof T, K>]: T[P]; }
22+
>obj : T
23+
>k : K
24+
25+
return test2(obj, k);
26+
>test2(obj, k) : { [P in Exclude<keyof T, K>]: T[P]; }
27+
>test2 : <T, K extends string>(obj: T, k: K) => { [P in Exclude<keyof T, K>]: T[P]; }
28+
>obj : T
29+
>k : K
30+
}
31+
32+
export type Obj = {
33+
>Obj : { a: number; readonly foo: string; }
34+
35+
a: number;
36+
>a : number
37+
38+
readonly foo: string;
39+
>foo : string
40+
41+
};
42+
43+
export const processedInternally1 = wrappedTest1({} as Obj, "a");
44+
>processedInternally1 : { readonly foo: string; }
45+
>wrappedTest1({} as Obj, "a") : { readonly foo: string; }
46+
>wrappedTest1 : <T, K extends string>(obj: T, k: K) => { [P in Exclude<keyof T, K>]: T[P]; }
47+
>{} as Obj : Obj
48+
>{} : {}
49+
>"a" : "a"
50+
51+
export const processedInternally2 = wrappedTest2({} as Obj, "a");
52+
>processedInternally2 : { foo: string; }
53+
>wrappedTest2({} as Obj, "a") : { foo: string; }
54+
>wrappedTest2 : <T, K extends string>(obj: T, k: K) => { [P in Exclude<keyof T, K>]: T[P]; }
55+
>{} as Obj : Obj
56+
>{} : {}
57+
>"a" : "a"
58+
59+
=== other.ts ===
60+
// how Omit from lib is defined
61+
type OmitReal<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
62+
>OmitReal : OmitReal<T, K>
63+
64+
// what we see when we hover it
65+
type OmitUnveiled<T, K extends string | number | symbol> = {
66+
>OmitUnveiled : OmitUnveiled<T, K>
67+
68+
[P in Exclude<keyof T, K>]: T[P];
69+
};
70+
71+
export function test1<T, K extends string>(obj: T, k: K): OmitReal<T, K> {
72+
>test1 : <T, K extends string>(obj: T, k: K) => OmitReal<T, K>
73+
>obj : T
74+
>k : K
75+
76+
return {} as any;
77+
>{} as any : any
78+
>{} : {}
79+
}
80+
81+
export function test2<T, K extends string>(obj: T, k: K): OmitUnveiled<T, K> {
82+
>test2 : <T, K extends string>(obj: T, k: K) => OmitUnveiled<T, K>
83+
>obj : T
84+
>k : K
85+
86+
return {} as any;
87+
>{} as any : any
88+
>{} : {}
89+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// @declaration: true
2+
// @filename: index.ts
3+
import { test1, test2 } from "./other";
4+
5+
export function wrappedTest1<T, K extends string>(obj: T, k: K) {
6+
return test1(obj, k);
7+
}
8+
9+
export function wrappedTest2<T, K extends string>(obj: T, k: K) {
10+
return test2(obj, k);
11+
}
12+
13+
export type Obj = {
14+
a: number;
15+
readonly foo: string;
16+
};
17+
18+
export const processedInternally1 = wrappedTest1({} as Obj, "a");
19+
export const processedInternally2 = wrappedTest2({} as Obj, "a");
20+
// @filename: other.ts
21+
// how Omit from lib is defined
22+
type OmitReal<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
23+
// what we see when we hover it
24+
type OmitUnveiled<T, K extends string | number | symbol> = {
25+
[P in Exclude<keyof T, K>]: T[P];
26+
};
27+
28+
export function test1<T, K extends string>(obj: T, k: K): OmitReal<T, K> {
29+
return {} as any;
30+
}
31+
32+
export function test2<T, K extends string>(obj: T, k: K): OmitUnveiled<T, K> {
33+
return {} as any;
34+
}

0 commit comments

Comments
 (0)