Skip to content
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

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

Open
RyanCavanaugh opened this issue Mar 10, 2022 · 13 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

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