TypeScript: Declaration emit should not inline type definitions
TypeScript Version: 3.8.3, 3.8.1, probably others
Search Terms:
declaration inlining, dts inlining, declaration inline, inline literal, declaration literal
Code
// parent.ts
import { num, obj } from "./child"
export const reExportNum = num;
export const reExportObj = obj;
// child.d.ts
export declare const num: number;
export declare const obj: { a: 1 };
tsc index.ts --declaration
Expected behavior:
Declaration emit for parent.ts should not inline types.
// parent.d.ts
import { num, obj } from "./child"
export declare const reExportNum: typeof num;
export declare const reExportObj: typeof obj;
Actual behavior:
Today, declaration emit for parent.ts inlines the types and eliminates the import of the child.d.ts type definition.
// parent.d.ts
export declare const reExportNum: number;
export declare const reExportObj: {
a: 1;
};
This is a correctness issue, because consumers of parent.d.ts will not get the correct types if the types in child.d.ts change.
In practice, this is most likely to happen when parent and child are in separate packages, because they are published independently, i.e. an application uses parent-package which uses types from child-package. This is exacerbated by the current practice on npm of parent-package depending on an unpinned version, using package.json dependency syntax "child-package": "*".
This issue was co-authored with @robpalme
About this issue
- Original URL
- State: open
- Created 4 years ago
- Reactions: 7
- Comments: 18 (14 by maintainers)
I’m here because inlining everything caused my
.d.tsfile to be over 6.5MB long. The type checks started failing and I am wondering if the compiler simply ignores the end of the file because of some limitation.@aleksey-ilin the main solution I have found to solve huge declarations is to identify the root type that is inlined and then, assuming it is a statically known object type, create an
interfacefrom it and then refer to that interface at all usage sites.I have been experimenting with changing declaration emit so that shenanigans like this are not necessary. It kinda works and I’ll share that soon.
Separately, union and intersection types also get inlined.
interfacewill not save you in this case - there is no reliable userland workaround for these. Thankfully there is work in progress to reduce the inlining of these specific types in https://github.com/microsoft/TypeScript/pull/42149There’s a fairly fundamental tension here about what declaration emit means: Should declaration files represent the types as they existed when you compiled your program, or should they represent the types that a consuming library would have seen had your original program been compiled “in the context of” the consumer’s setup?
This gets really mind-bending if you think about conditional types or overloads, e.g.
Is the intent here that
cshould have the type that you saw when you invokedfunc? Or if a consuming library augmentsSomeTypein a way that changes the resulting type offunc(x), shouldchave some other new type? What if that causes some other use ofcto break a contract?I think one of these behaviors is much easier to reason about than the other, as you can probably tell from my descriptions of them.
Anyway the example in the OP is also in tension with people who want their end-result
.d.tsfile to be a single-ish artifact that doesn’t expose their entire program’s internal structure. This is a good goal anyway for performance - it’d be much better if we load 1 file per library instead of 12, and better if we handle some inline anonymous types instead of resolvingtypeofqueries everywhere.@cyberixae One mitigation to reduce inlining is to use
interfacerather thantypewhen defining object shapes. This causestscto reference the original type by name, which may further cause it to generate type-onlyimport()expressions.If you’re thinking of https://github.com/microsoft/TypeScript/pull/37444, that’s still out for review.