Description
Bug Report
This issue was reported in a comment on #46655, but I'm making a separate issue for it since the other issue has a fix available that only covers the mapped type cases but not the conditional type cases.
🔎 Search Terms
conditional type over union
distributive conditional type
🕗 Version & Regression Information
v4.6.2
This is the behavior in every version I tried, and I reviewed the FAQ for entries about distributive conditional types.
⏯ Playground Link
Can't demonstrate in a single playground file.
💻 Code
/// file: 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
/// file: api.ts
import {excludePrivateKeys1, excludePrivateKeys2} from './internal';
export const dropPrivateProps1 = <Obj>(obj: Obj) => excludePrivateKeys1(obj);
export const dropPrivateProps2 = <Obj>(obj: Obj) => excludePrivateKeys2(obj);
/// file: internal.ts
export declare function excludePrivateKeys1<Obj>(obj: Obj): {[K in PublicKeys1<keyof Obj>]: Obj[K]};
export declare function excludePrivateKeys2<Obj>(obj: Obj): {[K in PublicKeys2<keyof Obj>]: Obj[K]};
export type PublicKeys1<T> = T extends `_${string}` ? never : T;
type PublicKeys2<T> = T extends `_${string}` ? never : T;
🙁 Actual behavior
To repro this, you need to generate api.d.ts
and internal.d.ts
declaration files, and consume those from test.ts
. The three-file setup is the only way I could get tsc to inline the non-exported type PublicKeys2
in the .d.ts file. If you import api.ts
directly from source, everything works as expected. Things only break when consuming from the api.d.ts
file. So this issue is saying that the semantics of the emitted .d.ts
files is not the same as the sources they were emitted from.
As shown in test.ts
above, declaration emit type semantics change depending on whether an internal type is marked with export
or not. In the above example, the declarations emitted for api.d.ts
are:
export declare const dropPrivateProps1: <Obj>(obj: Obj) => { [K in import("./internal").PublicKeys1<keyof Obj>]: Obj[K]; };
export declare const dropPrivateProps2: <Obj>(obj: Obj) => { [K in keyof Obj extends `_${string}` ? never : keyof Obj]: Obj[K]; };
Specifically, the conditional type in PublicKeys2
loses its distributivity the way it is inlined above, so the type's meaning changes.
🙂 Expected behavior
Declaration emit semantics should be the same as the source code, regardless of whether or not tsc
preserves an internal type in the emit, or expands it directly into a type expression.
See also
#46655 (similar issue but for mapped types)
Activity