Skip to content

Declaration emit can change type semantics when inlining internal types #46655

@yortus

Description

@yortus

Bug Report

This is a cut-down repro for an issue I encountered while fixing a bug in rtti. What I saw was the same types behaving differently when consumed inside this package (ie referencing the types directly in src/) vs in downstream deps (ie referencing the emitted .d.ts types in dist/).

🔎 Search Terms

mapped type over union
mapped type over generic

🕗 Version & Regression Information

v4.4.4
v4.5 beta
v4.6 nightly (2 Nov 2021)

This is the behavior in every version I tried, and I reviewed the FAQ for entries about mapped types / homomorphic mapped types

⏯ Playground Link

Can't demonstrate in a single playground file.

💻 Code

/// file: test.ts
import {mappedUnionWithExportedType, mappedUnionWithPrivateType} from './api';
const a = mappedUnionWithExportedType({foo: 42}, {bar: 'bar'}); // type is {foo: number} | {bar: number}
'foo' in a ? a.foo.toFixed : a.bar.padStart;                    // OK
const b = mappedUnionWithPrivateType({foo: 42}, {bar: 'bar'});  // type is {}
'foo' in b ? b.foo.toFixed : b.bar.padStart;
//             ^^^             ^^^                              // ERRORS: 'foo' and 'bar' don't exist on {}

/// file: api.ts
import {useExportedType, usePrivateType} from './internal';
export const mappedUnionWithExportedType = <T extends unknown[]>(...args: T) => useExportedType(...args);
export const mappedUnionWithPrivateType = <T extends unknown[]>(...args: T) => usePrivateType(...args);


/// file: internal.ts
export declare function useExportedType<T extends unknown[]>(...args: T): ExportedMapped<T[any]>;
export declare function usePrivateType<T extends unknown[]>(...args: T): PrivateMapped<T[any]>;

export type ExportedMapped<Obj> = {[K in keyof Obj]: Obj[K]};
type PrivateMapped<Obj> = {[K in keyof Obj]: Obj[K]};

🙁 Actual behavior

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 mappedUnionWithExportedType: <T extends unknown[]>(...args: T) => import("./internal").ExportedMapped<T[any]>;
export declare const mappedUnionWithPrivateType: <T extends unknown[]>(...args: T) => { [K in keyof T[any]]: T[any][K]; };

🙂 Expected behavior

Mapping over a union type seems silly to begin with (indeed that was the rtti bug I was fixing), but regardless of that, I would expect declaration emit semantics to be preserved whether or not tsc preserves an internal type in the emit, or expands it directly into a type expression.

Metadata

Metadata

Assignees

Labels

BugA bug in TypeScriptFix AvailableA PR has been opened for this issue

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions