Skip to content

Commit 4d2fb54

Browse files
authored
Preserve the distributivity of inlined conditional types in declaration emit (#48592)
1 parent 94d33ba commit 4d2fb54

5 files changed

+252
-1
lines changed

src/compiler/checker.ts

+37-1
Original file line numberDiff line numberDiff line change
@@ -5102,13 +5102,49 @@ namespace ts {
51025102

51035103
function conditionalTypeToTypeNode(type: ConditionalType) {
51045104
const checkTypeNode = typeToTypeNodeHelper(type.checkType, context);
5105+
context.approximateLength += 15;
5106+
if (context.flags & NodeBuilderFlags.GenerateNamesForShadowedTypeParams && type.root.isDistributive && !(type.checkType.flags & TypeFlags.TypeParameter)) {
5107+
const newParam = createTypeParameter(createSymbol(SymbolFlags.TypeParameter, "T" as __String));
5108+
const name = typeParameterToName(newParam, context);
5109+
const newTypeVariable = factory.createTypeReferenceNode(name);
5110+
context.approximateLength += 37; // 15 each for two added conditionals, 7 for an added infer type
5111+
const newMapper = prependTypeMapping(type.root.checkType, newParam, type.combinedMapper || type.mapper);
5112+
const saveInferTypeParameters = context.inferTypeParameters;
5113+
context.inferTypeParameters = type.root.inferTypeParameters;
5114+
const extendsTypeNode = typeToTypeNodeHelper(instantiateType(type.root.extendsType, newMapper), context);
5115+
context.inferTypeParameters = saveInferTypeParameters;
5116+
const trueTypeNode = typeToTypeNodeOrCircularityElision(instantiateType(getTypeFromTypeNode(type.root.node.trueType), newMapper));
5117+
const falseTypeNode = typeToTypeNodeOrCircularityElision(instantiateType(getTypeFromTypeNode(type.root.node.falseType), newMapper));
5118+
5119+
5120+
// outermost conditional makes `T` a type parameter, allowing the inner conditionals to be distributive
5121+
// second conditional makes `T` have `T & checkType` substitution, so it is correctly usable as the checkType
5122+
// inner conditional runs the check the user provided on the check type (distributively) and returns the result
5123+
// checkType extends infer T ? T extends checkType ? T extends extendsType<T> ? trueType<T> : falseType<T> : never : never;
5124+
// this is potentially simplifiable to
5125+
// checkType extends infer T ? T extends checkType & extendsType<T> ? trueType<T> : falseType<T> : never;
5126+
// but that may confuse users who read the output more.
5127+
// On the other hand,
5128+
// checkType extends infer T extends checkType ? T extends extendsType<T> ? trueType<T> : falseType<T> : never;
5129+
// may also work with `infer ... extends ...` in, but would produce declarations only compatible with the latest TS.
5130+
return factory.createConditionalTypeNode(
5131+
checkTypeNode,
5132+
factory.createInferTypeNode(factory.createTypeParameterDeclaration(/*modifiers*/ undefined, factory.cloneNode(newTypeVariable.typeName) as Identifier)),
5133+
factory.createConditionalTypeNode(
5134+
factory.createTypeReferenceNode(factory.cloneNode(name)),
5135+
typeToTypeNodeHelper(type.checkType, context),
5136+
factory.createConditionalTypeNode(newTypeVariable, extendsTypeNode, trueTypeNode, falseTypeNode),
5137+
factory.createKeywordTypeNode(SyntaxKind.NeverKeyword)
5138+
),
5139+
factory.createKeywordTypeNode(SyntaxKind.NeverKeyword)
5140+
);
5141+
}
51055142
const saveInferTypeParameters = context.inferTypeParameters;
51065143
context.inferTypeParameters = type.root.inferTypeParameters;
51075144
const extendsTypeNode = typeToTypeNodeHelper(type.extendsType, context);
51085145
context.inferTypeParameters = saveInferTypeParameters;
51095146
const trueTypeNode = typeToTypeNodeOrCircularityElision(getTrueTypeFromConditionalType(type));
51105147
const falseTypeNode = typeToTypeNodeOrCircularityElision(getFalseTypeFromConditionalType(type));
5111-
context.approximateLength += 15;
51125148
return factory.createConditionalTypeNode(checkTypeNode, extendsTypeNode, trueTypeNode, falseTypeNode);
51135149
}
51145150

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//// [tests/cases/compiler/declarationEmitInlinedDistributiveConditional.ts] ////
2+
3+
//// [test.ts]
4+
import {dropPrivateProps1, dropPrivateProps2} from './api';
5+
const a = dropPrivateProps1({foo: 42, _bar: 'secret'}); // type is {foo: number}
6+
//a._bar // error: _bar does not exist <===== as expected
7+
const b = dropPrivateProps2({foo: 42, _bar: 'secret'}); // type is {foo: number, _bar: string}
8+
//b._bar // no error, type of b._bar is string <===== NOT expected
9+
10+
//// [api.ts]
11+
import {excludePrivateKeys1, excludePrivateKeys2} from './internal';
12+
export const dropPrivateProps1 = <Obj>(obj: Obj) => excludePrivateKeys1(obj);
13+
export const dropPrivateProps2 = <Obj>(obj: Obj) => excludePrivateKeys2(obj);
14+
15+
//// [internal.ts]
16+
export declare function excludePrivateKeys1<Obj>(obj: Obj): {[K in PublicKeys1<keyof Obj>]: Obj[K]};
17+
export declare function excludePrivateKeys2<Obj>(obj: Obj): {[K in PublicKeys2<keyof Obj>]: Obj[K]};
18+
export type PublicKeys1<T> = T extends `_${string}` ? never : T;
19+
type PublicKeys2<T> = T extends `_${string}` ? never : T;
20+
21+
//// [internal.js]
22+
"use strict";
23+
exports.__esModule = true;
24+
//// [api.js]
25+
"use strict";
26+
exports.__esModule = true;
27+
exports.dropPrivateProps2 = exports.dropPrivateProps1 = void 0;
28+
var internal_1 = require("./internal");
29+
var dropPrivateProps1 = function (obj) { return (0, internal_1.excludePrivateKeys1)(obj); };
30+
exports.dropPrivateProps1 = dropPrivateProps1;
31+
var dropPrivateProps2 = function (obj) { return (0, internal_1.excludePrivateKeys2)(obj); };
32+
exports.dropPrivateProps2 = dropPrivateProps2;
33+
//// [test.js]
34+
"use strict";
35+
exports.__esModule = true;
36+
var api_1 = require("./api");
37+
var a = (0, api_1.dropPrivateProps1)({ foo: 42, _bar: 'secret' }); // type is {foo: number}
38+
//a._bar // error: _bar does not exist <===== as expected
39+
var b = (0, api_1.dropPrivateProps2)({ foo: 42, _bar: 'secret' }); // type is {foo: number, _bar: string}
40+
//b._bar // no error, type of b._bar is string <===== NOT expected
41+
42+
43+
//// [internal.d.ts]
44+
export declare function excludePrivateKeys1<Obj>(obj: Obj): {
45+
[K in PublicKeys1<keyof Obj>]: Obj[K];
46+
};
47+
export declare function excludePrivateKeys2<Obj>(obj: Obj): {
48+
[K in PublicKeys2<keyof Obj>]: Obj[K];
49+
};
50+
export declare type PublicKeys1<T> = T extends `_${string}` ? never : T;
51+
declare type PublicKeys2<T> = T extends `_${string}` ? never : T;
52+
export {};
53+
//// [api.d.ts]
54+
export declare const dropPrivateProps1: <Obj>(obj: Obj) => { [K in import("./internal").PublicKeys1<keyof Obj>]: Obj[K]; };
55+
export declare const dropPrivateProps2: <Obj>(obj: Obj) => { [K in keyof Obj extends infer T ? T extends keyof Obj ? T extends `_${string}` ? never : T : never : never]: Obj[K]; };
56+
//// [test.d.ts]
57+
export {};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
=== tests/cases/compiler/test.ts ===
2+
import {dropPrivateProps1, dropPrivateProps2} from './api';
3+
>dropPrivateProps1 : Symbol(dropPrivateProps1, Decl(test.ts, 0, 8))
4+
>dropPrivateProps2 : Symbol(dropPrivateProps2, Decl(test.ts, 0, 26))
5+
6+
const a = dropPrivateProps1({foo: 42, _bar: 'secret'}); // type is {foo: number}
7+
>a : Symbol(a, Decl(test.ts, 1, 5))
8+
>dropPrivateProps1 : Symbol(dropPrivateProps1, Decl(test.ts, 0, 8))
9+
>foo : Symbol(foo, Decl(test.ts, 1, 29))
10+
>_bar : Symbol(_bar, Decl(test.ts, 1, 37))
11+
12+
//a._bar // error: _bar does not exist <===== as expected
13+
const b = dropPrivateProps2({foo: 42, _bar: 'secret'}); // type is {foo: number, _bar: string}
14+
>b : Symbol(b, Decl(test.ts, 3, 5))
15+
>dropPrivateProps2 : Symbol(dropPrivateProps2, Decl(test.ts, 0, 26))
16+
>foo : Symbol(foo, Decl(test.ts, 3, 29))
17+
>_bar : Symbol(_bar, Decl(test.ts, 3, 37))
18+
19+
//b._bar // no error, type of b._bar is string <===== NOT expected
20+
21+
=== tests/cases/compiler/api.ts ===
22+
import {excludePrivateKeys1, excludePrivateKeys2} from './internal';
23+
>excludePrivateKeys1 : Symbol(excludePrivateKeys1, Decl(api.ts, 0, 8))
24+
>excludePrivateKeys2 : Symbol(excludePrivateKeys2, Decl(api.ts, 0, 28))
25+
26+
export const dropPrivateProps1 = <Obj>(obj: Obj) => excludePrivateKeys1(obj);
27+
>dropPrivateProps1 : Symbol(dropPrivateProps1, Decl(api.ts, 1, 12))
28+
>Obj : Symbol(Obj, Decl(api.ts, 1, 34))
29+
>obj : Symbol(obj, Decl(api.ts, 1, 39))
30+
>Obj : Symbol(Obj, Decl(api.ts, 1, 34))
31+
>excludePrivateKeys1 : Symbol(excludePrivateKeys1, Decl(api.ts, 0, 8))
32+
>obj : Symbol(obj, Decl(api.ts, 1, 39))
33+
34+
export const dropPrivateProps2 = <Obj>(obj: Obj) => excludePrivateKeys2(obj);
35+
>dropPrivateProps2 : Symbol(dropPrivateProps2, Decl(api.ts, 2, 12))
36+
>Obj : Symbol(Obj, Decl(api.ts, 2, 34))
37+
>obj : Symbol(obj, Decl(api.ts, 2, 39))
38+
>Obj : Symbol(Obj, Decl(api.ts, 2, 34))
39+
>excludePrivateKeys2 : Symbol(excludePrivateKeys2, Decl(api.ts, 0, 28))
40+
>obj : Symbol(obj, Decl(api.ts, 2, 39))
41+
42+
=== tests/cases/compiler/internal.ts ===
43+
export declare function excludePrivateKeys1<Obj>(obj: Obj): {[K in PublicKeys1<keyof Obj>]: Obj[K]};
44+
>excludePrivateKeys1 : Symbol(excludePrivateKeys1, Decl(internal.ts, 0, 0))
45+
>Obj : Symbol(Obj, Decl(internal.ts, 0, 44))
46+
>obj : Symbol(obj, Decl(internal.ts, 0, 49))
47+
>Obj : Symbol(Obj, Decl(internal.ts, 0, 44))
48+
>K : Symbol(K, Decl(internal.ts, 0, 62))
49+
>PublicKeys1 : Symbol(PublicKeys1, Decl(internal.ts, 1, 100))
50+
>Obj : Symbol(Obj, Decl(internal.ts, 0, 44))
51+
>Obj : Symbol(Obj, Decl(internal.ts, 0, 44))
52+
>K : Symbol(K, Decl(internal.ts, 0, 62))
53+
54+
export declare function excludePrivateKeys2<Obj>(obj: Obj): {[K in PublicKeys2<keyof Obj>]: Obj[K]};
55+
>excludePrivateKeys2 : Symbol(excludePrivateKeys2, Decl(internal.ts, 0, 100))
56+
>Obj : Symbol(Obj, Decl(internal.ts, 1, 44))
57+
>obj : Symbol(obj, Decl(internal.ts, 1, 49))
58+
>Obj : Symbol(Obj, Decl(internal.ts, 1, 44))
59+
>K : Symbol(K, Decl(internal.ts, 1, 62))
60+
>PublicKeys2 : Symbol(PublicKeys2, Decl(internal.ts, 2, 64))
61+
>Obj : Symbol(Obj, Decl(internal.ts, 1, 44))
62+
>Obj : Symbol(Obj, Decl(internal.ts, 1, 44))
63+
>K : Symbol(K, Decl(internal.ts, 1, 62))
64+
65+
export type PublicKeys1<T> = T extends `_${string}` ? never : T;
66+
>PublicKeys1 : Symbol(PublicKeys1, Decl(internal.ts, 1, 100))
67+
>T : Symbol(T, Decl(internal.ts, 2, 24))
68+
>T : Symbol(T, Decl(internal.ts, 2, 24))
69+
>T : Symbol(T, Decl(internal.ts, 2, 24))
70+
71+
type PublicKeys2<T> = T extends `_${string}` ? never : T;
72+
>PublicKeys2 : Symbol(PublicKeys2, Decl(internal.ts, 2, 64))
73+
>T : Symbol(T, Decl(internal.ts, 3, 17))
74+
>T : Symbol(T, Decl(internal.ts, 3, 17))
75+
>T : Symbol(T, Decl(internal.ts, 3, 17))
76+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
=== tests/cases/compiler/test.ts ===
2+
import {dropPrivateProps1, dropPrivateProps2} from './api';
3+
>dropPrivateProps1 : <Obj>(obj: Obj) => { [K in import("tests/cases/compiler/internal").PublicKeys1<keyof Obj>]: Obj[K]; }
4+
>dropPrivateProps2 : <Obj>(obj: Obj) => { [K in keyof Obj extends `_${string}` ? never : keyof Obj]: Obj[K]; }
5+
6+
const a = dropPrivateProps1({foo: 42, _bar: 'secret'}); // type is {foo: number}
7+
>a : { foo: number; }
8+
>dropPrivateProps1({foo: 42, _bar: 'secret'}) : { foo: number; }
9+
>dropPrivateProps1 : <Obj>(obj: Obj) => { [K in import("tests/cases/compiler/internal").PublicKeys1<keyof Obj>]: Obj[K]; }
10+
>{foo: 42, _bar: 'secret'} : { foo: number; _bar: string; }
11+
>foo : number
12+
>42 : 42
13+
>_bar : string
14+
>'secret' : "secret"
15+
16+
//a._bar // error: _bar does not exist <===== as expected
17+
const b = dropPrivateProps2({foo: 42, _bar: 'secret'}); // type is {foo: number, _bar: string}
18+
>b : { foo: number; }
19+
>dropPrivateProps2({foo: 42, _bar: 'secret'}) : { foo: number; }
20+
>dropPrivateProps2 : <Obj>(obj: Obj) => { [K in keyof Obj extends `_${string}` ? never : keyof Obj]: Obj[K]; }
21+
>{foo: 42, _bar: 'secret'} : { foo: number; _bar: string; }
22+
>foo : number
23+
>42 : 42
24+
>_bar : string
25+
>'secret' : "secret"
26+
27+
//b._bar // no error, type of b._bar is string <===== NOT expected
28+
29+
=== tests/cases/compiler/api.ts ===
30+
import {excludePrivateKeys1, excludePrivateKeys2} from './internal';
31+
>excludePrivateKeys1 : <Obj>(obj: Obj) => { [K in import("tests/cases/compiler/internal").PublicKeys1<keyof Obj>]: Obj[K]; }
32+
>excludePrivateKeys2 : <Obj>(obj: Obj) => { [K in keyof Obj extends `_${string}` ? never : keyof Obj]: Obj[K]; }
33+
34+
export const dropPrivateProps1 = <Obj>(obj: Obj) => excludePrivateKeys1(obj);
35+
>dropPrivateProps1 : <Obj>(obj: Obj) => { [K in import("tests/cases/compiler/internal").PublicKeys1<keyof Obj>]: Obj[K]; }
36+
><Obj>(obj: Obj) => excludePrivateKeys1(obj) : <Obj>(obj: Obj) => { [K in import("tests/cases/compiler/internal").PublicKeys1<keyof Obj>]: Obj[K]; }
37+
>obj : Obj
38+
>excludePrivateKeys1(obj) : { [K in import("tests/cases/compiler/internal").PublicKeys1<keyof Obj>]: Obj[K]; }
39+
>excludePrivateKeys1 : <Obj>(obj: Obj) => { [K in import("tests/cases/compiler/internal").PublicKeys1<keyof Obj>]: Obj[K]; }
40+
>obj : Obj
41+
42+
export const dropPrivateProps2 = <Obj>(obj: Obj) => excludePrivateKeys2(obj);
43+
>dropPrivateProps2 : <Obj>(obj: Obj) => { [K in keyof Obj extends `_${string}` ? never : keyof Obj]: Obj[K]; }
44+
><Obj>(obj: Obj) => excludePrivateKeys2(obj) : <Obj>(obj: Obj) => { [K in keyof Obj extends `_${string}` ? never : keyof Obj]: Obj[K]; }
45+
>obj : Obj
46+
>excludePrivateKeys2(obj) : { [K in keyof Obj extends `_${string}` ? never : keyof Obj]: Obj[K]; }
47+
>excludePrivateKeys2 : <Obj>(obj: Obj) => { [K in keyof Obj extends `_${string}` ? never : keyof Obj]: Obj[K]; }
48+
>obj : Obj
49+
50+
=== tests/cases/compiler/internal.ts ===
51+
export declare function excludePrivateKeys1<Obj>(obj: Obj): {[K in PublicKeys1<keyof Obj>]: Obj[K]};
52+
>excludePrivateKeys1 : <Obj>(obj: Obj) => { [K in PublicKeys1<keyof Obj>]: Obj[K]; }
53+
>obj : Obj
54+
55+
export declare function excludePrivateKeys2<Obj>(obj: Obj): {[K in PublicKeys2<keyof Obj>]: Obj[K]};
56+
>excludePrivateKeys2 : <Obj>(obj: Obj) => { [K in PublicKeys2<keyof Obj>]: Obj[K]; }
57+
>obj : Obj
58+
59+
export type PublicKeys1<T> = T extends `_${string}` ? never : T;
60+
>PublicKeys1 : PublicKeys1<T>
61+
62+
type PublicKeys2<T> = T extends `_${string}` ? never : T;
63+
>PublicKeys2 : PublicKeys2<T>
64+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// @declaration: true
2+
// @filename: test.ts
3+
import {dropPrivateProps1, dropPrivateProps2} from './api';
4+
const a = dropPrivateProps1({foo: 42, _bar: 'secret'}); // type is {foo: number}
5+
//a._bar // error: _bar does not exist <===== as expected
6+
const b = dropPrivateProps2({foo: 42, _bar: 'secret'}); // type is {foo: number, _bar: string}
7+
//b._bar // no error, type of b._bar is string <===== NOT expected
8+
9+
// @filename: api.ts
10+
import {excludePrivateKeys1, excludePrivateKeys2} from './internal';
11+
export const dropPrivateProps1 = <Obj>(obj: Obj) => excludePrivateKeys1(obj);
12+
export const dropPrivateProps2 = <Obj>(obj: Obj) => excludePrivateKeys2(obj);
13+
14+
// @filename: internal.ts
15+
export declare function excludePrivateKeys1<Obj>(obj: Obj): {[K in PublicKeys1<keyof Obj>]: Obj[K]};
16+
export declare function excludePrivateKeys2<Obj>(obj: Obj): {[K in PublicKeys2<keyof Obj>]: Obj[K]};
17+
export type PublicKeys1<T> = T extends `_${string}` ? never : T;
18+
type PublicKeys2<T> = T extends `_${string}` ? never : T;

0 commit comments

Comments
 (0)