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

Types resolving to any across packages in monorepo using project references #54653

Closed
ernestostifano opened this issue Jun 15, 2023 · 4 comments
Assignees
Labels
Needs More Info The issue still hasn't been fully clarified

Comments

@ernestostifano
Copy link

ernestostifano commented Jun 15, 2023

Bug Report

🔎 Search Terms

  • TypeScript issue monorepo references
  • TypeScript issue monorepo types resolving to any
  • TypeScript issue types depth limit
  • TypeScript issue resolution limit
  • TypeScript issue references type any

🕗 Version & Regression Information

  • I have tested versions from 4.9.5 to 5.1.3, all presenting the same behaviour regarding this issue.

⏯ Playground Link

  • N/A.

💻 Code

NOTE

Unfortunately I have not been able to replicate this issue in a reproduction repository because I think it is related to type resolution limits of tsserver (so it depends on the size and complexity of the codebase) and I am not allowed to give access to the original repository. However, I will try to be as precise and linear as possible, providing all relevant information.

If there are any additional debugging steps that I can perform or any other information I can gather, I would be happy to do so.

ISSUE SUMMARY

We have a monorepo (~17 packages and ~1287 files) using Yarn Workspaces and Yarn PnP with TS Project references configured as per documentation.

Everything seems to work just fine, but for some reason, some types get resolved to any when consumed in other packages.

  • This is only happening when packages are not built (no types directory generated).
  • This is only happening in some packages, even if all packages are configured in the same way.
  • This is only happening for a particular type (see details below).
  • All packages imports get resolved correctly.
  • I suspect that the issue is only present in tsserver, since the project builds without errors using tsc --build.
  • @typescript-eslint/eslint-plugin is able to resolve the type correctly (I assume it uses tsc and not tsserver internally) (see test 2 below).
  • Error is shown both in WebStorm and in VSCode.
  • All references have been properly set (done automatically with a script and then manually checked multiple times).
  • There are no circular dependencies or other strange stuff.
  • Packages have been renamed for confidentiality reasons and non related/relevant data have been removed when possible.

PROJECT CONFIGURATION

Root's File Structure
.
├── package.json
├── packages
│   ├── package-a
│   ├── package-b
│   ├── package-c
│   ├── package-d
│   ├── package-e
│   ├── package-f
│   ├── package-g
│   ├── package-h
│   ├── package-i
│   ├── package-j
│   ├── package-k
│   ├── package-l
│   ├── package-m
│   ├── package-n
│   ├── package-o
│   ├── package-p
│   ├── package-q
├── tsconfig.base.json
└── tsconfig.json
Root's tsconfig.base.json
{
    "compilerOptions": {
        "baseUrl": "./",
        "paths": {
            "@scope/*": ["./packages/*"]
        },
        "target": "ESNext",
        "isolatedModules": true,
        "resolveJsonModule": true,
        "esModuleInterop": false,
        "allowJs": false,
        "checkJs": false,
        "strict": true,
        "noImplicitAny": true,
        "strictNullChecks": true,
        "allowUnreachableCode": false,
        "allowSyntheticDefaultImports": true,
        "forceConsistentCasingInFileNames": true,
        "useDefineForClassFields": true,
        "removeComments": false,
        "noEmitOnError": true,
        "declaration": true,
        "declarationMap": true,
        "emitDeclarationOnly": true
    }
}
Root's tsconfig.json
{
    "files": [],
    "references": [
        {
            "path": "packages/package-a"
        },
        {
            "path": "packages/package-b"
        },
        {
            "path": "packages/package-c"
        },
        {
            "path": "packages/package-d"
        },
        {
            "path": "packages/package-e"
        },
        {
            "path": "packages/package-f"
        },
        {
            "path": "packages/package-g"
        },
        {
            "path": "packages/package-h"
        },
        {
            "path": "packages/package-i"
        },
        {
            "path": "packages/package-j"
        },
        {
            "path": "packages/package-k"
        },
        {
            "path": "packages/package-l"
        },
        {
            "path": "packages/package-m"
        },
        {
            "path": "packages/package-n"
        },
        {
            "path": "packages/package-o"
        },
        {
            "path": "packages/package-p"
        },
        {
            "path": "packages/package-q"
        }
    ]
}
Package's File Structure
.
├── package.json
├── src
└── tsconfig.json
Package's tsconfig.json
{
    "extends": "../../tsconfig.base.json",
    "compilerOptions": {
        "rootDir": "./src",
        "outDir": "./types",
        "composite": true,
        "incremental": true,
        "lib": ["ESNext", "DOM"],
        "module": "ESNext",
        "moduleResolution": "NodeNext",
        "jsx": "preserve"
    },
    "include": ["./src/**/*"],
    "references": [
        {
            "path": "../package-a"
        },
        {
            "path": "../package-g"
        },
        {
            "path": "../package-h"
        },
        {
            "path": "../package-c"
        },
        {
            "path": "../package-d"
        }
    ]
}
Package's package.json
{
    "name": "@scope/package-f",
    "types": "./types/index.d.ts",
    "module": "./esm/index.js",
    "main": "./esm/index.js",
    "exports": {
        ".": {
            "types": "./types/index.d.ts",
            "import": "./esm/index.js",
            "default": "./esm/index.js"
        }
    },
    "dependencies": {
        "@scope/package-a": "workspace:*",
        "@scope/package-g": "workspace:*",
        "@scope/package-h": "workspace:*"
    },
    "devDependencies": {
        "@scope/package-c": "workspace:*",
        "@scope/package-d": "workspace:*"
    }
}

ISSUE DETAILS

The problematic type is declared in a certain package (package-g) and is applied to a wrapper of createUseStyles from react-jss.

createUseStyles Declaration
import {getFileCallerDetails} from '@scope/package-b';
import type {Classes, Styles} from 'jss';
import {createUseStyles as _createUseStyles} from 'react-jss';
import type {ITheme} from '../../defines/theme.types';

type TStyles<TC extends string, TProps, TTheme> =
    | Styles<TC, TProps, TTheme>
    | ((theme: TTheme) => Styles<TC, TProps>);

type TUseStyles<TC extends string, TProps, TTheme> = (
    data?: TProps & {theme?: TTheme}
) => Classes<TC>;

function createUseStyles<
    TProps = unknown,
    TC extends string = string,
    TTheme = ITheme
>(styles: TStyles<TC, TProps, TTheme>): TUseStyles<TC, TProps, TTheme> {
    if (process.env.NODE_ENV === 'development') {
        const {componentName, packageName} = getFileCallerDetails();

        return _createUseStyles(styles, {
            name: `${packageName}-${componentName}`
        });
    }

    return _createUseStyles(styles);
}

export {createUseStyles};

Now, the type works perfectly fine when importing and using the function in most of the other packages:

Screenshot 2023-06-15 at 14 15 35

But in some packages, this happens:

Screenshot 2023-06-15 at 14 14 38
  • @scope/package-g is properly declared as a dependency for all packages in the same way.
  • @scope/package-g is imported and used in the same way in all packages.
  • There are no other significative differences between working and non-working packages.
  • I suspect that this is related to the overall complexity of the type (thus some kind of limitation), because the following modification in the declaration also makes the issue go away:
type TStyles<TC extends string, TProps, TTheme> =
    // | Styles<TC, TProps, TTheme>
    | ((theme: TTheme) => Styles<TC, TProps>);

INTERESTING TESTS

1

If I move the file containing the above error from its original position, to the root of the src directory of the package, the error disappears:

Screenshot 2023-06-15 at 14 30 50 Screenshot 2023-06-15 at 14 32 18 Screenshot 2023-06-15 at 14 32 36

It only seems to work when placed just inside src, also for the other non-working packages.

2

I changed the type for breakpoint.s from number to null | boolean to make ESLint complain (@typescript-eslint/restrict-template-expressions), proving that it is correctly resolving the type. TypeScript still resolved it to any.

Screenshot 2023-06-15 at 14 50 48 Screenshot 2023-06-15 at 14 49 17 Screenshot 2023-06-15 at 14 50 33

POTENTIAL WORKAROUND

I have been trying to figure this out by myself for around 10 days, so believe me I have researched and tried a lot.

This is the farthest I got:

If I change the paths configuration in the root's tsconfig.base.json file from this:

{
    "paths": {
        "@scope/*": ["./packages/*"]
    }
}

To this:

{
    "paths": {
        "@scope/*": ["./packages/*", "./packages/*/src"]
    }
}

The issue goes away and everything seems to work perfectly. IntelliSense and overall performance also seemed to be improved.

Screenshot 2023-06-15 at 15 31 07

But, does this configuration make sense? What other issues could it provoke? The only issue I could think of is if packages are build in the wrong order (should not happen using tsc --build).

If a package gets built before one of its dependencies, a direct relative import will be generated in declaration files, which is obviously wrong. When done in the correct order, the first pattern of paths has priority (types directory can be found), and build is correct.

Wouldn't this workaround be ideal? I mean, does not it make the entire monorepo to be seen as a single source? So the IDE does not need need to build packages in the background?

In any case, right now, this workaround is triggering some errors:

  • TS6059: File '...' is not under 'rootDir' '...' .'rootDir' is expected to contain all source files.
  • TS6307: File '...' is not listed within the file list of project '...'. Projects must list all files or use an 'include' pattern. File is CommonJS module because '...' does not have field "type".

But I think those can be easily solved by changing rootDir config and having a separate TS config for building (with the correct rootDir).

POTENTIALLY RELATED ISSUES

@ernestostifano ernestostifano changed the title Types resolving to any only across some packages in monorepo using project references Types resolving to any across packages in monorepo using project references Jun 15, 2023
@gianluca932
Copy link

I've the same problem.
I tried to fix it in other ways, however I didn't find a workaround as useful as yours.

@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label Jun 22, 2023
@andrewbranch
Copy link
Member

Thanks @ernestostifano for the very detailed description. A few thoughts/questions:

  1. Would you be able to give me read access to the repo where this is happening? Ultimately, that, or an equivalent repro, might be the only way I can determine if there’s actually a TS bug and figure out a real fix.
  2. Is there a reason you don’t want to or can’t use yarn workspaces (symlinks to local packages in node_modules, or whatever equivalent Yarn PnP sets up) instead of compilerOptions.paths? Especially considering you’re using --moduleResolution nodenext and package.json exports, it’s highly recommended to set things up so that module resolution of your local packages works the way it will actually work in the runtime/bundler; i.e. through node_modules.
  3. FYI, Require module/moduleResolution to match when either is node16/nodenext #54567 made --module esnext --moduleResolution nodenext, as your packages have, an error. You probably want to switch to either just --module nodenext or --module esnext --moduleResolution bundler, depending on where your packages are getting loaded. (I doubt this will affect the bug you were observing, just an off-topic observation.)
  4. What does go-to-definition on createUseStyles do when you’re getting the error? Does it change when types are built?
  5. Are you sure disableSourceOfProjectReferenceRedirect isn’t enabled anywhere?

@andrewbranch andrewbranch added Needs More Info The issue still hasn't been fully clarified and removed Needs Investigation This issue needs a team member to investigate its status. labels Jun 29, 2023
@zb-sj
Copy link

zb-sj commented Sep 12, 2023

From lots of trial and errors with extensive searches, this config helped my problem.

  "typeAcquisition": {
    "enable": true
  }

@andrewbranch
Copy link
Member

In the interest of housekeeping, I’m going to close this since I can’t reproduce the issue and haven’t received any follow-up info I could use to try to narrow down the problem. I really appreciate the significant effort given here to provide a lot of detail in lieu of a repro, but unfortunately this kind of issue is nearly impossible to diagnose even with TS Server logs, and definitely impossible without. If anyone is able to provide a repro, please file a new issue. Thanks!

@andrewbranch andrewbranch closed this as not planned Won't fix, can't repro, duplicate, stale Nov 9, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs More Info The issue still hasn't been fully clarified
Projects
None yet
Development

No branches or pull requests

5 participants