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:
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
andexclude
still in play if there are some files specified on the commandline?
Question 3:
- What if
someFile.ts
was in theexclude
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.
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.
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:
Otherwise:
Works great for my use-case. Hope others may find useful. π
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
Adding to the responses to the questions:
Question 1
- Overwrite the
files
setting
Question 2
- Ignore the
include
andexclude
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 callnode 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 oftsc
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.
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
fromtsc
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! π±
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!