TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

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

Open RyanCavanaugh opened this issue 2 years ago • 10 comments

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/@[email protected]/node_modules/@fluentui/merge-styles'. This is likely not portable. A type annotation is necessary.

🙂 Expected behavior

No error

RyanCavanaugh avatar Mar 10 '22 22:03 RyanCavanaugh

This seems to be related to the issue I've opened earlier here: https://github.com/microsoft/TypeScript/issues/47663

renke avatar Mar 24 '22 18:03 renke

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

AkonXI avatar Apr 26 '22 08:04 AkonXI

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

renke avatar Jul 15 '22 09:07 renke

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

alex-kinokon avatar Jul 21 '22 12:07 alex-kinokon

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/[email protected] and @types/[email protected] 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.

stevenxu-db avatar Aug 03 '22 18:08 stevenxu-db

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.

RyanCavanaugh avatar Aug 04 '22 23:08 RyanCavanaugh

Same problem

vaibhavkumar-sf avatar Sep 02 '22 16:09 vaibhavkumar-sf

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

saiichihashimoto avatar Sep 02 '22 20:09 saiichihashimoto

I've made a smaller reproduction here: https://github.com/microsoft/TypeScript/issues/47663#issuecomment-1267631748

Hope its helpful

mrmeku avatar Oct 04 '22 22:10 mrmeku

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.

@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 https://github.com/microsoft/TypeScript/issues/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?

tinganho avatar Oct 19 '22 13:10 tinganho