microsoft/TypeScript

Allow tsconfig.json when input files are specified

brapifra opened this issue Β· 48 comments

I don't know why the tsconfig.json is ignored even when the --project or -p option is specified.
In my opinion, the right implementation should be:

  • If no --project or -p option: Ignore tsconfig.json
  • Otherwise: Use the configuration file specified. All the options used in the command line should overwrite the ones of the configuration file. E.g. The include/exclude keys of the tsconfig.json will be ignored when input files are specified.

I don't know why the tsconfig.json is ignored even when the --project or -p option is specified.

Could you provide a repro for a situation where tsconfig is ignored?

Sure @andy-ms , but I mean, this is not even a bug. This behaviour is explicitly explained in the docs: When input files are specified on the command line, tsconfig.json files are ignored.

When you do tsc file.ts -p tsconfig.json you get:
captura de pantalla de 2018-09-26 22-34-25

The lines of code are pretty easy to find

I just can't understand why it's implemented this way.

Probably because no one's implemented it -- could you explain what the use cases are?

A pretty common practice is to have a pre-commit hook to compile only the staged files in your git project (It makes no sense to compile all the project).
If you try to create this hook that runs tsc over each staged file, you won't be able to use the tsconfig.json, as you are specifying some input files. @andy-ms

I have another use case for this. We have many projects that use typescript, and would like to reduce boilerplate and keep our typescript config in one place, so it can be uniformly applied across all our projects for consistency.

At the moment, there isn't a way to do this, since you can't specify both a glob of files to check, and an external path to a tsconfig.json file. The only workaround in the meantime is to copy/paste a tsconfig.json file into the root of every project and have it only contain an "extends" statement. This isn't the end of the world, but it's more boilerplate than we want - the fewer duplicate files across multiple projects we can have, the better.

This same goal is easily accomplished with TSLint, by using their --config option as well as a glob of files to lint. Is there any specific reason why it is disallowed by typescript itself? If not, I would be happy to take a stab at implementing it via pull request, just want to make sure its ok with maintainers.

related, but never considered #12958

I have a different use case that needs this as well. We have a pre-commit hook that does a quick type check (tsc -p tsconfig.json --noEmit). Currently it checks all the files specified in tsconfig.json, but it would be nice to only type check the files that were modified. We are currently using lint-staged to run linting tools on modified files, it would be great to only run the type check on the modified files too.

Like @brapifra said here, #27379 (comment), it's written in the docs that this should be possible, but it clearly isn't.

We have some questions for everyone in this thread

Long story short, the real problem we see is that people make a tsconfig.json, then run

tsc someFile.ts

and get garbage confusing errors because e.g. their tsconfig specifies target: ESNext and the error says something like "This is an ES5+ only API, change your target!". So we'd like to change that so that tsc always picks up a nearby tsconfig file, and provide an opt-out like --ignore-tsconfig for people who want to ignore it intentionally.

However, we're not sure what tsc someFile.ts means if there's a tsconfig present:

Question 1, do we:

  • Overwrite the files setting
  • Append to the files setting
  • Prepend to the files setting (order rarely matters but sometimes does...)

Question 2:

  • Are include and exclude still in play if there are some files specified on the commandline?

Question 3:

  • What if someFile.ts was in the exclude list?

Thoughts?

Just my two cents:

A pretty common practice is to have a pre-commit hook to compile only the staged files in your git project (It makes no sense to compile all the project).

This use case seems problematic in general, since (AFAIK) TS needs to see your whole project for some things to work (e.g. const enum, declaration merging), unless you use isolatedModules which disables those features.

Question 1.

I would expect that we'll "Overwrite the files setting".

Question 3.

If we'll treat someFile.ts as file to override files list, then no problem here - from the docs:

Files included using "include" can be filtered using the "exclude" property. However, files included explicitly using the "files" property are always included regardless of "exclude".

Question 2.

IMO include should be ignored in this case.

I think there is cases when some modules pollute global scope and it might break the compilation (because not all modules require that modules with globals even if they use them), but I believe developers should import deps explicitly (via triple-slash directives or even import statements in this case).

So we'd like to change that so that tsc always picks up a nearby tsconfig file, and provide an opt-out like --ignore-tsconfig for people who want to ignore it intentionally.

@RyanCavanaugh So, we will not be able to pass path to the custom tsconfig via --project CLI option, right? I believe it's a use case as well and it would be good to cover it too.

mmkal commented

This can be hackily worked-around with jq (YMMV: probably lots of edge cases this won't work for, *sh only, and at this point you might prefer to write a wrapper js file which uses child_process):

tsc $(cat tsconfig.json | jq -r '.compilerOptions | to_entries[] | "--\(.key) \(.value)"') path/to/dir/**/*.ts

At very least we should have a way to tell tsc to use certain tsconfig.json, so I think -P, --project argument should work either way. And I agree with @timocov, overriding files with input should put the whole thing straight.

alloy commented

We have some questions for everyone in this thread

@RyanCavanaugh For my case, which is to focus on sub-sections of a large code-base going through a large migration that yields many errors all over the place, I would be fine if tsc would simply use tsconfig.json as normal for its work but only print errors that occur in the files I’ve specified. I.e. it being simply a filter.

Perhaps it would be more explicit to have an option for that, but I don’t feel strongly about it.

$ tsc --pretty --show-errors-from=./src/Apps/WorksForYou/{**/,}*.{ts,tsx}

Hello. I too am on the same lint-staged / lint-prepush boat where I would like to have a git hook verify that a list of files pass the typecheck settings specified in tsconfig's compilerOptions.

Question 1, do we:

  • Overwrite the files setting
  • Append to the files setting
  • Prepend to the files setting (order rarely matters but sometimes does...)

I expect usage like tsc -p ./tsconfig.json myfiles.ts, for myfiles.ts to overwrite/replace the files setting. I would just like to "steal" the 20+ compilerOptions from tsconfig.json and get myfiles.ts typechecked with these compilerOptions instead of having to re-construct them in the CLI.

Thank you.

Any update on this? As it stands using tsc on a precommit hook isn't very viable for large projects.

The documentation mentions this scenario:

Transpile any .ts files in the folder src, with the compiler settings from tsconfig.json
tsc --project tsconfig.json src/*.ts

It does not seem to work. Should documentation be updated to remove the misleading example?

I'm also in the camp that wants to run the compiler in a pre-commit hook.

Inspired by @mmkal's suggestion, I put together this simple node script: https://gist.github.com/dsernst/b1d2df3bb5be777e1dcb27e5c0d2474d#file-typecheck-js-L13

Example w/ error:

Screen Shot 2020-02-04 at 4 01 37 AM

Otherwise:

Screen Shot 2020-02-04 at 3 51 38 AM

Works great for my use-case. Hope others may find useful. πŸ‘

@JasonKaz, @brapifra try to declare external lint-staged config like here. Option -p tsconfig.json can be omitted for root tsconfig.

Then reinstall husky and lint-staged to avoid mistakes.

dandv commented

Just wanted to throw another perspective in: a colleague new to TypeScript expected tsconfig.json to govern the transpilation of individual files as well, just as eslint reads .eslintrc when it's passed only one file to lint.

Hey, I've built https://npm.im/tsc-files which is a tiny wrapper on tsc to handle my need to type-check specific files with lint-staged. Hope it helps some of you.

I'm afraid my solution (https://npm.im/tsc-files) will miss type errors on other files. 😞

For example, suppose changing foo.ts will break bar.ts. If you use lint-staged, only foo.ts will be type-checked (because it's the only staged file) and the type error that appeared at bar.ts won't be caught. If anyone knows how to fix that, please send me a PR.

There are explicit examples in https://www.typescriptlang.org/docs/handbook/compiler-options.html which show usage of --project and input files together... tsc --project tsconfig.json src/*.ts

@willstott101 Yes, it was mentioned above, but it doesn't seem to work. In fact, the new version of the documentation doesn't have that one example anymore: https://www.typescriptlang.org/v2/docs/handbook/compiler-options.html

msbit commented

Adding to the responses to the questions:

Question 1

  • Overwrite the files setting

Question 2

  • Ignore the include and exclude settings if files are specified on the commandline

Question 3

  • Not applicable due to answer to Question 2

We would find value in this for writing an arcanist tsc linter.

Please find below the use-case and workaround in case you need it

The way arcanist external linters work is by specifying an includes regex and then running your linter on every file that matches it. Tried pretty much everything, unfortunately, right now the only way to make tsc <file> type-checking work and handle tsconfig.json seems to write a runner (a.k.a tsc spawner). It works by:

  • specifying it to arcanist as the only file to type check in .arclint
  • implementing an ArcanistExternalLinter whose sole responsibility is to call node runner.js
  • runner will be called back as node <runnerPath/runner.js> <projectRoot> <runnerPath/runner.js>
  • on runner.js, parse args and execute a spawn of tsc from project root, to type the whole codebase

This way, tsc is called once, type-checks the whole codebase and handles tsconfig.json as it should.

.arclint

{
  "linters": {
    "tsc": {
      "type": "tsc",
      // will call back linter just once, because we include just one file
      // it could be any existing file in filesystem, it doesn't matter
      "include": "/^linters\\/arc-tsclint\\/lint\\/linter\\/runner\\.js$/"
    }
  }
}

linters/tsc/lint/linter/runner.js

function main() {
  // We don't end up using currentFile, refactor when 27379 is fixed
  const [, , projectRoot, currentFile] = process.argv;

  const { stderr, stdout, status } = spawnSync("tsc", ["--noEmit", "--jsx", "react"], {
    cwd: projectRoot,
  });
  // Do arcanist-related stuff with stderr stdout and status
}

Overall a similar use-case as the one @brapifra noted, and we would like not to be reliant on such a hack.

Envek commented

One more use-case is serverless Lambda functions.

It is convenient to have many related Lambda functions together in one project with common tsconfig.js and common reused utility code reused by some but not all functions. Given that every Lambda function has only one entry point – its handler – we can compile only this handler file and will get only files required to run this given Lambda function.

This not only allows to put less files into function's code, but (which is more important) to update only those functions that really uses changed code.

Example

Given following project structure:

── src
    β”œβ”€β”€ handlers
    β”‚   β”œβ”€β”€ a.ts
    β”‚   β”œβ”€β”€ b.ts
    β”‚   └── c.ts
    └── utils
        β”œβ”€β”€ ab.js
        └── bc.js

By executing following comands:

tsc --project . --outDir .aws-sam/build/AFunction src/handlers/a.ts
tsc --project . --outDir .aws-sam/build/BFunction src/handlers/b.ts
tsc --project . --outDir .aws-sam/build/CFunction src/handlers/c.ts

I want to get following transpiled results that uses settings from tsconfig.json:

.aws-sam/build/AFunction
└── src
    β”œβ”€β”€ handlers
    β”‚   └── a.js
    └── utils
        └── ac.js
.aws-sam/build/AFunction
└── src
    β”œβ”€β”€ handlers
    β”‚   └── b.js
    └── utils
        β”œβ”€β”€ ab.js
        └── bc.js
.aws-sam/build/CFunction
└── src
    β”œβ”€β”€ handlers
    β”‚   └── c.js
    └── utils
        └── bc.js

Workaround

Generate one-off child tsconfig files that overwrites includes property as described at https://stackoverflow.com/a/44748041

echo "{\"extends\": \"./tsconfig.json\", \"include\": [\"src/handlers/a.ts\"] }" > tsconfig-only-handler-a.json
tsc --build tsconfig-only-handler-a.json

As https://github.com/AkhmadBabaev mentioned here #27379 (comment)

This worked for my project:

  • tsconfig.json in project root
  • use lint-staged.config.js
  • omit -p tsconfig.json from tsc command
// lint-staged.config.js
module.exports = {
  "*.{js,ts,tsx}": "eslint --cache --fix",
  "**/*.ts?(x)": () => "tsc --noEmit",
};

I end up with esbuild so far.

npx esbuild ./config/*config.ts  \
    --tsconfig=./config/tsconfig.esm.json \
    --outdir=./config-out \
    --format=esm \
    --out-extension:.js=.mjs \
    --watch

My use-case is that we have converted a messy JS codebase to TS, so there are thousands of type errors. We want to go through and clean them up gradually, over time.

This means that we can't fail the build in CI for errors running tsc for the whole project.

It also means that running tsc --watch to diagnose and fix errors is pretty useless, you have to scroll through a bunch of errors unrelated to what you're working on.

I would love to be able to run a command like tsc --focus foo.ts bar.ts which will only show errors that occur in foo.ts or bar.ts (not even related errors in other files) – literally just filter the output of tsc by filepath.

My use-case has to do with Jest tests or npm run utility scripts invoked using ts-node. In my tsconfig.json I have some non-default compiler options set (e.g., "strict": true, "target": "ES2021", etc.), as well as the use of a dist output directory for the compiled JavaScript. When Jest tests or ts-node runs a single .ts file, it compiles with different options (i.e., the default options) than I have specified, and, worse, pollutes my src/** directory with a bunch of individual .js files (since, by default, the TypeScript compiler outputs the .js alongside the .ts source).

In my opinion, if a project takes the effort to define a project-wide tsconfig.json, those settings should be used whenever tsc is invoked. Command-line options to tsc at that point can/should override any conflicting setting in tsconfig.json.

This issue has hundreds of πŸ‘ responses. In #27379 (comment) @RyanCavanaugh asked important questions about how it should work. In #27379 (comment) @timocov answered those questions, ("overwrite files setting") and got 9 πŸ‘ emojis and no negative responses. There was another response #27379 (comment) from @moopmonster which gave the exact same answer, and got 24 πŸ‘ emojis.

@RyanCavanaugh's question appears to be settled.

I speculate that a PR would be welcome that simply implemented the "overwrite files" approach.

It will be really nice if TypeScript team puts time on this issue.
I've read TypeScript's tsconfig.json behavior, it says TypeScript tries to seek every relative file that imports "ns" if you import "ns" as below.

import * ns from "mod"

And if the TS compiler finds files which import "ns", it automatically includes them in its compiling even though you don't want them to be type-checked. So to me, it sounds like TS will eventually look at extra files which might not be the ones you modified.

We want the option to tell the compiler to check the specific files, about TS grammar only, Not checking every referencing file! 😱

@moonformeli

We want the option to tell the compiler to check the specific files

This exists and is called noResolve

i run this code for my precommit hook and i am facing the same issue

const childProcess = require('child_process');
const util = require('util');

const exec = util.promisify(childProcess.exec);

const getTSConfigPath = require('../utils/getTSConfigPath');

const run = async () => {
  let isIncludeFiles = false;

  const files = process.argv.reduce((result, arg) => {
    if (isIncludeFiles) {
      result.push(arg);
      return result;
    }
    if (arg === 'files') {
      isIncludeFiles = true;
    }
    return result;
  }, []);

  const queue = files.map((file) => {
    const tsconfigPath = getTSConfigPath(file);
    return () => exec(`tsc ${file} --project ${tsconfigPath} --noEmit`);
  });

  let error = false;

  const runQueue = (index) =>
    queue[index]()
      .then(() => runQueue(index + 1))
      .catch((err) => {
        error = err;
      });

  await runQueue(0);

  if (error) {
    throw error;
  }
};

run();

managed to solve the problem:

const childProcess = require('child_process');
const fs = require('fs');
const path = require('path');
const util = require('util');

const exec = util.promisify(childProcess.exec);

const getPackagePath = require('../utils/getPackagePath');

const run = async () => {
  let isIncludeFiles = false;

  const files = process.argv.reduce((result, arg) => {
    if (isIncludeFiles) {
      result.push(arg);
      return result;
    }
    if (arg === 'files') {
      isIncludeFiles = true;
    }
    return result;
  }, []);

  const queue = files.map((file) => async () => {
    const packagePath = getPackagePath(file);
    const tsconfigPath = path.resolve(packagePath, 'tsconfig.json');
    const tsconfigData = fs.readFileSync(tsconfigPath, 'utf-8');
    fs.writeFileSync(tsconfigPath, tsconfigData.replace(/"include": \[[\W\w]*\]/, `"include": ["${file}"]`));
    try {
      await exec(`cd ${packagePath} && yarn run tslint`);
      // eslint-disable-next-line no-useless-catch
    } catch (error) {
      throw new Error(error.stdout);
    } finally {
      fs.writeFileSync(tsconfigPath, tsconfigData, 'utf-8');
    }
    return true;
  });

  const runQueue = (index) => queue[index]().then(() => runQueue(index + 1));

  await runQueue(0);
};

run();

@basketball7hero the tsconfig include trick is neat, but I suggest to create temporary tsconfig file, rather overwriting project existing config and restoring it later.

Our ideal use case would be to use tsc for fast type checking only for modified files (lint-staged?) and the related one (like jest --findRelatedTests flag), with the same logic as ESLint which takes into account the closest config file based on the file to check so that it can also be used in a monorepo project structure (we use Nx)

Our ideal use case would be to use tsc for fast type checking only for modified files (lint-staged?)

Yes please! πŸ™‡β€β™‚οΈ πŸ™

My use case for this would be as an alternative to ts-jest, since ts-jest runs slower than things like babel-jest and swc/jest

How does VS Code generate red underline for TypeScript issues on individual files?

How does VS Code generate red underline for TypeScript issues on individual files?

I think it doesn't compile files individually. You just see those lines after Typescript has processed the entire codebase

any update here?

Same as others, this breaks my lint-staged configuration and took me hours to find out why.

I understand the ambiguity of tsc someFile.ts with a tsconfig file, i would maybe add just a flag for the most common use case which seems to be: run it as you run the folder and it was just an individual file)

Also two less ambiguous options:
A) a -skipConfig or -skipProject or -force could force it against the tsconfig
B) a flag to use the tsconfig on purpose, like just `tsc someFile.ts ../..tsconfig.json

I would probably do B so it would cause no regressions. Also that's the hallucinated solution that ChatGPT gives when asking why the tsconfig is not working and how to force it.

Btw this is a cool workaround: https://github.com/gustavopch/tsc-files

Hi everyone,

I’ve encountered the same issue discussed here and wanted to share a solution I’ve been working on. I created a project called tscw-config that addresses this problem. It allows users to run tsc with both files and tsconfig.json at the same time:

  • It provides CLI and API that you can use.
  • It seamlessly integrates with most popular package managers, including:
    • npm
    • pnpm
    • Yarn
    • Yarn (Plug’n’Play)
  • It is well tested.

Feel free to check it out and let me know if you have any feedback or suggestions!