Skip to content

Declaration emit may change type semantics when inlining conditional types #48140

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

Closed
yortus opened this issue Mar 6, 2022 · 0 comments · Fixed by #48592
Closed

Declaration emit may change type semantics when inlining conditional types #48140

yortus opened this issue Mar 6, 2022 · 0 comments · Fixed by #48592
Assignees
Labels
Fix Available A PR has been opened for this issue Needs Investigation This issue needs a team member to investigate its status.

Comments

@yortus
Copy link
Contributor

yortus commented Mar 6, 2022

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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Fix Available A PR has been opened for this issue Needs Investigation This issue needs a team member to investigate its status.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants