TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

tsconfig.json should support multiple configurations

Open bolinfest opened this issue 4 years ago • 6 comments

Search Terms

multiple configurations, transitive closure

Suggestion

When using multiple packages in a monorepo, it is common to have a tsconfig.shared.json that all packages extend in their individual tsconfig.json. The current system makes it difficult to support various build configurations across packages (such as "dev" vs. "prod").

Because a property named "config" seems redundant in a file named tsconfig.json, I'll borrow the term select from Bazel and propose the following new option in a tsconfig.json file for defining configurations:

// tsconfig.shared.json
{
  // Via "select", we define two configurations: "universal" and "modern".
  "select": {
    "universal": {
      "compilerOptions": {
        "target": "es3",
        "downlevelIteration": true,
      },
    },
    "modern": {
      "compilerOptions": {
        "target": "es2019",
      },
    },
  },

  // These compilerOptions are common to both configurations.
  "compilerOptions": {
    "sourceMap": true,
    "module": "esnext",
    "moduleResolution": "node",
    "lib": ["dom", "es5", "es2019"],
    "strict": true,
  },
}

To complement this, tsc would have to support a new --select flag:

# Assume tsconfig.json extends tsconfig.json and defines "include" and "references".
# This would build the modern configuration.
$ tsc --build --select modern -p tsconfig.json

Ideally, it would also be possible to parameterize paths such as outDir so that you could also define this on the base compilerOptions in tsconfig.shared.json:

"outDir": "./dist/${select}/",

though I haven't been able to get "outDir" to do what I want in tsconfig.shared.json in my own project, so there might be more work to do on that front.

Use Cases

I want to be able to have a shared tsconfig.json that defines multiple configurations in one place that can be shared across multiple modules, as opposed to the current situation where we need O(M ⋅ C) tsconfig.json files where M is the number of modules and C is the number of configurations. A detailed example is below.

Examples

I have a Yarn workspace where each package is under the modules/ folder, so my root package.json contains the following:

{
  "workspaces": [
    "modules/*"
  ],
  "scripts": {
    "compile": "tsc --build modules/*"
  }
}

I also have a shared.tsconfig.json in the root of my Yarn workspace where I define the "compilerOptions" that I want all packages in the workspace to use. (For example, I set "target": "es2019" in shared.tsconfig.json.) Each folder under modules/ contains its own tsconfig.json file that contains the line:

  "extends": "../../tsconfig.shared.json",

and there is also a "references" property with the appropriate list of relative paths to other packages in the workspace.

Ideally, each such tsconfig.json would contain only "extends" and "references", but it seems I have to redeclare "compilerOptions.outDir" and "include" in each of these files even though the values are the same in each one (["src/**/*"] and "./dist", respectively).

OK, so far, so good, but now I want to be able to build my entire Yarn workspace with a different set of compilerOptions, specifically:

"downlevelIteration": true,
"target": "es3",

Ideally, I would be able to specify this in one place and leverage the "references" I already had to define so I could compile one .ts file or package and all of its transitive deps with these compiler options. (I happen to be using ts-loader in Webpack, so my ultimate output is a single .js file, which perhaps biases my expectations here.)

Unfortunately, there does not appear to be any clean way to do that. I could go through and define a tsconfig.es3.json in each of my packages that looks like this:

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "downlevelIteration": true,
    "target": "es3",
  },
}

though even if I did that, I'm not sure I could build all of my modules as ES3 with a one-liner as I did in my original package.json file. Now I'm faced with the additional incidental complexity of:

  • Maintaining O(M) tsconfig.es3.json files and ensuring all of them are in sync.
  • Introducing Lerna or some other tool to "walk my DAG" and run tsc -b -p tsconfig.es3.json or something like that.

With the --select proposal, I could avoid the extra tsconfig.json files and still build via a one-liner:

tsc --build --select universal modules/*

I would expect if any of the tsconfig.json files under modules/* did not define a "universal" configuration, the build command would fail (or at least the strictness should be configurable).

Checklist

My suggestion meets these guidelines:

  • [x] This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • [x] This wouldn't change the runtime behavior of existing JavaScript code
  • [x] This could be implemented without emitting different JS based on the types of the expressions
  • [x] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • [x] This feature would agree with the rest of TypeScript's Design Goals.

bolinfest avatar Apr 10 '20 04:04 bolinfest

related https://github.com/microsoft/TypeScript/issues/21951

mohsen1 avatar Apr 11 '20 13:04 mohsen1

I had a similar problem about a year ago so I built a tool to help manage all the tsconfig.json files in a monorepo. https://github.com/isomorphic-typescript/ts-monorepo Currently I'm working on moving it from Lerna to Yarn v2 and adding a templates feature so one config can inherit multiple templates and overwrite the specific values it wants to.

ozyman42 avatar Apr 14 '20 20:04 ozyman42

What do you guys think about supporting a tsconfig.js in addition to tsconfig.json? That way we would be free to build our own sharing / extending of configs through the full power of js. I personally prefer js-configs over json-config as they give me much more freedom and power (at the price of doing stupid mistakes of course).

shaman-apprentice avatar May 01 '20 13:05 shaman-apprentice

I'm having a similar problem, not having this feature forces me to have duplicate configs in my monorepo.

Alexandre-Fernandez avatar Sep 17 '22 22:09 Alexandre-Fernandez

What do you guys think about supporting a tsconfig.js in addition to tsconfig.json? That way we would be free to build our own sharing / extending of configs through the full power of js. I personally prefer js-configs over json-config as they give me much more freedom and power (at the price of doing stupid mistakes of course).

The team took a stance on that here, but I haven't looked to see if anything's changed about their stance since then.

nicolas377 avatar Sep 17 '22 22:09 nicolas377

I don't see how we're supposed to use TypeScript in cross-platform code in conjunction with babel-module-resolver-plugin without support for multiple configs via tsconfig.js or some other approach.

For example:

  import { someFunc } from 'zlib-platform';

In Node this resolves to zlib-node and in React Native this resolves to zlib-rn, but we're stuck with just "Cannot find module 'zlib-platform' or its corresponding type declarations." because we can't tell TypeScript how to resolve the modules.

Nantris avatar Sep 22 '22 23:09 Nantris