Repository navigation
Declaration emit should not inline type definitions #37151
Description
Activity
DanielRosenwasser commented
on Mar 3, 2020 MemberMore actionsWhat would you have TypeScript emit in the case of the following?
export const exportNumNum = num + num; export const exportExtendedObj = { ...obj, someOtherProp: 200, };
Thanks for looking into this, Daniel Rosenwasser (@DanielRosenwasser) . As your examples illustrate, there is a long tail of correctness issues in this area.
If examples like the one in the issue description are the common case, maybe it would be possible to solve them independently of the uncommon cases.
We have some ideas for how to deal with the less common cases and can open a separate issue for them if you think it's a good idea. Perhaps related: #29043.
- addedNeeds InvestigationThis issue needs a team member to investigate its status.This issue needs a team member to investigate its status.
on Mar 4, 2020 RyanCavanaugh commented
on Mar 4, 2020 MemberMore actionsThere'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.
declare var x: SomeType; export const c = func(x);
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.Reacted by ExE Boss and BernardReacted by ExE Bossreply to #37151 (comment)
Ryan Cavanaugh (@RyanCavanaugh) thanks for your response, I find your description of the two mental models really helpful: the "types are conceptually inlined" model vs. the "types always flow through" model.
I'm not sure the inlining is easier to reason about, given that "types flow through" is how
.tsfiles work and is also similar to how the runtime works:export { foo } from "./foo"; // re-exposes whatever is in ./foo
It also seems like the inlining model doesn't work for nominal types: it would lead to spurious type incompatibility errors. That's probably why TS currently does not inline nominal types.
So if the choices are:
- two mental models, one of which leads to runtime errors
- a single mental model in which types are correct
I'd choose the latter. Please let me know if this is a misleading way of stating the choices!
I'm curious about this issue: is there a way to help progress this?
I could open a separate issue for Ryan Cavanaugh (@RyanCavanaugh) 's more fundamental topic re the point of a declaration file.
Following up on this issue: it's causing us some pain, so it would be good to have some guidance.
RyanCavanaugh commented
on Mar 31, 2020 MemberMore actionsMax Heiber (@mheiber) have you tried with
@nextlately? We recently had some PRs go through that might improve the emit to more closely match your expecations.If you're thinking of #37444, that's still out for review.
Reacted by Ryan CavanaughThanks Ryan and Wes! I'll check out Wes' PR.
Thanks for the recommendation. I tried #37444 and am not seeing a difference in the output for the example above.
Yeah, didn't think it would - you're looking for everything to be represented with
typeofqueries and indexed accesses where possible, essentially. Which is just plain not something we even track the information to do right now.Naturally, you can always just write the type annotations yourself if preserving that last bit of origin information is important (eg, because you expect augmentations somehow).
Wesley Wigham (@weswigham) thanks for explaining. After looking at your related PR, I think I can see how the information is not currently tracked.
Regarding design (rather than implementation), the fundamental issue seems to me to be that declaration files are neither:
- fully dynamic: types always "flow through" from transitive dependencies, just like values flow through at runtime
- fully static: types are always fully inlined
I gave some reasons above why I see advantages to the dynamic model (#37151 (comment)). One of the most compelling reasons, in my opinion, is that it's the only way I can see that will work well with nominal types.
Am I understanding the design issue correctly?
Daniel Rosenwasser (@DanielRosenwasser) re:
What would you have TypeScript emit in the case of the following?
export const exportNumNum = num + num; export const exportExtendedObj = { ...obj, someOtherProp: 200, };
A superpowered
typeofthat works for arbitrary expressions would solve this problem:export declare const exportNumNum: typeof num + num;
Would also address:
- Separate type application from function application #29043
- Make the type-level
typeofaware of generic arguments #28931 - https://stackoverflow.com/questions/60524543/how-can-i-get-the-type-of-a-function-application-in-typescript
Would
typeof with arbitrary expressionsbe worth considering in a separate issue? Titian Cernicova-Dragomir (@dragomirtitian) experimented with implementing this before.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.Reacted by Oliver Joseph Ash, Alex Vechy, Peter Shih, ExE Boss and electrovirReacted by ExE BossToni Ruottu (@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.Reacted by Sachin RajaMax Heiber (@mheiber) Have you find a solve for this problem?
IlinAlekseyS (@aleksey-ilin) I'm not writing TS full-time anymore, Rob Palmer (@robpalme) is more up to date. But my understanding is that this is a fundamental issue with TS not picking a consistent model for type inlining.
IlinAlekseyS (@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.export interface WrappedProblemType extends ProblemType {}
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 #42149Reacted by Brandon TsangReacted by Sachin Raja, Bnaya Peretz, Magnus, Thomas F. K. Jorna and Brandon TsangThe fact that the TypeScript compiler does this has caused many issues for me, including:
- JSDoc comments on the original code are not preserved in the inlined types. (example, every method is missing its JSDoc comment)
- Large union type aliases that get inlined (multiple times throughout the project) explode the declaration file size and make editor tooltips impossible to read. (example, scroll to the right)
- Sometimes type imports from dependencies are wiped out entirely, breaking everything. (example, fixed by writing my own declaration file here)
Reacted by Brandon TsangThank you for the examples.
There's more work on the way related to Isolated Declarations that may mitigate 1 and 2.
3 just sounds like a bug. If you have a small repro, please file a standalone issue.
Just ran into this little issue too.
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?
I've got a package I'm trying to use types from. For the sake of this repro it's "./bar";
foo.ts
import { Bar } from "./bar"; export const foo = <T extends Bar = Bar>(bar: T) => "";
bar.ts
export const bar = ["a", "b", "c"] as const; export type Bar = (typeof bar)[number];
emits:
import { Bar } from "./bar"; export declare const foo: <T extends Bar = "a" | "b" | "c">(param: T) => string;
Which is kinda both worlds? We've got the extends Bar but then the inlined definition? Which is particularly tricky when the type of Bar gets updated in a separate package update.
If I change
Barto the type'a' | 'b' | 'c'it won't inline the type.- added a commit that references this issue
on May 12, 2025
TypeScript Version: 3.8.3, 3.8.1, probably others
Search Terms:
declaration inlining, dts inlining, declaration inline, inline literal, declaration literal
Code
tsc index.ts --declarationExpected behavior:
Declaration emit for
parent.tsshould not inline types.Actual behavior:
Today, declaration emit for
parent.tsinlines the types and eliminates the import of thechild.d.tstype definition.This is a correctness issue, because consumers of
parent.d.tswill not get the correct types if the types inchild.d.tschange.In practice, this is most likely to happen when
parentandchildare in separate packages, because they are published independently, i.e. an application usesparent-packagewhich uses types fromchild-package. This is exacerbated by the current practice on npm ofparent-packagedepending on an unpinned version, usingpackage.jsondependency syntax"child-package": "*".