TypeScript
TypeScript copied to clipboard
Module "..." cannot be named without a reference to "..." error when decl emitting references to nested modules
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
This seems to be related to the issue I've opened earlier here: https://github.com/microsoft/TypeScript/issues/47663
Althought I don't know why , "preserveSymlinks": true
resolved my problems
I think preserveSymlinks
can sometimes solve this (or at least similar problems) but that's not really solution to the underlying problem.
This error is also not suppressible through @ts-expect-error
. It will just complain Unused '@ts-expect-error' directive.
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:
- 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
. - 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.
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.
Same problem
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.
I've made a smaller reproduction here: https://github.com/microsoft/TypeScript/issues/47663#issuecomment-1267631748
Hope its helpful
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?