Skip to content
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

Feature request: allow user to merge extended arrays in tsconfig files #20110

Open
jsamr opened this issue Nov 17, 2017 · 47 comments
Open

Feature request: allow user to merge extended arrays in tsconfig files #20110

jsamr opened this issue Nov 17, 2017 · 47 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@jsamr
Copy link

jsamr commented Nov 17, 2017

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"]
}
@weswigham
Copy link
Member

weswigham commented Nov 17, 2017

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

@DanielRosenwasser DanielRosenwasser added In Discussion Not yet reached consensus Suggestion An idea for TypeScript labels Nov 17, 2017
@mhegazy
Copy link
Contributor

mhegazy commented Nov 17, 2017

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.

@mhegazy
Copy link
Contributor

mhegazy commented Nov 18, 2017

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.

@weswigham
Copy link
Member

"Do not over-complicate the implementation with abstractions where a little copying will suffice" - Unattributed programming koan

@jsamr
Copy link
Author

jsamr commented Nov 18, 2017

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.

@jsamr
Copy link
Author

jsamr commented Nov 18, 2017

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.

@weswigham
Copy link
Member

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.

@jsamr
Copy link
Author

jsamr commented Nov 18, 2017

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

@weswigham
Copy link
Member

I, personally, would prefer a code file. @mhegazy disagrees. :)
As I said, strong opinions.

@jsamr
Copy link
Author

jsamr commented Nov 18, 2017

@weswigham OK, then I'm totally with you on this. Either through extended syntax or "codable" config file, I would love the feature.

@mhegazy
Copy link
Contributor

mhegazy commented Nov 18, 2017

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.

@mhegazy
Copy link
Contributor

mhegazy commented Nov 18, 2017

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.

@jsamr
Copy link
Author

jsamr commented Nov 19, 2017

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

@mhegazy
Copy link
Contributor

mhegazy commented Nov 20, 2017

We are always open to revisiting requests.

@mhegazy mhegazy added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature and removed In Discussion Not yet reached consensus labels Nov 20, 2017
@donaldpipowitch
Copy link
Contributor

donaldpipowitch commented Jul 23, 2018

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);

@zaverden
Copy link

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

@asciidiego
Copy link

asciidiego commented May 19, 2020

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

@zaverden
Copy link

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

@asciidiego
Copy link

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

@zaverden
Copy link

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

@anthonycaron
Copy link

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

I totally agree with that !

@laverdet
Copy link
Contributor

laverdet commented Dec 1, 2022

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.json per package.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.json files
  • tsc -b outputs *.d.ts files for each subproject, and these are specified in the package.json exports -> types fields. 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.

@karlhorky
Copy link
Contributor

More use cases for extending the include and exclude arrays of an extends config from node_modules:

  1. Companies create multiple projects based on their "boilerplate", with no customizations needed for include or exclude
  2. Students create multiple projects based on a programming boilerplate, with no customizations needed for include or exclude (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:

@karlhorky
Copy link
Contributor

@mhegazy in #20110 (comment):

Code file is not statically alayzable

@iPherian in #20110 (comment)

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?

This seems to make sense for me - it seems like the extends feature already suggests executing code to retrieve the full configuration anyway.

@mhegazy would you reconsider your position on a code-based tsconfig.ts file? It is a very common pattern in the tooling ecosystem, and would enable a lot of use cases, including the use cases I mentioned above and also that monorepos / workspaces are much more mainstream now.

@karlhorky
Copy link
Contributor

One other thing I've been watching is the new extends capabilities from #50403 (PR by @navya9singh, reviewed by @andrewbranch, @sheetalkamat, @RyanCavanaugh).

To me, it doesn't seem like it solves these extends problems of merging include, exclude, files arrays, but maybe I'm missing something.

Is there an alternative path forward connected to the comment about "it's likely that we'll be encouraging bundlers/etc to ship specific config subsets that describe their behavior" from @RyanCavanaugh?

lightyen added a commit to lightyen/typescript-paths that referenced this issue Mar 20, 2023
@BlackAF
Copy link

BlackAF commented Mar 31, 2023

A way to merge paths would be great, the use case I want to achieve is the following:

The project structure:

├── apps
│   ├── api
│   │   └── tsconfig.json
│   ├── webapp
│   │   └── tsconfig.json
│   └── etc
├── libs
│   ├── libA
│   │   └── tsconfig.json
│   ├── libB
│   │   └── tsconfig.json
│   └── etc
├── tsconfig.base.json
├── tsconfig.paths.json
└── etc

Every library/app has its own tsconfig with a path alias for the ~ home path:

libXYZ - tsconfig.json
{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "paths": {
      "~/*": ["./libs/libXYZ/*"]
    }
  },
}

All libraries are listed in the tsconfig.paths.json config (this can either point to the lib's dist or src, pros/cons):

// tsconfig.paths.json
{
  "compilerOptions": {
    "paths": {
      "@myorg/libA": ["dist/libs/libA/src/index.d.ts"],
      "@myorg/libB": ["dist/libs/libB/src/index.d.ts"],
      ...
    }
  }
}

The root config with all the main common settings:

// tsconfig.base.json
{
  "extends": ["./tsconfig.paths.json", "etc"],
  "compilerOptions": {
    ...
  },
}

This setup allows you (well would allow you if it worked) to:

  1. Refer to your internal libraries with aliases: import { Abc } from '@myorg/libname'
  2. Get rid of all relative paths such as import { User } from '../../../models' in favor of import { User } from '~/src/models'

Spent a good hour trying to figure out why this setup wasn't working until I found this issue and realized extends wasn't a merge but an override 😅 Would be nice if this could be implemented. As monorepos become more and more adopted, I'm guessing that the need for this will only grow. Right now the only way to get a setup like this to work is to copy the paths value from tsconfig.paths.json to every single library, this quickly gets out of hand once you have hundreds of libs.

@ehsan-yaqubi
Copy link

ehsan-yaqubi commented Apr 2, 2023

It's been years, and seems code and/or tsconfig.js is not even considered as an option, else it would be supported by now.

And suddenly changing extends to do "merge" instead of "overwrite" would break backward compatibility.

Solution

#1 Merge mode:

Add merges option to tsconfig.json file's syntax, which should mean and function same as extends, but additionally tells TSC to use "merge" mode instead of "overwrite" mode.

#2 Inherit placeholder:

Support <inherit> as object-key and array-value, which we use whenever we don't want to merge each and everything.

Inherit Example

{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      // The value should be an empty-string for now,
      // but in future, maybe also support RegExp pattern as value, which further filters what we inherit ;-)
      "<inherit>": "",
      "@shortcut/*": ["src/app/my-module/*"]
    }
  }

}

Note that in above, only paths is merged, and compilerOptions overwrites the root tsconfig.json file.

#3 Version info:

Optionally, add version option, depending on which TSC decides what it should do with keywords like <inherit>
(if version is new do inherit/merge, if version is old then use as plain-text).

@WoodyWoodsta
Copy link

It doesn't seem to have been mentioned before in this thread, so thought I would add it to the list of use cases: I was quite surprised that the lib and types compiler option arrays are also overwritten. These aren't really subject to the same complexities as having paths in the arrays, and are very much additive type options.

In our setup, we have a central repository of tsconfigs that represent either a base, a platform or a framework. The intention is that we can mix and match these configs in the extends array as of TS 5, and the expectation was that we would get the sum of all provided. This is how eslint extends works (to the best of my experience and knowledge), and I was sortof assuming it would be the case here.

Happy to open another issue if we'd like to treat these separately.

@lobsterkatie
Copy link

lobsterkatie commented Apr 5, 2023

Support <inherit> as object-key and array-value, which we use whenever we don't want to merge each and everything.

I'd be +1 on <inherit>. Pros:

  • It's backwards compatible.
  • It's explicit.
  • It's easy to understand.
  • It allows for not merging (by omitting it) if that's not what you want.

@toantd90
Copy link

toantd90 commented May 2, 2023

+1 on having the option to merge.

@cest-la-v
Copy link

cest-la-v commented May 8, 2023

Here to vote for the <inherit> solution.

If code is not an option, at least provide a way to inherit to avoid the redundent copying right?

Plus, if I want to overwrite or exclude some of it, just use the same key with an empty value:

{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      // The value should be an empty-string for now,
      // but in future, maybe also support RegExp pattern as value, which further filters what we inherit ;-)
      "<inherit>": "",
      "@shortcut/*": ["src/app/my-module/*"],
      // duplicated key to overwrite
      "@here's-to-overwrite/*": ["src/path/*"],
      // empty value to exclude
      "@here's-to-exclude/*": ""
    }
  }
}

@top-master
Copy link

top-master commented May 18, 2023

Because "<inherit>" is not just for "paths", the mentioned empty-string or null may already be used for something else.

Hence, I prefer the RegExp filter idea that was mentioned:

"... support RegExp pattern as value, which further filters what we inherit ..."

Using that, we could simply use the "(?!...)" negative look-ahead pattern,
to tell TSC that it should inherit any object-key, except the keys listed, like:

{
    "<inherit>": "/(?!key-to-exclude-here|some-other-key-to-exclude)/gi"
}

Note that even tools that allow code use Regular-Expressions in such cases, for example see:
https://stackoverflow.com/a/55803188/8740349

@bneigher
Copy link

bneigher commented Jun 1, 2023

would love to see the implemented. I am unable to use both tsconfig.base paths (nx monorepo libs) and tsconfig.local paths (relative path cleanup).. it's one or the other

@laurencefass
Copy link

laurencefass commented Mar 27, 2024

Very surprised to find that this is not possible given the maturity of TS and the prevalence of monorepos. At the moment Im having to replicate "../../../../pathnames" to common resources throughout my project tsconfigs when they are already defined in a common tsconfig root.

Are there any alternative configurations to using extends directive? e.g. project references? Im reading up on it but dont know enough about it yet.

@laverdet
Copy link
Contributor

We use a pnpm monorepo with a handful of internal packages. I wrote a script which walks all the project references in a top-level tsconfig.json file, gets their dependencies from the package.json, and updates path and references in each tsconfig.json to point at the required packages.

https://gist.github.com/laverdet/a192df6ca10458e6fd2c2b32330c5923

@hichemfantar
Copy link

Extending from base configs is pretty painful due to this limitation.
Would appreciate some movement on this by the TS team.

@eternal-eager-beaver
Copy link

Any progress on this issue ?

@gipo355
Copy link

gipo355 commented Jul 6, 2024

Having the same issue as the others.

In an nx monorepo, i'm forced to choose between having root paths or local paths.

On a side note, from day one it made me scratch my head the question: why a tsconfig.ts (and a package.js) wasn't chosen.

@on3dd
Copy link

on3dd commented Jul 18, 2024

This makes working with nested tsconfig.json's very annoying...

@floratmin
Copy link

Monorepo plus fastify plugins becomes copy + paste hell.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests