TypeScript
TypeScript copied to clipboard
Feature request: allow user to merge extended arrays in tsconfig files
Scenario: As a user, I would like to optionally merge extended arrays in tsconfig files. To do so, I would add a nested dot array ["..."] reminding spread operator to the property I want to merge. Here is an example:
tsconfig-base.json
{
"exclude": ["**/__specs__/*"]
}
tsconfig-custom.json
{
"extends": "./tsconfig-base.json",
"exclude": [["...tsconfig-base"], "lib"] // resolved to ["**/__specs__/*"; "lib"]
}
Alternative: using a config {} object
tsconfig-custom.json
{
"extends": "./tsconfig-base.json",
"exclude": [{ "extends": "tsconfig-base" }, "lib"] // resolved to ["**/__specs__/*"; "lib"]
}
Personally, I'd sooner like to see a tsconfig.js or tsconfig.ts affordance (similar to a webpack config) to generally leverage JS syntax for complex configuration merging rather than introduce special-snowflake "syntax" into a json file with complex layering and execution semantics. There's already a thing that exists to describe these kinds of procedural transforms, and it's called "code".
Personally, I'd sooner like to see a tsconfig.js or tsconfig.ts affordance (similar to a webpack config) to generally leverage JS syntax for complex configuration merging rather than introduce special-snowflake "syntax" into a json file with complex layering and execution semantics. There's already a thing that exists to describe these kinds of procedural transforms, and it's called "code".
Noooooooooooooooo.
We did talk about the OP when we first put in the configuration inheritance in place, and had a proposal of extends and overwrites to be two different behaviors for cases like these.. but in the end we chose to avoid complexity and just go with extends meaning overwrite. i think in hindsight that was a good discussion, and most users did not have the need for additional complexity. i would say for this request as well, the additional complexity (both in supporting the feature, and for users tsconfig.json files) is not worth the value you get out of it.
"Do not over-complicate the implementation with abstractions where a little copying will suffice" - Unattributed programming koan
I understand the points you all made - especially the programming koan -, and I am only a novice. However you might be interested in the use-case : a project with many npm libraries acting as program modules, orchestrated with yarn workspaces and lerna. In such a situation, I find configuration reusability extended to the feature I proposed very convenient and clean.
As a side note, I find in some of your comments sarcasm I was absolutely not expecting from typescript repo maintainers. You don't need to scorn at people to make your point.
Sorry if I came off a bit sarcastic (I suppose quotes around things in text imply sarcastic airquotes). I probably should have used italics to imply emphasis and emphaticness rather than quotes in my first comment. I do understand the desire for reusable configuration, I just want the conversation and discussion on the issue to remain a bit light-hearted (and attempted to set such a tone) as once you start talking about code as configuration like I was, in my experience people start having very strong opinions (both for and against). I mean no slight to either your suggestion or you personally.
@weswigham I greatly appreciate this clarification :-) But just to understand your viewpoint, would you really rather have a tsconfig.js ? Or, at the same time you consider code more appropriate and a javascript config file off-putting as too webpackish (the team reaction to your post is confusing) ?
I, personally, would prefer a code file. @mhegazy disagrees. :) As I said, strong opinions.
@weswigham OK, then I'm totally with you on this. Either through extended syntax or "codable" config file, I would love the feature.
Code file is not statically alayzable. We have a whole set of tools that rely on this config file to drive user experiences. Code file is a non starter.
As I noted earlier, we have discussed such configuration inheritance use cases when we were implementing the feature; as a matter of fact @weswigham when he first propsed it had an extends and overrides, he also had multiple inheritance support. Back then we chose to not complicate the feature and ended up pulling the plug on these two proposals.
After having this in use for a few years now, I do not see that as a bad choice, and I do not think there are new use cases that are blocked by that decision.
And yes, it would be nice if you can configure every possible inheritance model you can think of; but features come with a cost, both for the compiler and toolset maintainers and for new users. There is always a trade of.
@mhegazy Thank you for both your posts which were very insightful. This is only speculation, but perhaps the shift towards monorepos (here is typescript workspaces plugin) will make the need for high config reusability more widespread.
We are always open to revisiting requests.
Code file is not statically alayzable.
Don't we have TypeScript for that? :D Thanks to allowJs I type check my Jest and Webpack configs like this https://twitter.com/PipoPeperoni/status/1016199550330195971 and to get features like code completion and so on. Are there more things left where .json is a strong requirement?
By the way I wonder what is the correct way to "read" an extended config? Is this the "shortest" way?:
import { parseJsonConfigFileContent, readConfigFile, sys } from 'typescript';
const path = './foo/tsconfig.json';
const { config } = readConfigFile(path, sys.readFile);
const host = {
useCaseSensitiveFileNames: false,
readDirectory: sys.readDirectory,
fileExists: sys.fileExists,
readFile: sys.readFile
};
const { options } = parseJsonConfigFileContent(
config,
host,
basename(path)
);
console.log('Parsed (!) CompilerOptions:', options);
+1 for merge
I use nrwl/nx to manage my monorepo. Files structure looks like this:
├── apps
│ ├── simple-app
│ │ └── tsconfig.json
│ └── complex-app
│ └── tsconfig.json
├── libs
│ └── shared-lib
│ └── tsconfig.json
├── tsconfig.json
└── package.json
All apps/libs tsconfigs extend root tsconfig ("extends": "../../tsconfig.json").
In order for apps to import shared-lib NX adds a paths to root config:
"baseUrl": ".",
"paths": {
"@product/shared-lib": ["libs/shared-lib/src/index.ts"]
}
It works great! But a complex-app is really complex, so I want to add additional alias to the root of the app:
// in complex-app/tsconfig.json
"baseUrl": "src",
"paths": {
"@@/*": ["./*"]
}
And I've just lost an @product/shared-lib alias from the root config. I can add it manually. But it would be great if I could somehow merge paths.
@zaverden What about using the xplat architecture? Would not that be sufficient to tackle your use-case? After all, if you want to refactor modules, you simply have to rename your module and all the shared modules between your apps, so it's one additional file change per app, which IMO is a very scalable solution, but there might be a better approach. What does @weswigham think?
@diegovincent I don't think I understand how xplat can help to address the issue.
I have custom path aliases defined in complex-app/tsconfig.json. Now if I want to use some lib in complex-app I have to redefine a path alias to the lib, though the lib path alias is already defined in the root tsconfig.json.
So I don't understand how xplat can help me to get rid of custom path aliases on application level, when whole team use them a lot and think they are very convenient.
@zaverden I mean their architecture just makes yourself wonder if you truly need what you are asking for. A lot of the times we all stay too long wondering if we can do something, but not if we should do it.
I was just thinking, maybe all your features can probably be refactored outside of your app, or, in the case they are extremely app-specific, they can simply be refactored into Angular modules, hence you only import stuff once, then moving stuff around is just a matter of moving the module and its import. If the module is already inside your complex-app folder, then the nesting will not be something like ../../../..., but instead something like ./feature-a/feature-a.module or ./feature-a.module which is already really clean; that, in the worst case scenario. Best case scenario, you can refactor your functionality outside of the scope of your app, maybe you have some backend utilities, date parsing libraries, translate modules, and core logic of your company which is not precisely app-specific. So, after you refactor everything to libraries outside your complex-app folder, you can simply import it using:
import { MyModule } from '@my-org/libs/core/my-business-domain'
Which already solves your problem. My only concern is, can the experts at Microsoft or Angular or Nx or Xplat assure that it is indeed a best practice to create additional paths in nested tsconfig files, or this is just a sympton of a (mono)repo that needs refactoring?
Those are my 2 cents.
@diegovincent thank you for clarification. Indeed you are right, these custom aliases are signals that something went wrong. Unfortunately, my team did not understand these signals on earlier stages of a project.
Now we develop new app with xplat-like approach (we defined our own categories and templates). But there are dozens of old-style active apps. We cannot refactor them all in one day.
@zaverden
My recommendation is that you do not try to "over-complicate" the tsconfig file in your monorepo(s). Simply, refactor mercilessly but little by little. If you have a C-level rank at your company, or you are a Tech Lead, I highly recommend that you deeply read Clean Architecture book and the Extreme Programming website—although you certainly do not have to ingrain all the rules in your company by any means, just "steal" their best practices for your team.
Good luck, I am going to be active in this kind of topics during this year, feel free to contact me!
D.
I just tried to add typeorm shim to my frontend to share entities using paths in the frontend tsconfig which extends my main monorepo tsconfig.
Is there a work around or I'll have to copy my main tsconfig path to the frontend tsconfig?
I have something I could use this for!
I mix together node and browser code in the same modules often. I write a lot of web scraping stuff, and I might launch puppeteer and send over some browser code etc.
And using node 12 so my tsconfig has something like:
"extends": "@tsconfig/node12/tsconfig.json",
and now as mentioned I need "lib": ["DOM"] for browser things but what we extend has it's own lib:
"lib": ["es2019", "es2020.promise", "es2020.bigint", "es2020.string"],
So I have to repeat all of that in my own "lib" property. It'd be nice to not have to do that.
Indeed, I recently forgot and just had this as my lib:
"lib": ["DOM"]
And it took me some time to figure out what happened when typescript error'd on some es2019 features which I knew worked in node 12. Rather confusing.
So it's not just monorepos. Also, what happens when the author of a module e.g. of @tsconfig made a mistake with the "lib" property and they changed it later, after a lot of people had already copied the value due to the lack of array merging? I think the user of a library shouldn't be expected to know the internals of that library, even if it is just config files.
As far as static analyzability, we already have an "extends" keyword. Meaning that one already has to run some logic over a tsconfig to get what it "really" says. What's the difference between that and executing it as a tsconfig.js file? Or having a special merge array syntax?
I also face this problem as @zaverden, also while using nx. The thing is that having a nested folders in one app does not always mean that that these nested folders should be placed in separate libs. It would be great if tsconfig could give an option to merge paths from different extended tsconfig files.
I also see a problem with monorepos. I also agree that it's better to let this issue to be checked by the guys who is developing monorepo tools first. And, yup, it's not a problem to use relative paths instead of aliases inside of applications, but it just may look bad. I also agree that having too much app internals may be a signal to perform some refactoring.
But i am don't think that having some internal libs per application is a bad pratice at all. First of all, putting all inside base tsconfig doesn't gives us possibility to check if app A has access to app B. It just creates some global paths across all the apps. So, this means desired library should be kept inside of concrete app.
Five more cents: for example, let's look at NX. NX has a solution to limit access to some libs (an linter extension, see enforce-module-boundaries) and it may cover the purpose of checking if everything is fine here. But anyway if some of your app libs is app-specific and you just want to escape from '../../../../' nightmare you will need either to keep calm and keep it as is or make your lib public (performing some refactoring to make your lib less app-dependent). Obviously second variant will always be a good choice, but, you know, but i don't think spending all your time for building an great architecture is a good choice ...especially if your customer is not ready to pay you for refactoring and planning :D
Anyway, it's just an opinion.
Can i live without this? Yes. Do i want to have such feature? Yes. Mostly i agree with @diegonyc. This feature should not be done 'as is', even if "paths inheritance" feels like a pretty significant feature.
It's not just merging issue, the extends mechanism is partially broken.
Consider below issue:
I have a root tsconfig.json, like:
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@shortcut/*": ["src/app/my-module/*"]
}
},
// ... and so on
}
And another file extending above (in src sub-directory), like:
{
"extends": "../tsconfig.json",
"compilerOptions": {
"baseUrl": "./",
}
// ... and so on
}
Result:
even though the "paths" in root-config are valid, the new "baseUrl" will make them all useless.
Solution
Either go create PR god knows where, and teach TSC how to cache configurations ONLY after convert to absolute-path.
Or simply update root-config, adding ./ to all paths, like:
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@shortcut/*": ["./src/app/my-module/*"]
}
}
}
Making root's
baseUrloption useless, but we get error if we don't have it.
But unfortunately not even above did work, so, happy copy-pasting all paths into sub-directories ;-)
Update 2023; At least baseUrl issue may be fixed by now.
We also have the same issue described before, which means that we have a monorepo and would like to be able to extend the paths defined in our default tsconfig.json file. With the growing trend of building microfrontends I guess this topic will become a painpoint for many people. Copy/pasting lines of code around can't be a viable solution for too long. It would be so nice having the ability to add some code in there.
Personally, I'd sooner like to see a
tsconfig.jsortsconfig.tsaffordance (similar to a webpack config) to generally leverage JS syntax for complex configuration merging rather than introduce special-snowflake "syntax" into a json file with complex layering and execution semantics. There's already a thing that exists to describe these kinds of procedural transforms, and it's called "code".
I totally agree with that !
I wrote a utility for our organization that essentially copy/pastes paths and references using the package.json exports field as the one source of truth. We have the following invariants:
- One
tsconfig.jsonperpackage.json, and they are located next to each other in the subproject directory - There is a top-level TypeScript composite "solution" project, as described in the TypeScript documentation which references all monorepo
tsconfig.jsonfiles tsc -boutputs*.d.tsfiles for each subproject, and these are specified in thepackage.jsonexports->typesfields. When running node we directly reference the build artifacts, we don't use ts-node.
This is the utility: https://github.com/laverdet/typescript-monorepo/blob/main/update-tsconfig.mjs
And here's how it manages the various tsconfig files. It only touches paths and references and leaves the rest of the tsconfig file alone, byte for byte. Therefore, we can still include comments and project-specific flags in each tsconfig without having them blown away.
https://github.com/laverdet/typescript-monorepo/blob/main/packages/common/tsconfig.json
After implementing this, the TypeScript experience in VS Code got a lot better. Fox example, renaming a symbol "just works" across multiple dependencies. In the example repo you can rename the "utility" function and it carries over to @org/client. Before this solution I had "TypeScript: Restart TS Server" bound to a hotkey because I had to invoke it any time I changed a function signature in a different monorepo package.
More use cases for extending the include and exclude arrays of an extends config from node_modules:
- Companies create multiple projects based on their "boilerplate", with no customizations needed for
includeorexclude - Students create multiple projects based on a programming boilerplate, with no customizations needed for
includeorexclude(this is our use case)
Such a "boilerplate" config may look like this:
"include": [
"../../../../**/.eslintrc.cjs",
"../../../../**/*.ts",
"../../../../**/*.tsx",
"../../../../**/*.js",
"../../../../**/*.jsx",
"../../../../**/*.cjs",
"../../../../**/*.mjs",
"../../../../.next/types/**/*.ts",
"../../../../next-env.d.ts"
],
More about this in this issue here:
- https://github.com/microsoft/TypeScript/issues/51213