Skip to content

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

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

Closed
valerybugakov opened this issue Sep 8, 2020 · 18 comments
Closed
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@valerybugakov
Copy link

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)

@RyanCavanaugh RyanCavanaugh added the Design Limitation Constraints of the existing architecture prevent this from being fixed label Sep 9, 2020
@RyanCavanaugh
Copy link
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.

@valerybugakov
Copy link
Author

valerybugakov commented Sep 10, 2020

@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 @sheetalkamat could suggest something regarding use of this functionality via CLI.

@RyanCavanaugh
Copy link
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.

@taylorkline
Copy link

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

@valerybugakov
Copy link
Author

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

@scottwillmoore
Copy link

scottwillmoore commented Jul 10, 2023

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.

@neongreen
Copy link

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?

@ValentinGurkov
Copy link

ValentinGurkov commented Mar 4, 2024

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.

@ValeryLosik
Copy link

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.

@natew
Copy link

natew commented Apr 23, 2024

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.

@aryzing
Copy link

aryzing commented Feb 23, 2025

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?

@aweebit
Copy link

aweebit commented Feb 24, 2025

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

@robbiespeed
Copy link

@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.

@aweebit
Copy link

aweebit commented Feb 24, 2025

@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?

@robbiespeed
Copy link

@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.

@aweebit
Copy link

aweebit commented Feb 27, 2025

@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.

@aweebit
Copy link

aweebit commented Feb 27, 2025

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:

@aweebit
Copy link

aweebit commented Feb 27, 2025

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
Development

No branches or pull requests