microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
98.02k stars 12.18k forks source link

"The inferred type of X cannot be named without a reference to Y" (TS2742) occurs when multiple modules with the same package ID are resolved #47663

Open renke opened 2 years ago

renke commented 2 years ago

Bug Report

🔎 Search Terms

🕗 Version & Regression Information

Problems occurs with 4.5.x and 4.6.x and most likely earlier versions (I've tried a few other versions). It stills occurs on 4.7.0-dev.20220321.

⏯ Playground Link

I've created minimal repository that shows the problem: https://github.com/renke/typescript-package-id-merge-repro

The problem occurs when using pnpm (due to the modules layout it uses). No problems occur when using npm/yarn.

To reproduce the problem run pnpm install and then pnpm check pnpx tsc -b.

💻 Code

I don't think the code itself matters to much except from the fact that it in fact does not have explicit type annotations (which is kind of the idea when using zod and by extension @renke/vommer).

import { vod } from "@renke/vommer";
import { z } from "zod";

export const UserId = vod("UserId", z.string());

export type UserId = z.infer<typeof UserId>;

export const Email = vod("Email", z.string().min(0));

export type Email = z.infer<typeof Email>;

export const User = vod(
  "User",
  z.object({
    id: UserId,
    email: Email,
  })
);

export type User = z.infer<typeof User>;

🙁 Actual behavior

The following error occurs when trying to build a composite TypeScript project (same happens when just using declaration: true).

The inferred type of 'User' cannot be named without a reference to '.pnpm/@renke+vo@0.2.0/node_modules/@renke/vo'. This is likely not portable. A type annotation is necessary.

The dependency tree of @renke/vommer

@renke/vommer 0.2.0
├── @renke/vo 0.2.0
└─┬ @renke/vod 0.2.0
  └── @renke/vo 0.2.0

Looking at the resolution trace TypeScript tries to resolve @renke/vo two times the first time from @renke/vommer and the second time from @renke/vod. Both end up having the package ID @renke/vo/dist/index.d.ts@0.2.0.

Using "preserveSymlinks": true doesn't solve the problem in so far that the error disappears but the type is inferred as any, because the dependencies of @renke/vommer are not found. Also I don't actually want to use it.

🙂 Expected behavior

The error should not occur when there are two (or more) modules that have the same resolved package ID. It would make sense for the error to occur when they have different versions.

zengguirong commented 2 years ago

same problem

renke commented 2 years ago

So, aside from finding a solution to this problem, is my assumption correct that having multiple packages with typings that have the same version should not cause any problems?

I'd also appreciate any hints on where to look at the source code to solve this problem.

dohooo commented 2 years ago

same problem!

quadristan commented 1 year ago

@renke i have a repro where everything has the same version here : https://github.com/quadristan/ts-indirect-type-reference-bug

agmitron commented 1 year ago

same problem :( have you solved it?

renke commented 1 year ago

I haven't solved it yet, but there is also a similar issue https://github.com/microsoft/TypeScript/issues/48212 with a milestone of TypeScript 4.8.0. Let's hope it will be solved soon.

StarHosea commented 1 year ago

same problem

kiastorm commented 1 year ago

I got this issue because the mentioned dependency had this code in its index.d.ts:

declare module 'react' {
  interface DOMAttributes<T> {
    css?: InterpolationWithTheme<any>
  }
}

and the file it was complaining in was using React.HTMLAttributes<HTMLElement> which references React.DOMAttributes. I worked around this issue by omitting the declared properties Omit<React.HTMLAttribute<HTMLElement>, "css">

laurix-dev commented 1 year ago

Same problem :(

vaibhavkumar-sf commented 1 year ago

Same Problem for me too :(

ivanbanov commented 1 year ago

is there any expected timeline for a fix for this problem?

grmkris commented 1 year ago

same here

patroza commented 1 year ago

@RyanCavanaugh

yingpengsha commented 1 year ago

Same problem :(

Evertt commented 1 year ago

same here

xlboy commented 1 year ago

same here

shellscape commented 1 year ago

What's worse is that neither // @ts-ignore nor // @ts-expect-error will allow us to ignore the incorrect error. This is a pretty gnarly bug.

mrmeku commented 1 year ago

This bug occurs pretty readily when using pnpm since the node_modules directory layout is different under pnpm than npm/yarn. Any npm package whose types reference another npm package's type will generate the error

I've made a minimal reproduction here: https://github.com/mrmeku/portable_types_repro

shellscape commented 1 year ago

I've been deep-diving the issues related to this error and symlinks and pnpm specifically. Since 2019 it appears that this regression is reintroduced at least twice for each major version. That would suggest that tests are not sufficient for the case, nor to catch the regression.

quadristan commented 1 year ago

I have a repro + a workaround/solution here

https://github.com/quadristan/ts-indirect-type-reference-bug

tl-dr:


import type {} from "Y"; // or whatever Y really refers to
i7eo commented 1 year ago

I have a repro + a workaround/solution here

https://github.com/quadristan/ts-indirect-type-reference-bug

tl-dr:

import type {} from "Y"; // or whatever Y really refers to

Nice job! This is by far the best practice.👍

RyanCavanaugh commented 1 year ago

I've been deep-diving the issues related to this error and symlinks and pnpm specifically. Since 2019 it appears that this regression is reintroduced at least twice for each major version. That would suggest that tests are not sufficient for the case, nor to catch the regression.

The reason you see a history of this "regressing" at each version is that pnpm patches TypeScript when you install it, so every time TS updates, there's a few days where it appears to not work because pnpm hasn't updated their patching code yet.

RIP21 commented 1 year ago

@RyanCavanaugh really? Do PNPM is patching TS in order for it to be compatible each and every time it installs it? That's a huge surprise for me really. Feels very unsustainable and kinda scary. Why is TSC itself is not compatible with PNPM? I hear about the first time in my life and never saw anything like that in the PNPM change log (not that I reading them (aside from major releases)) :)

shellscape commented 1 year ago

I've been deep-diving the issues related to this error and symlinks and pnpm specifically. Since 2019 it appears that this regression is reintroduced at least twice for each major version. That would suggest that tests are not sufficient for the case, nor to catch the regression.

The reason you see a history of this "regressing" at each version is that pnpm patches TypeScript when you install it, so every time TS updates, there's a few days where it appears to not work because pnpm hasn't updated their patching code yet.

could you point me at the code responsible for that? I'm a core contributor to PNPM and this is the first time I'm hearing of it. not to say that I disagree with you or that it isn't true, I'm just unaware of that.

RyanCavanaugh commented 1 year ago

Sorry, you're right. I've misconfused this with yarn.

RyanCavanaugh commented 1 year ago

@shellscape while I've got you here, we've been trying to reach security@ pnpm.io regarding an issue we identified a while back, but haven't received a reply. Can you poke the appropriate folks to take a look?

shellscape commented 1 year ago

sure thing. feel free to DM me info on twitter as well.

aabccd021 commented 1 year ago

In my case the error looks like this

The inferred type of 'Foo' cannot be named without a reference to '.pnpm/@morphic-ts+model-algebras@3.0.0_.../node_modules/@morphic-ts/model-algebras/lib/types'. This is likely not portable. A type annotation is necessary

The type definition wasn't exported from root of the package but it was from/lib/types.

So instead of just importing the package:

import type {} from '@morphic-ts/model-algebras'

I had to specify the path which the type definition was exported:

import type {} from '@morphic-ts/model-algebras/lib/types'
matthew-dean commented 1 year ago

@quadristan

You saved my life! I added:

import type {} from 'react'

This is definitely a super-nasty TypeScript bug. One of the worst I've encountered because it can't be ignored and, in this case, took me hours to debug and find this solution.

CarlRibbegaardh commented 1 year ago
import type {} from "Y"

Oooh! Thank you so much!! And this can be done in the root index.ts or anywhere really. So good. 😄

ggascoigne commented 1 year ago

I wonder if the root cause is at all similar to this eslint issue, I've been using that patch to modify eslint to stat linked files to work out if they are actually the same. Perhaps a similar approach might work here?

ggascoigne commented 1 year ago

And because I didn't see it listed here. I had this same problem and couldn't get the "import type {} from ..." trick to work, but adding preserveSymlinks: true to my tsconfig compiler options completely fixed my problem. See this issue for more comments. Probably relevant, but my issue was with a pnpm monorepo.

raulsanchez1024 commented 1 year ago

@ggascoigne - This got rid of the error but broke inference in my Redux store. Fwiw, I'm also encountering this issue in a monorepo (turborepo) + pnpm

ggascoigne commented 1 year ago

@ggascoigne - This got rid of the error but broke inference in my Redux store. Fwiw, I'm also encountering this issue in a monorepo (turborepo) + pnpm

Yeah, I've just found another project, pnpm and monorepo, where neither solution works. I've slowly been converting all my projects over to use pnpm and this bug is making me rethink that whole plan.

shellscape commented 1 year ago

@ggascoigne I'm a pnpm core contributor and if you need help getting past this, please ping

quadristan commented 1 year ago

Happy to help too. My simple repro if the issue is using pnpm and I have a fix for it

https://github.com/quadristan/ts-indirect-type-reference-bug

Possibly also you are using another tool that tinker with the imports. Maybe a ttsc? ts-loader ? Webpack?

ggascoigne commented 1 year ago

It's a monorepo where the main app uses next.js, and the library module is pulled in using next.js's experimental transpilePackages support. And it was working fine with this setup until I added a library that had previously only been used in that library to the main app. Now when code in the library is compiled in the app it gets a conflict.

The specific library is tss-react, it's installed in two places:

$ ls -l apps/acnw/node_modules
...
lrwxr-xr-x   1 ggp   93 Dec 14 16:45 tss-react -> ../../../node_modules/.pnpm/tss-react@4.4.4_7gkcoq2nf4xufkduddb7kah6jm/node_modules/tss-react
$ ls -l packages/ui/node_modules
...
lrwxr-xr-x   1 ggp   93 Dec 11 15:33 tss-react -> ../../../node_modules/.pnpm/tss-react@4.4.4_hp5f5nkljdiwilp4rgxyefcplu/node_modules/tss-react
$ls -ld node_modules/.pnpm/tss-react*
drwxr-xr-x  3 ggp  staff  96 Dec 14 16:45 node_modules/.pnpm/tss-react@4.4.4_7gkcoq2nf4xufkduddb7kah6jm
drwxr-xr-x  3 ggp  staff  96 Dec 11 15:33 node_modules/.pnpm/tss-react@4.4.4_hp5f5nkljdiwilp4rgxyefcplu

eventually these resolve to identical hard linked files, though all of the directories along the way are unique.

And then it fails to compile with:

../../packages/ui/components/Card/Card.tsx:5:14
Type error: The inferred type of 'useStyles' cannot be named without a reference to '../../../../apps/acnw/node_modules/tss-react/types'. This is likely not portable. A type annotation is necessary.

  3 | import {} from 'tss-react/types'
  4 |
> 5 | export const useStyles = makeStyles()({
    |              ^

So, is this a pnpm bug? Well probably not really, but is it caused by pnpm, yes, it really is. And you can see from the error, the import trick isn't working in this situation. the preserveSymlinks trick didn't help on this one either, it just moved the error around.

quadristan commented 1 year ago

Can you try import {} from 'tss-react' instead import {} from 'tss-react/types' ?

Basically, using imports outside of the root folder could make old versions of typescript behave strangely... . See https://github.com/microsoft/TypeScript/issues/33079

I see that tss-react types is defining by-folder exports, but they do not redefine the types

ggascoigne commented 1 year ago

Well that worked, thank you. I thought that I'd tried that one, apparently not. And thanks for the explanation about what tss-react is doing, I'd no idea that that was even a thing.

cdaringe commented 1 year ago

Basically, using imports outside of the root folder could make typescript ... behave oddly.

hey @quadristan, im having a hard time following. can you ELI5 to help me catch up? It seemed to me that the imports were to dependencies local to the lib or apps local package.json(s) (eg workspace libs), despite being resolved deep off into the pnpm store. What am I missing? Also, hey @ggascoigne , long time no see. I hope you’re well!

quadristan commented 1 year ago

When using exports in the package.json file, you can specify sub-paths instead of importing the whole index, or diving into the package deployed folders on your disk

A good example is rxjs You can see that for example if you do import {something} from 'rxjs/operators' you can see that it impacts require('rxjs/xx'), import from'rxjs/xx', and it will also tell typescript where to find the types.

Doc is here

However, this feature is "quite new" ( i would have to confirm which versions brings it. i think ..4.7 ? ) If there is any issue regarding typings and importing from a sub-folder of a package that is using exports .. i simply recomand to import the root of the package.. or to upgrade TS version.

donggua-nor commented 1 year ago

因为我没有看到它在这里列出。我遇到了同样的问题,无法从......获得“导入类型 {}”技巧工作,但添加到我的 tsconfig 编译器选项完全解决了我的问题。有关更多评论,请参阅此问题。可能相关,但我的问题是 pnpm 单存储库。preserveSymlinks: true

same problem with using pnpm but not monorepo. "preserveSymlinks": true also worked.

Keep looking for the real problem and the better solution

patroza commented 1 year ago

Basically there are 3 issues playing a role:

pnpm specific:

If no one can run with this, I hope to get some time to look at a clean patch and error report

marwan38 commented 1 year ago

Hey @patroza, pretty cool that you've managed to "properly" patch tsc. I was wondering if those patches are for the special tsplus typescript? Only second point mentions that it's for tsplus. Not that I know the difference between tsplus/tsc and microsoft/tsc but I wanted to apply your patch if it works properly.

wladpaiva commented 1 year ago

Super weird bug, I've created a reproduction repo so I could test some workarounds and turned out I've manage to get it working without changing pnpm configuration or using other hack BUT.... now I find that typescript breaks when types come from aliased files.

Repo: https://github.com/wladiston/create-t3-ts-error-repro

If you clone this repo, and change this line from instead of using ../trpc to ~/trpc, you'll immediately get typechecking errors on the other packages. Change any file to load from ~/ and you'll get the same error.

I though it could be because of the * in the tsconfig or even because I use the same alias on the other packages but even though setting a specific alias to a file it gives the same kind of error.

Screenshot 2023-03-02 at 10 37 20 am Screenshot 2023-03-02 at 10 37 09 am Screenshot 2023-03-02 at 10 37 59 am
moatorres commented 1 year ago

Peeps, I've tried almost all workarounds found here without success.

However, since the error (kind of) happened "out of nowhere" today while I was building a package npm run build AND I didn't have changed anything code-wise since the last build (which was yesterday) EXCEPT a couple of dependency tweaks (upgrading packages), I've decided to delete my node_modules folder and reinstall everything and the error simply disappeared. 🤷‍♂️

After reading this thread, I had a gut feeling that it could be actually some transient-ish dependency-ish issue. Who knows?! Fixed to me. 🙇‍♂️

evelant commented 1 year ago

Just a cautionary note -- preserveSymlinks: true can be a double edged sword. I wasn't able to use it because It can cause havoc with "goto definition" in vscode. Not sure if that's a bug in vscode or tsserver.

Sometimes you'll cmd+click and end up opening a path like packages/my_module/node_modules/my_other_module/src/foo.ts when you should have been navigated to packages/my_other_module/src/foo.ts. When this happens tsserver and vscode go nuts with errors all over the place because they can't resolve tsconfig or other files correctly from the "fake" symlink path that got opened.

arantespp commented 1 year ago

In my case, I changed to interface instead of type when reexporting.

I'm using monorepo with pnpm with ui and forms package. On ui I was exporting the InputProps this way:

import { type InputProps as InputPropsUi } from 'theme-ui;

export type InputProps = InputPropsUI & {
  leadingIcon?: IconType;
  trailingIcon?: IconType;
}

And on forms, I was importing:

import { InputProps } from "ui";

That was causing the error:

src/FormFieldInput.tsx(5,14): error TS2742: The inferred type of 'FormFieldInput' cannot be named without a reference to '.pnpm/@theme-ui+components@0.15.7_@emotion+react@11.10.6_react@18.2.0/node_modules/@theme-ui/components'. This is likely not portable. A type annotation is necessary.

The error disappeared when I changed from type to interface on ui:

import { type InputProps as InputPropsUi } from 'theme-ui;

export interface InputProps extends InputPropsUI {
  leadingIcon?: IconType;
  trailingIcon?: IconType;
}
pkerschbaum commented 1 year ago

We at @hokify migrated a monorepo (~170 packages) based on npm workspaces to pnpm workspaces, and we had this error message a couple of times.
I want to share some insights we gained while resolving them - hope it helps others!

The problem

I created a minimal repository showing the error https://github.com/pkerschbaum/typescript-issue-pnpm-workspaces-this-is-likely-not-portable.
The package, just called express-app, has one dependency, express.
It takes only three lines to get the error:

import * as express from "express";
const app = express();
//     ^ The inferred type of 'app' cannot be named without a reference to '.pnpm/@types+express-serve-static-core@4.17.33/node_modules/@types/express-serve-static-core'. This is likely not portable. A type annotation is necessary.ts(2742)
export { app };

Seems like there must be some prerequisites fulfilled to get this error:

While compiling, for every .ts source file, tsc has to create the corresponding declaration file .d.ts. But in declaration files, type inference can technically not work; there are only types, no function bodies or assignments etc. - so where should a type even get inferred from?
Consequently, when tsc compiles the source code and wants to output the .d.ts declaration files, then it must explicitly type the variables and functions. It must fill in the "holes" we leave when relying on type inference, so to say.

For the code above, tsc wants to create the following declaration file:

declare const app: import("express-serve-static-core").Express;
export { app };

This is because how express is typed:

import * as core from 'express-serve-static-core';
declare function e(): core.Express;

But here's the thing: express-serve-static-core is not a dependency of express-app. Only express is. So any package consuming that declaration file of express-app might not have express-serve-static-core installed.
Hence the error This is likely not portable.

Workarounds

(the repository https://github.com/pkerschbaum/typescript-issue-pnpm-workspaces-this-is-likely-not-portable has a branch for each of these workarounds)

1. Disable declaration emit of express-app (GitHub compare)

Just set "declaration": false in tsconfig.json. Without declaration emit, tsc does not have to replace type inference by explicit types.
Is of course only an option if the package is not consumed by any other TS package.

2. Explicitly type the variable/function (GitHub compare)

import * as express from "express";
- const app = express();
+ const app: ReturnType<typeof express> = express();
export { app };

tsc will keep that explicit type in the declaration file:

import * as express from "express";
declare const app: ReturnType<typeof express>;
export { app };

Now the code only imports express, which is a dependency of express-app - thus it is valid and portable.

3. Within the types of the library, export the interface/type which is required in your codebase

3.1 Re-export the interface in the types of the dependency. (GitHub compare)

When we change the types of express like this:

import * as core from 'express-serve-static-core';
+ export { Express } from 'express-serve-static-core';

...the code compiles!

This is because now, tsc can emit a valid declaration file:

import * as express from "express";
declare const app: express.Express;
export { app };

But I think Express must be an interface and not a type for this to work, see also my comment below.

You can use pnpm patch to modify the code of dependencies.

3.2 Introduce an "intermediary interface" in the types of the dependency (GitHub compare)

If workaround 3.1 does not work because Express is a type (and not an interface), you can introduce an "intermediary interface" in the types of the dependency like this:

import * as core from 'express-serve-static-core';
+ export interface MyExpress extends core.Express {}
- declare function e(): core.Express;
+ declare function e(): MyExpress;

This fixes the problem in a similar way like workaround 3.1 - the declaration then is:

import * as express from "express";
declare const app: express.MyExpress;
export { app };

4. Add the missing transitive dependency to the direct dependencies (GitHub compare)

We can fix compilation of express-app by adding express-serve-static-core to the dependencies.
Then, tsc can emit the declaration file and any package installing express-app will also get express-serve-static-core.

Drawback is of course that now the implementation details of express leak into our express-app.

5. (⚠️ problematic) Switch from "isolated mode" to "hoisted mode" (GitHub compare)

In pnpm you can configure the option node-linker=hoisted. This will put express-serve-static-core to the root of node_modules, thus tsc will emit the declaration file

import * as core from 'express-serve-static-core';
declare function e(): core.Express;

...because it thinks this dependency is available for sure.

But that might not be the case; imagine you publish express-app to the npm registry and someone installs it in a project using isolated mode - then express-serve-static-core is not present and it could introduce TypeScript errors on their side!

6. (⚠️ problematic) Add the transitive dependency as path alias, and import it explicitly (GitHub compare)

Add this to tsconfig.json:

{
  "compilerOptions": {
    "paths": {
      "express-serve-static-core": [
        "./node_modules/.pnpm/@types+express-serve-static-core@4.17.33/node_modules/@types/express-serve-static-core"
      ]
    }
  }
}

...and an import statement to the TS source file:

import * as express from "express";
+ import "express-serve-static-core";
const app = express();
export { app };

The import statement will put express-serve-static-core into the tsc compilation process, and thanks to the path alias it finds it.
But this suffers from the same problem as the previous workaround - the declaration file:

import * as core from 'express-serve-static-core';
declare function e(): core.Express;

...could be problematic for package consumers.

7. (⚠️ problematic) Enable "preserveSymlinks" in tsconfig.json (GitHub compare)

tsc emits a declaration file which is inherently broken:

declare const app: core.Express; // TS error here: Cannot find namespace 'core'
export { app };

8. (⚠️ problematic) Use declare module to silent the error (GitHub compare)

Add a file global.d.ts and add:

declare module "express-serve-static-core" {}

This silences the error, however the emitted declaration is broken as it is in workaround #7:

declare const app: core.Express; // TS error here: Cannot find namespace 'core'
export { app };
ivancuric commented 11 months ago

@pkerschbaum sadly none of these work for my use case. The best I could do is use node-linker=hoisted, but that also requires additional work, explained further below.

I have a project structure with 3 different "lib" packages and one app package:

  1. Wasm/emscripten compilation and TS types (without declarations). This package also uses https://www.npmjs.com/package/@types/emscripten.
  2. the web worker which loads the Wasm, has Comlink as a dependency
  3. the main JS bundle that bundles up the web worker and Wasm resources, also has Comlink as a dependency.

There's also an app that consumes the main JS bundle, and copies the web worker and the Wasm resources to a public folder.

The only thing that works is that I install all peer deps as dependencies or dev dependencies, and import an empty type from each of them. This means the Wasm, worker and main package, including @types/emscripten and Comlink.

When an error references a package, I have to be able to do import type {} from "package";, which fixes it. Hoisting helps insofar so that I don't need to explicitly declare peer dependencies.