Skip to content

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

Closed
@yortus

Description

@yortus
Contributor

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)

Metadata

Metadata

Assignees

Labels

Fix AvailableA PR has been opened for this issueNeeds InvestigationThis issue needs a team member to investigate its status.

Type

No type

Projects

No projects

Relationships

None yet

    Participants

    @yortus@weswigham@RyanCavanaugh@typescript-bot

    Issue actions

      Declaration emit may change type semantics when inlining conditional types · Issue #48140 · microsoft/TypeScript