Skip to content

Module "..." cannot be named without a reference to "..." error when decl emitting references to nested modules #48212

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

Open
RyanCavanaugh opened this issue Mar 10, 2022 · 14 comments
Assignees
Labels
Bug A bug in TypeScript Domain: Declaration Emit The issue relates to the emission of d.ts files Rescheduled This issue was previously scheduled to an earlier milestone

Comments

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Mar 10, 2022

Bug Report

πŸ”Ž Search Terms

cannot be named without a reference to symlink

πŸ•— Version & Regression Information

  • This is the behavior in every version I tried

⏯ Playground Link

N/A

πŸ’» Code

https://github.com/jcreamer898/monorepo-examples/tree/main/pnpm-example

TL;DR file layout:

// monorepo-examples\pnpm-example\packages\pkg-a\index.ts
import { FontSizes, FontWeights, ITheme, IStyle } from "@fluentui/react";

// This expression's inferred type depends on @fluentui/merge-styles
export const something = { ... 

@fluentui/react is nested in /monorepo-examples/pnpm-example/node_modules/.pnpm/@fluentui+react

Adding a blank import to @fluentui/merge-styles in index.ts makes the problem go away

πŸ™ Actual behavior

src/index.ts:28:14 - error TS2742: The inferred type of 'personScopeListItemOverrides' cannot be named without a reference to '.pnpm/@fluentui+merge-styles@8.3.0/node_modules/@fluentui/merge-styles'. This is likely not portable. A type annotation is necessary.

πŸ™‚ Expected behavior

No error

@RyanCavanaugh RyanCavanaugh added Bug A bug in TypeScript Domain: Declaration Emit The issue relates to the emission of d.ts files labels Mar 10, 2022
@RyanCavanaugh RyanCavanaugh added this to the TypeScript 4.7.1 milestone Mar 10, 2022
@renke
Copy link

renke commented Mar 24, 2022

This seems to be related to the issue I've opened earlier here: #47663

@AkonXI
Copy link

AkonXI commented Apr 26, 2022

Althought I don't know why , "preserveSymlinks": true resolved my problems

@renke
Copy link

renke commented Jul 15, 2022

I think preserveSymlinks can sometimes solve this (or at least similar problems) but that's not really solution to the underlying problem.

@alex-kinokon
Copy link

This error is also not suppressible through @ts-expect-error. It will just complain Unused '@ts-expect-error' directive.

@stevenxu-db
Copy link

stevenxu-db commented Aug 3, 2022

Thanks for scheduling this for 4.8! I'm looking forward to tracking the fix. Would it be possible to get some insight from language designers on what this is trying to protect us from[1]? Understanding this or having some clue about the recommended mitigation while we await upstream fix would be helpful. Here's a dump of what I've found so far.

A note, the problem I'm seeing might not be representative of the full presentation of this error. To provide some context, our use case similar to pnpm's where the real path of node_modules lives outside of the project root (we create carefully sandboxed roots for each build action to ensure we have a clean build graph), and the main area I'm seeing this error is in React code like export const A = forwardRef<HTMLElement, B>(...) where B is in a third-party package whose definition relies on a transitive dep C.

For reference, the type definition of @types/react@17.0.15 and @types/react@18.0.15 forwardRef is:

    function forwardRef<T, P = {}>(render: ForwardRefRenderFunction<T, P>): ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<T>>;

Our code looks like:

import * as DropdownMenu from `@radix-ui/react-dropdown-menu`;
export const Separator = forwardRef<HTMLDivElement, DropdownMenu.MenuSeparatorProps>(...);

And @radix-ui/react-dropdown-menu code is:

import * as MenuPrimitive from "@radix-ui/react-menu";
type MenuSeparatorProps = Radix.ComponentPropsWithoutRef<typeof MenuPrimitive.Separator>;
export interface DropdownMenuSeparatorProps extends MenuSeparatorProps {
}
export const DropdownMenuSeparator: React.ForwardRefExoticComponent<DropdownMenuSeparatorProps & React.RefAttributes<HTMLDivElement>>;
t

Using traditional node_modules linking, TS expands the generic arguments fully when generating the type declaration from our code. Turning on our pnpm-like linking method causes TS to fail to compile this file.

import type { ForwardRefExoticComponent, PropsWithoutRef, RefAttributes } from 'react';
export declare const Separator: import("react").ForwardRefExoticComponent<Pick<import("@radix-ui/react-menu").MenuSeparatorProps & import("react").RefAttributes<HTMLDivElement>, "className" | "children" | "..."> & import("react").RefAttributes<HTMLDivElement>>;

To begin, this seems like problematic behavior because only @radix-ui/react-dropdown-menu and not @radix-ui/react-menu is a direct dep of our code, so it's not guaranteed that import("@radix-ui/react-menu") resolves to the correct version when resolved from our code. I don't know if this is a configuration error on our part, a bug in TS, or some compromise to make the ecosystem work. Naively, I'd expect the generated type to look something like this:

import type { ForwardRefExoticComponent, PropsWithoutRef, RefAttributes } from 'react';
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
export declare const Separator: ForwardRefExoticComponent<PropsWithoutRef<DropdownMenu.MenuSeparatorProps> & RefAttributes<HTMLDivElement>>;

In fact, around 50% of exported types already look like this, but not all, for reasons I don't understand yet:

export declare const Content: import("react").ForwardRefExoticComponent<DropdownMenuProps & import("react").RefAttributes<HTMLDivElement>>;
export declare const Trigger: import("react").ForwardRefExoticComponent<DropdownMenu.DropdownMenuTriggerProps & import("react").RefAttributes<HTMLButtonElement>>;
export declare const Item: import("react").ForwardRefExoticComponent<DropdownMenu.DropdownMenuItemProps & import("react").RefAttributes<HTMLDivElement>>;
export declare const Label: import("react").ForwardRefExoticComponent<DropdownMenu.DropdownMenuLabelProps & import("react").RefAttributes<HTMLDivElement>>;
export declare const Separator: import("react").ForwardRefExoticComponent<Pick<import("@radix-ui/react-menu").MenuSeparatorProps & import("react").RefAttributes<HTMLDivElement>, "className" | "children" | "slot" | "style" | "title" | "key" | "color" | "translate" | "hidden" | "id" | "dir" | "accessKey" | "draggable" | "lang" | "prefix" | "contentEditable" | "inputMode" | "tabIndex" | "defaultChecked" | "defaultValue" | "suppressContentEditableWarning" | "suppressHydrationWarning" | "contextMenu" | "placeholder" | "spellCheck" | "radioGroup" | "role" | "about" | "datatype" | "inlist" | "property" | "resource" | "typeof" | "vocab" | "autoCapitalize" | "autoCorrect" | "autoSave" | "itemProp" | "itemScope" | "itemType" | "itemID" | "itemRef" | "results" | "security" | "unselectable" | "is" | "aria-activedescendant" | "aria-atomic" | "aria-autocomplete" | "aria-busy" | "aria-checked" | "aria-colcount" | "aria-colindex" | "aria-colspan" | "aria-controls" | "aria-current" | "aria-describedby" | "aria-details" | "aria-disabled" | "aria-dropeffect" | "aria-errormessage" | "aria-expanded" | "aria-flowto" | "aria-grabbed" | "aria-haspopup" | "aria-hidden" | "aria-invalid" | "aria-keyshortcuts" | "aria-label" | "aria-labelledby" | "aria-level" | "aria-live" | "aria-modal" | "aria-multiline" | "aria-multiselectable" | "aria-orientation" | "aria-owns" | "aria-placeholder" | "aria-posinset" | "aria-pressed" | "aria-readonly" | "aria-relevant" | "aria-required" | "aria-roledescription" | "aria-rowcount" | "aria-rowindex" | "aria-rowspan" | "aria-selected" | "aria-setsize" | "aria-sort" | "aria-valuemax" | "aria-valuemin" | "aria-valuenow" | "aria-valuetext" | "dangerouslySetInnerHTML" | "onCopy" | "onCopyCapture" | "onCut" | "onCutCapture" | "onPaste" | "onPasteCapture" | "onCompositionEnd" | "onCompositionEndCapture" | "onCompositionStart" | "onCompositionStartCapture" | "onCompositionUpdate" | "onCompositionUpdateCapture" | "onFocus" | "onFocusCapture" | "onBlur" | "onBlurCapture" | "onChange" | "onChangeCapture" | "onBeforeInput" | "onBeforeInputCapture" | "onInput" | "onInputCapture" | "onReset" | "onResetCapture" | "onSubmit" | "onSubmitCapture" | "onInvalid" | "onInvalidCapture" | "onLoad" | "onLoadCapture" | "onError" | "onErrorCapture" | "onKeyDown" | "onKeyDownCapture" | "onKeyPress" | "onKeyPressCapture" | "onKeyUp" | "onKeyUpCapture" | "onAbort" | "onAbortCapture" | "onCanPlay" | "onCanPlayCapture" | "onCanPlayThrough" | "onCanPlayThroughCapture" | "onDurationChange" | "onDurationChangeCapture" | "onEmptied" | "onEmptiedCapture" | "onEncrypted" | "onEncryptedCapture" | "onEnded" | "onEndedCapture" | "onLoadedData" | "onLoadedDataCapture" | "onLoadedMetadata" | "onLoadedMetadataCapture" | "onLoadStart" | "onLoadStartCapture" | "onPause" | "onPauseCapture" | "onPlay" | "onPlayCapture" | "onPlaying" | "onPlayingCapture" | "onProgress" | "onProgressCapture" | "onRateChange" | "onRateChangeCapture" | "onSeeked" | "onSeekedCapture" | "onSeeking" | "onSeekingCapture" | "onStalled" | "onStalledCapture" | "onSuspend" | "onSuspendCapture" | "onTimeUpdate" | "onTimeUpdateCapture" | "onVolumeChange" | "onVolumeChangeCapture" | "onWaiting" | "onWaitingCapture" | "onAuxClick" | "onAuxClickCapture" | "onClick" | "onClickCapture" | "onContextMenu" | "onContextMenuCapture" | "onDoubleClick" | "onDoubleClickCapture" | "onDrag" | "onDragCapture" | "onDragEnd" | "onDragEndCapture" | "onDragEnter" | "onDragEnterCapture" | "onDragExit" | "onDragExitCapture" | "onDragLeave" | "onDragLeaveCapture" | "onDragOver" | "onDragOverCapture" | "onDragStart" | "onDragStartCapture" | "onDrop" | "onDropCapture" | "onMouseDown" | "onMouseDownCapture" | "onMouseEnter" | "onMouseLeave" | "onMouseMove" | "onMouseMoveCapture" | "onMouseOut" | "onMouseOutCapture" | "onMouseOver" | "onMouseOverCapture" | "onMouseUp" | "onMouseUpCapture" | "onSelect" | "onSelectCapture" | "onTouchCancel" | "onTouchCancelCapture" | "onTouchEnd" | "onTouchEndCapture" | "onTouchMove" | "onTouchMoveCapture" | "onTouchStart" | "onTouchStartCapture" | "onPointerDown" | "onPointerDownCapture" | "onPointerMove" | "onPointerMoveCapture" | "onPointerUp" | "onPointerUpCapture" | "onPointerCancel" | "onPointerCancelCapture" | "onPointerEnter" | "onPointerEnterCapture" | "onPointerLeave" | "onPointerLeaveCapture" | "onPointerOver" | "onPointerOverCapture" | "onPointerOut" | "onPointerOutCapture" | "onGotPointerCapture" | "onGotPointerCaptureCapture" | "onLostPointerCapture" | "onLostPointerCaptureCapture" | "onScroll" | "onScrollCapture" | "onWheel" | "onWheelCapture" | "onAnimationStart" | "onAnimationStartCapture" | "onAnimationEnd" | "onAnimationEndCapture" | "onAnimationIteration" | "onAnimationIterationCapture" | "onTransitionEnd" | "onTransitionEndCapture" | "asChild"> & import("react").RefAttributes<HTMLDivElement>>;
export declare const TriggerItem: import("react").ForwardRefExoticComponent<DropdownMenu.DropdownMenuTriggerItemProps & import("react").RefAttributes<HTMLDivElement>>;
export declare const CheckboxItem: import("react").ForwardRefExoticComponent<Pick<import("@radix-ui/react-menu").MenuCheckboxItemProps & import("react").RefAttributes<HTMLDivElement>, "className" | "children" | "slot" | "style" | "title" | "key" | "color" | "translate" | "hidden" | "disabled" | "id" | "dir" | "accessKey" | "draggable" | "lang" | "prefix" | "contentEditable" | "inputMode" | "tabIndex" | "checked" | "defaultChecked" | "defaultValue" | "suppressContentEditableWarning" | "suppressHydrationWarning" | "contextMenu" | "placeholder" | "spellCheck" | "radioGroup" | "role" | "about" | "datatype" | "inlist" | "property" | "resource" | "typeof" | "vocab" | "autoCapitalize" | "autoCorrect" | "autoSave" | "itemProp" | "itemScope" | "itemType" | "itemID" | "itemRef" | "results" | "security" | "unselectable" | "is" | "aria-activedescendant" | "aria-atomic" | "aria-autocomplete" | "aria-busy" | "aria-checked" | "aria-colcount" | "aria-colindex" | "aria-colspan" | "aria-controls" | "aria-current" | "aria-describedby" | "aria-details" | "aria-disabled" | "aria-dropeffect" | "aria-errormessage" | "aria-expanded" | "aria-flowto" | "aria-grabbed" | "aria-haspopup" | "aria-hidden" | "aria-invalid" | "aria-keyshortcuts" | "aria-label" | "aria-labelledby" | "aria-level" | "aria-live" | "aria-modal" | "aria-multiline" | "aria-multiselectable" | "aria-orientation" | "aria-owns" | "aria-placeholder" | "aria-posinset" | "aria-pressed" | "aria-readonly" | "aria-relevant" | "aria-required" | "aria-roledescription" | "aria-rowcount" | "aria-rowindex" | "aria-rowspan" | "aria-selected" | "aria-setsize" | "aria-sort" | "aria-valuemax" | "aria-valuemin" | "aria-valuenow" | "aria-valuetext" | "dangerouslySetInnerHTML" | "onCopy" | "onCopyCapture" | "onCut" | "onCutCapture" | "onPaste" | "onPasteCapture" | "onCompositionEnd" | "onCompositionEndCapture" | "onCompositionStart" | "onCompositionStartCapture" | "onCompositionUpdate" | "onCompositionUpdateCapture" | "onFocus" | "onFocusCapture" | "onBlur" | "onBlurCapture" | "onChange" | "onChangeCapture" | "onBeforeInput" | "onBeforeInputCapture" | "onInput" | "onInputCapture" | "onReset" | "onResetCapture" | "onSubmit" | "onSubmitCapture" | "onInvalid" | "onInvalidCapture" | "onLoad" | "onLoadCapture" | "onError" | "onErrorCapture" | "onKeyDown" | "onKeyDownCapture" | "onKeyPress" | "onKeyPressCapture" | "onKeyUp" | "onKeyUpCapture" | "onAbort" | "onAbortCapture" | "onCanPlay" | "onCanPlayCapture" | "onCanPlayThrough" | "onCanPlayThroughCapture" | "onDurationChange" | "onDurationChangeCapture" | "onEmptied" | "onEmptiedCapture" | "onEncrypted" | "onEncryptedCapture" | "onEnded" | "onEndedCapture" | "onLoadedData" | "onLoadedDataCapture" | "onLoadedMetadata" | "onLoadedMetadataCapture" | "onLoadStart" | "onLoadStartCapture" | "onPause" | "onPauseCapture" | "onPlay" | "onPlayCapture" | "onPlaying" | "onPlayingCapture" | "onProgress" | "onProgressCapture" | "onRateChange" | "onRateChangeCapture" | "onSeeked" | "onSeekedCapture" | "onSeeking" | "onSeekingCapture" | "onStalled" | "onStalledCapture" | "onSuspend" | "onSuspendCapture" | "onTimeUpdate" | "onTimeUpdateCapture" | "onVolumeChange" | "onVolumeChangeCapture" | "onWaiting" | "onWaitingCapture" | "onAuxClick" | "onAuxClickCapture" | "onClick" | "onClickCapture" | "onContextMenu" | "onContextMenuCapture" | "onDoubleClick" | "onDoubleClickCapture" | "onDrag" | "onDragCapture" | "onDragEnd" | "onDragEndCapture" | "onDragEnter" | "onDragEnterCapture" | "onDragExit" | "onDragExitCapture" | "onDragLeave" | "onDragLeaveCapture" | "onDragOver" | "onDragOverCapture" | "onDragStart" | "onDragStartCapture" | "onDrop" | "onDropCapture" | "onMouseDown" | "onMouseDownCapture" | "onMouseEnter" | "onMouseLeave" | "onMouseMove" | "onMouseMoveCapture" | "onMouseOut" | "onMouseOutCapture" | "onMouseOver" | "onMouseOverCapture" | "onMouseUp" | "onMouseUpCapture" | "onSelect" | "onSelectCapture" | "onTouchCancel" | "onTouchCancelCapture" | "onTouchEnd" | "onTouchEndCapture" | "onTouchMove" | "onTouchMoveCapture" | "onTouchStart" | "onTouchStartCapture" | "onPointerDown" | "onPointerDownCapture" | "onPointerMove" | "onPointerMoveCapture" | "onPointerUp" | "onPointerUpCapture" | "onPointerCancel" | "onPointerCancelCapture" | "onPointerEnter" | "onPointerEnterCapture" | "onPointerLeave" | "onPointerLeaveCapture" | "onPointerOver" | "onPointerOverCapture" | "onPointerOut" | "onPointerOutCapture" | "onGotPointerCapture" | "onGotPointerCaptureCapture" | "onLostPointerCapture" | "onLostPointerCaptureCapture" | "onScroll" | "onScrollCapture" | "onWheel" | "onWheelCapture" | "onAnimationStart" | "onAnimationStartCapture" | "onAnimationEnd" | "onAnimationEndCapture" | "onAnimationIteration" | "onAnimationIterationCapture" | "onTransitionEnd" | "onTransitionEndCapture" | "asChild" | "textValue" | "onCheckedChange"> & import("react").RefAttributes<HTMLDivElement>>;

I have so far found 2 partial workarounds that sometimes work:

  1. Add an explicit type to the exported member. This isn't always possible because declaring the type correctly can sometimes require using non-exported types from the API whose type is inferred. But for our use case, this is possible, if verbose. This causes the exported declarations to match my expectation, which is to use the immediate dep @radix-ui/react-dropdown-menu and never try to import the transitive dep @radix-ui/react-menu.
  2. Add a useless import type {} from 'module-name' for the module that TS is complaining about. This isn't always possible because the module may not be a direct dependency of the code being compiled, and if it's a transitive dep, importing it directly could fail outright or technically resolve to the wrong version. This appears to cause TS to happily generate the code we saw during traditional nm linking, transitive dep import and everything.

Between the two, the first option seems better, even if it's technically different behavior, but I'd obviously like to make sure I'm not shooting myself in the foot somehow. Thanks!

[1] - My guess looking at the behavior and some past analysis is that it's trying to avoid unnamed deps that are technically resolveable at build time but are not distributed in a proper way that would be available to downstream users e.g. from node_modules in a user's home directory.

@RyanCavanaugh
Copy link
Member Author

Would it be possible to get some insight from language designers on what this is trying to protect us from

You get this error any time the declaration emitter can't synthesize a workable specifier for a module which it needs to name a type from. For example, if it appears that the only legal path is ../../other_module/foo via some file that's in <<outDir>>/whatever, then that's not likely to work because the disk layout of the produced artifacts don't really have implicit dependencies on what peer directories of the output directory have.

The logic to synthesize these specifiers starts with the easy route of "Has this already been imported?", in which case re-use is easy and fine. Immediately past that lie many dragons and it's easy to get into a novel corner case where there is a speakable name to a module but TS just can't figure it out. Adding the import yourself is the easiest way to resolve the situation.

This isn't always possible because the module may not be a direct dependency of the code being compiled, and if it's a transitive dep, importing it directly could fail outright or technically resolve to the wrong version.

Note that if this isn't possible, then the error is correct and working around it by manually adding an import you know to be invalid is, well, invalid.

@vaibhavkumar-sf
Copy link

Same problem

@saiichihashimoto
Copy link

We're also running into this with saiichihashimoto/sanity-typed-schema-builder#155. It's unclear what should happen here, considering transitive type dependencies should work.

@mrmeku
Copy link

mrmeku commented Oct 4, 2022

I've made a smaller reproduction here: #47663 (comment)

Hope its helpful

@tinganho
Copy link
Contributor

tinganho commented Oct 19, 2022

You get this error any time the declaration emitter can't synthesize a workable specifier for a module which it needs to name a type from. For example, if it appears that the only legal path is ../../other_module/foo via some file that's in <>/whatever, then that's not likely to work because the disk layout of the produced artifacts don't really have implicit dependencies on what peer directories of the output directory have.

@RyanCavanaugh does this problem occur due to TS thinking the resolved transitive dependency is being resolved "outside" of the project? Or is it just multiple module specifiers being synthesised to the same id as @renke mentioned in #47663?

If TS think it is the former, i.e. deps being resolved outside the project. I'm interested to know if outside just means parent or sibling directory relative to your project directory?

@RyanCavanaugh RyanCavanaugh added the Rescheduled This issue was previously scheduled to an earlier milestone label Feb 1, 2023
@unional
Copy link
Contributor

unional commented Mar 29, 2023

I got this when upgrading from 4.9.5 to 5.0.2 with no other changes.

using pnpm in monorepo

@MLoughry
Copy link

MLoughry commented Apr 3, 2023

I started seeing this after upgrading from 4.9 to 5.0, but only for one dependency, and only when building everything at once (rather than using project references), and only when using "resolvePackageJsonExports": true

anthonyblond added a commit to australiangreens/ag-internal-components that referenced this issue Aug 21, 2023
@gabrielcosi
Copy link

Same problem

@limwa
Copy link

limwa commented Mar 18, 2025

This isn't always possible because the module may not be a direct dependency of the code being compiled, and if it's a transitive dep, importing it directly could fail outright or technically resolve to the wrong version.

Note that if this isn't possible, then the error is correct and working around it by manually adding an import you know to be invalid is, well, invalid.

After reading the comments above and this comment (#58176 (comment)), I'm left with some questions, probably due to the fact I'm not very familiar with how TypeScript emits declarations, so I'll write down some of the thoughts I have to see if I'm understanding the problem correctly.

Let's say we have 3 packages: app, middle-library and base-library and that the dependency tree is as follows:

app
|
---> middle-library @^1.0.0
     | 
     ----> base-library @^1.4.5

So, if middle-library uses types provided by base-library, the declaration files emitted by tsc for middle-library will look something like this:

// middle-library/dist/index.d.ts
import type { ... } from "base-library/types";

// more declarations that use base-library's exported types go here...

Note that the types imported from base-library are not re-exported, unless the author of middle-library does that explicitly, which is expected.

Now, since app has middle-library as a direct dependency, it can import the types exported by middle-library. When TypeScript is resolving those types, it finds the imports from base-library and tries to resolve them. However, since base-library is not a direct dependency of app, TypeScript fails to resolve the import.

There are a few workarounds, listed in #47663 (comment), however, each has its own issues:

  • Disable declaration emit for app

    • If app is in a monorepo with Project References, then it must have composite: true and declaration emit cannot be turned off.
  • Explicitly type the variable/function in app

    • Although this might be possible/feasible in many situations, it can lead to a poor DX for the developer of app. This is because some libraries use very complex types, which are cumbersome to type by hand. Aditionally, this issue sometimes happens in libraries that make use of type inference and, therefore, typing the return type by hand goes against the purpose of making the return type inferable.
  • Within the types of the middle-library, export the interface/type which is required in app or introduce an "intermediary interface" in the types of the middle-library

    • In my opinion, this is the best workaround, however, currently, I don't know of any way for the developer of middle-library to know which types need to be reexported. An option to enforce such a constraint was already "proposed" (first sentence in the following comment, Check nearest package.json dependencies for possible package names for specifier candidatesΒ #58176 (comment)), and, at least from my point-of-view, I can see why such an option would be helpful (Check nearest package.json dependencies for possible package names for specifier candidatesΒ #58176 (comment)). Currently, since such an option does not exist, if the developer of middle-library really wants to be sure that the developers of app don't experience this issue, then middle-library needs to reexport all of the type imports it uses from base-library. As such, without such an option implemented in tsc, this approach is cumbersome for the developers of middle-library and hard to enforce in a CI, for instance. Also, sometimes, the developers of middle-library might forget to reexport a new type that they have started importing from base-library, which will then break app.

      However, even if middle-library reexports all of the types it imports from base-library, there's still another problem, which is outlined in Check nearest package.json dependencies for possible package names for specifier candidatesΒ #58176 (comment).

      But TypeScript favors import('sub_dep').Options over import('other_package').Options even if other_package explicitly exports Options.

      This means that even if middle-library reexports all of the types it imports from base-library, the type declarations emitted when building app will have imports that are not resolvable.

  • Add the missing transitive dependency to the direct dependencies

    • I'm not really knowledgeable enough about how TypeScript handles version mismatches, but what I'm guessing could happen is that app could install base-library as a dependency, which could install base-library@2.0.1. This could be an issue since the types exported by middle-library expect types from base-library@^1.4.5, a different major version. Again, I want to emphasize that I'm not sure if this is what really happens, and would really appreciate it if someone could correct me! Either way, in my opinion, installing a transitive dependency as a direct dependency would be problematic because what happens if middle-library declares a new dependency on another package? Then, suddenly, all of the dependents of middle-library also need to install it as a direct dependency. Otherwise, they risk getting declaration emit errors like this one. Doesn't this mean that what could otherwise be a minor or patch change for middle-library might instead become a breaking change?

Now, from what I understand, as explained above, there are no perfect workarounds for this problem and a more in-depth solution from tsc might be required.

I think an option to let library maintainers know what types they need to reexport is viable, but this would also need to take into account the package exports. I can also see TypeScript generating this "bulk type export" file automatically so it becomes a "standard".

I've also though of another approach that I would like to get your opinions on: since this is specific to .d.ts files, I'm guessing the import doesn't need to be accessible at runtime, since it is only resolved by TypeScript. Therefore, TypeScript could have a specific dependency specifier for transitive dependencies, something like this:

// app/dist/index.d.ts
import type { ... } from "middle-library::base-library/types";

This dependency specifier would be automatically created by TypeScript when a type from a transitive dependency is used. I think this solution would be the simplest for library authors and application developers to adopt, since everything is determined automatically by TypeScript. On the other hand, this would require changes to the current resolution mechanism, I'm guessing.

Finally, this was a very long message and I probably wrote a lot of stuff that's just wrong, so I'd appreciate it if you could correct me. Also, thank you for reading, this is an issue that I'm facing right now and the only way for me to work around it is to patch ~8 packages that I'm using, so... yeah, that's not very viable and I'm trying to understand what could be done to improve TypeScript's capabilities in this domain.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Domain: Declaration Emit The issue relates to the emission of d.ts files Rescheduled This issue was previously scheduled to an earlier milestone
Projects
None yet
Development

No branches or pull requests