Skip to content

Project-references type check with --noEmit fails without built files #40431

Description

TypeScript Version: ^4.0.2

Search Terms: "has not been built from source file", "project-references", "typecheck without emit"

Description

In monorepo with project-references it's not possible to use tsc for type check only without emitting files.
Whereas calling tsc -b works without any issues. But there's no point in emitting files if we only need to perform type check of all related modules.

It seems that is should be possible because of: #32028

Expected behavior:

tsc with --noEmit builds all referenced projects without emitting anything:

tsc --noEmit -p ./zoo

Found 0 errors.

Actual behavior:

It fails because no built files are found for referenced projects:

tsc --noEmit -p ./zoo

zoo/zoo.ts:1:32 - error TS6305: Output file '/Users/val/Desktop/patch/project-references-demo/lib/core/index.d.ts' has not been built from source file '/Users/val/Desktop/patch/project-references-demo/core/index.ts'.

> import { makeRandomName } from "@p/core";

Playground Link:

repro repo

git clone https://github.com/valerybugakov/project-references-demo

cd ./project-references-demo
npm i
npm run typecheck

Related Issues:
#25613
#25600
#30661 (comment)

Activity

  1. RyanCavanaugh commented on Sep 9, 2020

    @RyanCavanaugh
    Member

    A .d.ts can be subtly different from the .ts that produced it, so you might not get identical results between a "virtual" build like this and a proper one, so enabling this is a dangerous thing to do. We could produce the .d.ts files in-memory in a serial build, but that's just build mode, and you're not invoking build mode so we can't legally go and inspect the referenced project. I'm not sure what we could do here; this was one of the listed caveats of project references from the get-go.

  2. valerybugakov commented on Sep 10, 2020

    @valerybugakov
    Author

    Ryan Cavanaugh (@RyanCavanaugh) Thanks for the response. This functionality looks like what I want to achieve using --noEmit flag from the CLI. It doesn't use .d.ts files and relies on "virtual" build to provide better DX with type-checking by inspecting source files of the referenced projects.

    If it's true, it means that the mechanism is already in place. It would be great to expose some flags for developers to use it. Please correct me if I confuse two unrelated features. Maybe Sheetal Nandi (@sheetalkamat) could suggest something regarding use of this functionality via CLI.

  3. RyanCavanaugh commented on Sep 10, 2020

    @RyanCavanaugh
    Member

    That PR is implemented by presenting the type checker with a virtual file system that builds .d.ts files on the fly; we can't enable it with a flag from tsc because it uses code specific to the language service process.

  4. taylorkline commented on May 20, 2022

    @taylorkline

    Valery Bugakov (@valerybugakov) Did you ever find a way to solve the problem you've described here?

  5. valerybugakov commented on Jul 18, 2022

    @valerybugakov
    Author

    Taylor Kline (@taylorkline), not the root cause. We partially solved the performance issue in our project by caching type check commands per Typescript project with nx.

  6. scottwillmoore commented on Jul 10, 2023

    @scottwillmoore

    I think this issue should be reconsidered, and opened again.

    It appears that not only do we get TS6305 errors, any files in project references do not appear to be type checked. I could be wrong, and I will create a reproduction in the future.

    Project references are used in the Vite template for React and TypeScript which is very popular.

    Any execution of tsc to check the types of a project in these cases would dangerous, as while Visual Studio Code will check the types a give a false sense of safety any CI/CD or tool such as Vitest will silently fail.

    In addition, it is not efficient to run tsc --build, where many of these build with their own bunder.

    EDIT: Here is a reproduction of this issue which I created for the Vitest team.

  7. neongreen commented on Jan 14, 2024

    @neongreen

    That PR is implemented by presenting the type checker with a virtual file system that builds .d.ts files on the fly; we can't enable it with a flag from tsc because it uses code specific to the language service process.

    We could produce the .d.ts files in-memory in a serial build, but that's just build mode, and you're not invoking build mode so we can't legally go and inspect the referenced project.

    I'm confused — so is this a technical limitation (as the first quote implies), or a design/correctness limitation?

  8. ValentinGurkov commented on Mar 4, 2024

    @ValentinGurkov

    I hope this topic gets revisited in the future. I just ran into this when I explored the idea of using project references to speed up type checking (via --noEmit). I have also opted for caching results via nx for the time being.

  9. ValeryLosik commented on Mar 4, 2024

    @ValeryLosik

    The thing is that project references put on the table some unique functionality like project boundaries, which can be extra useful in monorepo's workflow,however, it currently overlooks its potential due to the necessity of building files, leading to a performance degradation when compared to the --noEmit check.

    In the realm of modern monorepo development, there's a shift towards employing rapid compilers like SWC and esbuild with extracting type checking into a separate task. In such approaches, emitting any files through TypeScript (tsc) becomes unnecessary, further enhancing overall efficiency. I would really like to see someday this feature in your roadmap.

  10. natew commented on Apr 23, 2024

    @natew

    Run into this many times, I believe it should be fixed. There should be no reason to need to emit declaration files if you are just trying to type check and have noEmit set, and the errors are quite confusing.

  11. aryzing commented on Feb 23, 2025

    @aryzing

    This is super annoying!

    Without using extra tooling, resorting to

    tsc -b || tsc -b --clean

    does the job of type-checking without leaving extra files behind.

    Type checking and building are treated as separate tasks by virtually every project, tsc would better serve users by supporting this flow for project references too.

    How does VS Code do it? It shows type errors without adding files to the project file tree. Can't that functionality be ported over to tsc?

  12. aweebit commented on Feb 24, 2025

    @aweebit

    The whole point of project references is1 to let one project consume another project's declarations, which is why generating .d.ts files for referenced projects is a requirement.

    The requirement is enforced by the fact that noEmit cannot be enabled and composite must be enabled for referenced projects1. Enabling composite in turn automatically enables declaration, and you are not allowed to disable it.

    You may, however, set emitDeclarationOnly to true to prevent .js files from being emitted, and you may set outDir or declarationDir to some directory ignored by version control so as not to have to run tsc -b --clean after each build if you don't want your project folders cluttered with .d.ts files. A good example for such a directory is node_modules/.tmp used by Vite's official templates as the output directory for .tsbuildinfo files generated during tsc -b runs.

    I am surprised this solution still hasn't been mentioned in this thread. As the TypeScript team does not seem to be willing to implement on-the-fly .d.ts file generation in a virtual file system for type checking purposes, I think it is the best one we have at the moment.

    Footnotes

    1. unless used in a “solution” tsconfig.json where files is set to an empty array, in which case project references are just a mechanism for combining several projects into one ↩ ↩2

  13. robbiespeed commented on Feb 24, 2025

    @robbiespeed

    aweebit outputting .d.ts leads to issues if files are moved, renamed, or deleted between builds. The old .d.ts files remain and result in undetectable errors, like importing from files that should no longer exist.

  14. aweebit commented on Feb 24, 2025

    @aweebit

    @aweebit outputting .d.ts leads to issues if files are moved, renamed, or deleted between builds. The old .d.ts files remain and result in undetectable errors, like importing from files that should no longer exist.

    mkdir leftover-declarations
    cd leftover-declarations
    npm init -y
    npm pkg set type="module"
    npm i -D typescript@~5.7.3
    mkdir referenced
    // tsconfig.referenced.json
    {
      "include": ["referenced"],
      "compilerOptions": {
        "outDir": "node_modules/.tmp",
        "composite": true,
        "emitDeclarationOnly": true
      }
    }
    // tsconfig.json
    {
      "exclude": ["referenced"],
      "compilerOptions": {
        "outDir": "node_modules/.tmp",
        "noEmit": true
      },
      "references": [
        { "path": "./tsconfig.referenced.json" }
      ]
    }
    echo "export default 42" > referenced/original.ts
    echo "import value from './referenced/original'" > index.ts
    npx tsc -b
    mv referenced/original.ts referenced/renamed.ts
    npx tsc -b

    This results in

    index.ts:1:19 - error TS2307: Cannot find module './referenced/original' or its corresponding type declarations.
    

    despite node_modules/.tmp/referenced/original.d.ts still existing.

    So I cannot seem to reproduce the issue you are describing. Could you give a concrete example where leftover declaration files actually lead to problems?

  15. robbiespeed commented on Feb 24, 2025

    @robbiespeed

    aweebit I mostly only uses project references in monorepo setups which is where I see the problem occur often.

    Minimal repro on StackBlitz

    Run pnpm build then rename packages/a/foo.ts, the import in packages/b/bar.ts will not error in the editor nor will it error if build is run a second time. After running pnpm build:clean then build will correctly fail because it removes the old .d.ts file.

  16. aweebit commented on Feb 27, 2025

    @aweebit

    Robbie Speed (@robbiespeed)

    A solution that turned out to be wrong:

    since you are using pnpm anyway, I suggest you try the "publishConfig" approach described in this article: https://colinhacks.com/essays/live-types-typescript-monorepo. I tried it in your StackBlitz demo and it worked like a charm.

    Update: I've just realized I only thought it worked because I left no exports other than "./*.js": "./src/*.ts" in the actual "exports" field (not the one in "publishConfig"), so the foo.ts source file was the only thing TypeScript could see, and so it reported an error if it couldn't be found. The problem is that Node now also only sees the source file when trying to import "foo.js", which results in a runtime error!


    The rest of the original comment:

    By the way, the article also mentions that you should always put your custom export conditions such as the "@ts-ref/source" condition you use in the demo before all other conditions, because the order matters. This doesn't help in our case with file renaming though since TypeScript falls back to the types condition when it doesn't find foo.ts, and so it still discovers foo.d.ts in the output directory. One possible solution to this would be if TypeScript introduced am option like noCustomConditionFallback that would cause the compiler to only resolve imports using custom conditions if at least one of those specified in customConditions is also specified in package.json for the module being imported.

    But it would be even better if there was an option to clean leftover files from the output directory during rebuilds. By that I don't mean something that would have the same effect as tsc -b --clean && tsc -b, but rather a smart process recognizing and deleting only output files that no longer have a corresponding source file. This way, no performance compromise would have to be made as incremental builds would still be possible.

  17. aweebit commented on Feb 27, 2025

    @aweebit

    But it would be even better if there was an option to clean leftover files from the output directory during rebuilds. By that I don't mean something that would have the same effect as tsc -b --clean && tsc -b, but rather a smart process recognizing and deleting only output files that no longer have a corresponding source file. This way, no performance compromise would have to be made as incremental builds would still be possible.

    It turns out there has been an open issue with a similar feature request for almost 8 years now:

    And I've also found an open issue requesting to support tsc --build --noEmit:

  18. aweebit commented on Feb 27, 2025

    @aweebit

    For those who received email notifications with my previous comments in their original form: the solution with pnpm's "publishConfig" turned out to be wrong! See the updated comment.

    I've just found a different solution on Stack Overflow: https://stackoverflow.com/a/79144361/9861000

    It involves an external dependency though, namely Google's Wireit tool. Its About section says:

    Wireit upgrades your npm/pnpm/yarn scripts to make them smarter and more efficient.

    The docs even have a dedicated TypeScript recipe:

    {
      "scripts": {
        "ts": "wireit"
      },
      "wireit": {
        "ts": {
          "command": "tsc --build --pretty",
          "clean": "if-file-deleted",
          "files": ["src/**/*.ts", "tsconfig.json"],
          "output": ["lib/**", ".tsbuildinfo"]
        }
      }
    }
    • Set "incremental": true and use --build to enable incremental compilation, which significantly improves performance.
    • Include .tsbuildinfo in output so that it is reset on clean builds. Otherwise tsc will get out of sync and produce incorrect output.
    • Set "clean": "if-file-deleted" so that you get fast incremental compilation when sources are changed/added, but also stale outputs are cleaned up when a source is deleted (tsc does not clean up stale outputs by itself).
    • Include tsconfig.json in files so that changing your configuration re-runs tsc.
    • Use --pretty to get colorful output despite not being attached to a TTY.

    I haven't tried it myself, but looks like exactly what I was looking for. Should be a viable solution for monorepos, although of course it is not cool that a third-party tool is required for something like that.

    In the answer to the Stack Overflow question, I also found another relevant issue from 2020:

    Unfortunately, TypeScript team's stance seems to be that the limitation is by design, so for now, it seems like Wireit running in watch mode during development is as good a solution as it gets.

  19. dbrxnds commented on Apr 29, 2025

    @dbrxnds

    Thanks aweebit, the wireit solution looks great. Do you happen to have a monorepo example that uses that setup? I imagine I would have to set up wireit for each internal package then or?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Design LimitationConstraints of the existing architecture prevent this from being fixed

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions