Repository: TypeScript
Issue: tsconfig.json should support multiple configurations
Labels: Suggestion,Awaiting More Feedback

## 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](https://docs.bazel.build/versions/master/configurable-attributes.html) from Bazel and propose the following new option in a `tsconfig.json` file for defining configurations:

```js
// 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:

```json
{
  "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:

```json
  "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](https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals).