diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 4fb6b55412e02..3c401041f5673 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -5102,13 +5102,49 @@ namespace ts { function conditionalTypeToTypeNode(type: ConditionalType) { const checkTypeNode = typeToTypeNodeHelper(type.checkType, context); + context.approximateLength += 15; + if (context.flags & NodeBuilderFlags.GenerateNamesForShadowedTypeParams && type.root.isDistributive && !(type.checkType.flags & TypeFlags.TypeParameter)) { + const newParam = createTypeParameter(createSymbol(SymbolFlags.TypeParameter, "T" as __String)); + const name = typeParameterToName(newParam, context); + const newTypeVariable = factory.createTypeReferenceNode(name); + context.approximateLength += 37; // 15 each for two added conditionals, 7 for an added infer type + const newMapper = prependTypeMapping(type.root.checkType, newParam, type.combinedMapper || type.mapper); + const saveInferTypeParameters = context.inferTypeParameters; + context.inferTypeParameters = type.root.inferTypeParameters; + const extendsTypeNode = typeToTypeNodeHelper(instantiateType(type.root.extendsType, newMapper), context); + context.inferTypeParameters = saveInferTypeParameters; + const trueTypeNode = typeToTypeNodeOrCircularityElision(instantiateType(getTypeFromTypeNode(type.root.node.trueType), newMapper)); + const falseTypeNode = typeToTypeNodeOrCircularityElision(instantiateType(getTypeFromTypeNode(type.root.node.falseType), newMapper)); + + + // outermost conditional makes `T` a type parameter, allowing the inner conditionals to be distributive + // second conditional makes `T` have `T & checkType` substitution, so it is correctly usable as the checkType + // inner conditional runs the check the user provided on the check type (distributively) and returns the result + // checkType extends infer T ? T extends checkType ? T extends extendsType ? trueType : falseType : never : never; + // this is potentially simplifiable to + // checkType extends infer T ? T extends checkType & extendsType ? trueType : falseType : never; + // but that may confuse users who read the output more. + // On the other hand, + // checkType extends infer T extends checkType ? T extends extendsType ? trueType : falseType : never; + // may also work with `infer ... extends ...` in, but would produce declarations only compatible with the latest TS. + return factory.createConditionalTypeNode( + checkTypeNode, + factory.createInferTypeNode(factory.createTypeParameterDeclaration(/*modifiers*/ undefined, factory.cloneNode(newTypeVariable.typeName) as Identifier)), + factory.createConditionalTypeNode( + factory.createTypeReferenceNode(factory.cloneNode(name)), + typeToTypeNodeHelper(type.checkType, context), + factory.createConditionalTypeNode(newTypeVariable, extendsTypeNode, trueTypeNode, falseTypeNode), + factory.createKeywordTypeNode(SyntaxKind.NeverKeyword) + ), + factory.createKeywordTypeNode(SyntaxKind.NeverKeyword) + ); + } const saveInferTypeParameters = context.inferTypeParameters; context.inferTypeParameters = type.root.inferTypeParameters; const extendsTypeNode = typeToTypeNodeHelper(type.extendsType, context); context.inferTypeParameters = saveInferTypeParameters; const trueTypeNode = typeToTypeNodeOrCircularityElision(getTrueTypeFromConditionalType(type)); const falseTypeNode = typeToTypeNodeOrCircularityElision(getFalseTypeFromConditionalType(type)); - context.approximateLength += 15; return factory.createConditionalTypeNode(checkTypeNode, extendsTypeNode, trueTypeNode, falseTypeNode); } diff --git a/tests/baselines/reference/declarationEmitInlinedDistributiveConditional.js b/tests/baselines/reference/declarationEmitInlinedDistributiveConditional.js new file mode 100644 index 0000000000000..fafb13d1d5c79 --- /dev/null +++ b/tests/baselines/reference/declarationEmitInlinedDistributiveConditional.js @@ -0,0 +1,57 @@ +//// [tests/cases/compiler/declarationEmitInlinedDistributiveConditional.ts] //// + +//// [test.ts] +import {dropPrivateProps1, dropPrivateProps2} from './api'; +const a = dropPrivateProps1({foo: 42, _bar: 'secret'}); // type is {foo: number} +//a._bar // error: _bar does not exist <===== as expected +const b = dropPrivateProps2({foo: 42, _bar: 'secret'}); // type is {foo: number, _bar: string} +//b._bar // no error, type of b._bar is string <===== NOT expected + +//// [api.ts] +import {excludePrivateKeys1, excludePrivateKeys2} from './internal'; +export const dropPrivateProps1 = (obj: Obj) => excludePrivateKeys1(obj); +export const dropPrivateProps2 = (obj: Obj) => excludePrivateKeys2(obj); + +//// [internal.ts] +export declare function excludePrivateKeys1(obj: Obj): {[K in PublicKeys1]: Obj[K]}; +export declare function excludePrivateKeys2(obj: Obj): {[K in PublicKeys2]: Obj[K]}; +export type PublicKeys1 = T extends `_${string}` ? never : T; +type PublicKeys2 = T extends `_${string}` ? never : T; + +//// [internal.js] +"use strict"; +exports.__esModule = true; +//// [api.js] +"use strict"; +exports.__esModule = true; +exports.dropPrivateProps2 = exports.dropPrivateProps1 = void 0; +var internal_1 = require("./internal"); +var dropPrivateProps1 = function (obj) { return (0, internal_1.excludePrivateKeys1)(obj); }; +exports.dropPrivateProps1 = dropPrivateProps1; +var dropPrivateProps2 = function (obj) { return (0, internal_1.excludePrivateKeys2)(obj); }; +exports.dropPrivateProps2 = dropPrivateProps2; +//// [test.js] +"use strict"; +exports.__esModule = true; +var api_1 = require("./api"); +var a = (0, api_1.dropPrivateProps1)({ foo: 42, _bar: 'secret' }); // type is {foo: number} +//a._bar // error: _bar does not exist <===== as expected +var b = (0, api_1.dropPrivateProps2)({ foo: 42, _bar: 'secret' }); // type is {foo: number, _bar: string} +//b._bar // no error, type of b._bar is string <===== NOT expected + + +//// [internal.d.ts] +export declare function excludePrivateKeys1(obj: Obj): { + [K in PublicKeys1]: Obj[K]; +}; +export declare function excludePrivateKeys2(obj: Obj): { + [K in PublicKeys2]: Obj[K]; +}; +export declare type PublicKeys1 = T extends `_${string}` ? never : T; +declare type PublicKeys2 = T extends `_${string}` ? never : T; +export {}; +//// [api.d.ts] +export declare const dropPrivateProps1: (obj: Obj) => { [K in import("./internal").PublicKeys1]: Obj[K]; }; +export declare const dropPrivateProps2: (obj: Obj) => { [K in keyof Obj extends infer T ? T extends keyof Obj ? T extends `_${string}` ? never : T : never : never]: Obj[K]; }; +//// [test.d.ts] +export {}; diff --git a/tests/baselines/reference/declarationEmitInlinedDistributiveConditional.symbols b/tests/baselines/reference/declarationEmitInlinedDistributiveConditional.symbols new file mode 100644 index 0000000000000..8931694249369 --- /dev/null +++ b/tests/baselines/reference/declarationEmitInlinedDistributiveConditional.symbols @@ -0,0 +1,76 @@ +=== tests/cases/compiler/test.ts === +import {dropPrivateProps1, dropPrivateProps2} from './api'; +>dropPrivateProps1 : Symbol(dropPrivateProps1, Decl(test.ts, 0, 8)) +>dropPrivateProps2 : Symbol(dropPrivateProps2, Decl(test.ts, 0, 26)) + +const a = dropPrivateProps1({foo: 42, _bar: 'secret'}); // type is {foo: number} +>a : Symbol(a, Decl(test.ts, 1, 5)) +>dropPrivateProps1 : Symbol(dropPrivateProps1, Decl(test.ts, 0, 8)) +>foo : Symbol(foo, Decl(test.ts, 1, 29)) +>_bar : Symbol(_bar, Decl(test.ts, 1, 37)) + +//a._bar // error: _bar does not exist <===== as expected +const b = dropPrivateProps2({foo: 42, _bar: 'secret'}); // type is {foo: number, _bar: string} +>b : Symbol(b, Decl(test.ts, 3, 5)) +>dropPrivateProps2 : Symbol(dropPrivateProps2, Decl(test.ts, 0, 26)) +>foo : Symbol(foo, Decl(test.ts, 3, 29)) +>_bar : Symbol(_bar, Decl(test.ts, 3, 37)) + +//b._bar // no error, type of b._bar is string <===== NOT expected + +=== tests/cases/compiler/api.ts === +import {excludePrivateKeys1, excludePrivateKeys2} from './internal'; +>excludePrivateKeys1 : Symbol(excludePrivateKeys1, Decl(api.ts, 0, 8)) +>excludePrivateKeys2 : Symbol(excludePrivateKeys2, Decl(api.ts, 0, 28)) + +export const dropPrivateProps1 = (obj: Obj) => excludePrivateKeys1(obj); +>dropPrivateProps1 : Symbol(dropPrivateProps1, Decl(api.ts, 1, 12)) +>Obj : Symbol(Obj, Decl(api.ts, 1, 34)) +>obj : Symbol(obj, Decl(api.ts, 1, 39)) +>Obj : Symbol(Obj, Decl(api.ts, 1, 34)) +>excludePrivateKeys1 : Symbol(excludePrivateKeys1, Decl(api.ts, 0, 8)) +>obj : Symbol(obj, Decl(api.ts, 1, 39)) + +export const dropPrivateProps2 = (obj: Obj) => excludePrivateKeys2(obj); +>dropPrivateProps2 : Symbol(dropPrivateProps2, Decl(api.ts, 2, 12)) +>Obj : Symbol(Obj, Decl(api.ts, 2, 34)) +>obj : Symbol(obj, Decl(api.ts, 2, 39)) +>Obj : Symbol(Obj, Decl(api.ts, 2, 34)) +>excludePrivateKeys2 : Symbol(excludePrivateKeys2, Decl(api.ts, 0, 28)) +>obj : Symbol(obj, Decl(api.ts, 2, 39)) + +=== tests/cases/compiler/internal.ts === +export declare function excludePrivateKeys1(obj: Obj): {[K in PublicKeys1]: Obj[K]}; +>excludePrivateKeys1 : Symbol(excludePrivateKeys1, Decl(internal.ts, 0, 0)) +>Obj : Symbol(Obj, Decl(internal.ts, 0, 44)) +>obj : Symbol(obj, Decl(internal.ts, 0, 49)) +>Obj : Symbol(Obj, Decl(internal.ts, 0, 44)) +>K : Symbol(K, Decl(internal.ts, 0, 62)) +>PublicKeys1 : Symbol(PublicKeys1, Decl(internal.ts, 1, 100)) +>Obj : Symbol(Obj, Decl(internal.ts, 0, 44)) +>Obj : Symbol(Obj, Decl(internal.ts, 0, 44)) +>K : Symbol(K, Decl(internal.ts, 0, 62)) + +export declare function excludePrivateKeys2(obj: Obj): {[K in PublicKeys2]: Obj[K]}; +>excludePrivateKeys2 : Symbol(excludePrivateKeys2, Decl(internal.ts, 0, 100)) +>Obj : Symbol(Obj, Decl(internal.ts, 1, 44)) +>obj : Symbol(obj, Decl(internal.ts, 1, 49)) +>Obj : Symbol(Obj, Decl(internal.ts, 1, 44)) +>K : Symbol(K, Decl(internal.ts, 1, 62)) +>PublicKeys2 : Symbol(PublicKeys2, Decl(internal.ts, 2, 64)) +>Obj : Symbol(Obj, Decl(internal.ts, 1, 44)) +>Obj : Symbol(Obj, Decl(internal.ts, 1, 44)) +>K : Symbol(K, Decl(internal.ts, 1, 62)) + +export type PublicKeys1 = T extends `_${string}` ? never : T; +>PublicKeys1 : Symbol(PublicKeys1, Decl(internal.ts, 1, 100)) +>T : Symbol(T, Decl(internal.ts, 2, 24)) +>T : Symbol(T, Decl(internal.ts, 2, 24)) +>T : Symbol(T, Decl(internal.ts, 2, 24)) + +type PublicKeys2 = T extends `_${string}` ? never : T; +>PublicKeys2 : Symbol(PublicKeys2, Decl(internal.ts, 2, 64)) +>T : Symbol(T, Decl(internal.ts, 3, 17)) +>T : Symbol(T, Decl(internal.ts, 3, 17)) +>T : Symbol(T, Decl(internal.ts, 3, 17)) + diff --git a/tests/baselines/reference/declarationEmitInlinedDistributiveConditional.types b/tests/baselines/reference/declarationEmitInlinedDistributiveConditional.types new file mode 100644 index 0000000000000..ba779f2eebcef --- /dev/null +++ b/tests/baselines/reference/declarationEmitInlinedDistributiveConditional.types @@ -0,0 +1,64 @@ +=== tests/cases/compiler/test.ts === +import {dropPrivateProps1, dropPrivateProps2} from './api'; +>dropPrivateProps1 : (obj: Obj) => { [K in import("tests/cases/compiler/internal").PublicKeys1]: Obj[K]; } +>dropPrivateProps2 : (obj: Obj) => { [K in keyof Obj extends `_${string}` ? never : keyof Obj]: Obj[K]; } + +const a = dropPrivateProps1({foo: 42, _bar: 'secret'}); // type is {foo: number} +>a : { foo: number; } +>dropPrivateProps1({foo: 42, _bar: 'secret'}) : { foo: number; } +>dropPrivateProps1 : (obj: Obj) => { [K in import("tests/cases/compiler/internal").PublicKeys1]: Obj[K]; } +>{foo: 42, _bar: 'secret'} : { foo: number; _bar: string; } +>foo : number +>42 : 42 +>_bar : string +>'secret' : "secret" + +//a._bar // error: _bar does not exist <===== as expected +const b = dropPrivateProps2({foo: 42, _bar: 'secret'}); // type is {foo: number, _bar: string} +>b : { foo: number; } +>dropPrivateProps2({foo: 42, _bar: 'secret'}) : { foo: number; } +>dropPrivateProps2 : (obj: Obj) => { [K in keyof Obj extends `_${string}` ? never : keyof Obj]: Obj[K]; } +>{foo: 42, _bar: 'secret'} : { foo: number; _bar: string; } +>foo : number +>42 : 42 +>_bar : string +>'secret' : "secret" + +//b._bar // no error, type of b._bar is string <===== NOT expected + +=== tests/cases/compiler/api.ts === +import {excludePrivateKeys1, excludePrivateKeys2} from './internal'; +>excludePrivateKeys1 : (obj: Obj) => { [K in import("tests/cases/compiler/internal").PublicKeys1]: Obj[K]; } +>excludePrivateKeys2 : (obj: Obj) => { [K in keyof Obj extends `_${string}` ? never : keyof Obj]: Obj[K]; } + +export const dropPrivateProps1 = (obj: Obj) => excludePrivateKeys1(obj); +>dropPrivateProps1 : (obj: Obj) => { [K in import("tests/cases/compiler/internal").PublicKeys1]: Obj[K]; } +>(obj: Obj) => excludePrivateKeys1(obj) : (obj: Obj) => { [K in import("tests/cases/compiler/internal").PublicKeys1]: Obj[K]; } +>obj : Obj +>excludePrivateKeys1(obj) : { [K in import("tests/cases/compiler/internal").PublicKeys1]: Obj[K]; } +>excludePrivateKeys1 : (obj: Obj) => { [K in import("tests/cases/compiler/internal").PublicKeys1]: Obj[K]; } +>obj : Obj + +export const dropPrivateProps2 = (obj: Obj) => excludePrivateKeys2(obj); +>dropPrivateProps2 : (obj: Obj) => { [K in keyof Obj extends `_${string}` ? never : keyof Obj]: Obj[K]; } +>(obj: Obj) => excludePrivateKeys2(obj) : (obj: Obj) => { [K in keyof Obj extends `_${string}` ? never : keyof Obj]: Obj[K]; } +>obj : Obj +>excludePrivateKeys2(obj) : { [K in keyof Obj extends `_${string}` ? never : keyof Obj]: Obj[K]; } +>excludePrivateKeys2 : (obj: Obj) => { [K in keyof Obj extends `_${string}` ? never : keyof Obj]: Obj[K]; } +>obj : Obj + +=== tests/cases/compiler/internal.ts === +export declare function excludePrivateKeys1(obj: Obj): {[K in PublicKeys1]: Obj[K]}; +>excludePrivateKeys1 : (obj: Obj) => { [K in PublicKeys1]: Obj[K]; } +>obj : Obj + +export declare function excludePrivateKeys2(obj: Obj): {[K in PublicKeys2]: Obj[K]}; +>excludePrivateKeys2 : (obj: Obj) => { [K in PublicKeys2]: Obj[K]; } +>obj : Obj + +export type PublicKeys1 = T extends `_${string}` ? never : T; +>PublicKeys1 : PublicKeys1 + +type PublicKeys2 = T extends `_${string}` ? never : T; +>PublicKeys2 : PublicKeys2 + diff --git a/tests/cases/compiler/declarationEmitInlinedDistributiveConditional.ts b/tests/cases/compiler/declarationEmitInlinedDistributiveConditional.ts new file mode 100644 index 0000000000000..3a9f762b3afd9 --- /dev/null +++ b/tests/cases/compiler/declarationEmitInlinedDistributiveConditional.ts @@ -0,0 +1,18 @@ +// @declaration: true +// @filename: test.ts +import {dropPrivateProps1, dropPrivateProps2} from './api'; +const a = dropPrivateProps1({foo: 42, _bar: 'secret'}); // type is {foo: number} +//a._bar // error: _bar does not exist <===== as expected +const b = dropPrivateProps2({foo: 42, _bar: 'secret'}); // type is {foo: number, _bar: string} +//b._bar // no error, type of b._bar is string <===== NOT expected + +// @filename: api.ts +import {excludePrivateKeys1, excludePrivateKeys2} from './internal'; +export const dropPrivateProps1 = (obj: Obj) => excludePrivateKeys1(obj); +export const dropPrivateProps2 = (obj: Obj) => excludePrivateKeys2(obj); + +// @filename: internal.ts +export declare function excludePrivateKeys1(obj: Obj): {[K in PublicKeys1]: Obj[K]}; +export declare function excludePrivateKeys2(obj: Obj): {[K in PublicKeys2]: Obj[K]}; +export type PublicKeys1 = T extends `_${string}` ? never : T; +type PublicKeys2 = T extends `_${string}` ? never : T; \ No newline at end of file