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

Allow overrides in tsconfig #33407

Open
5 tasks done
donaldpipowitch opened this issue Sep 13, 2019 · 19 comments
Open
5 tasks done

Allow overrides in tsconfig #33407

donaldpipowitch opened this issue Sep 13, 2019 · 19 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

@donaldpipowitch
Copy link
Contributor

donaldpipowitch commented Sep 13, 2019

Search Terms

  • tsconfig
  • overrides
  • eslint
  • glob

Suggestion

It would be nice if tsconfig.json files would allow an overrides section which works in the same way as ESLint. Here I have just one config file in my root for all kind of files and use cases. TypeScript currently needs multiple tsconfig.json files.

Use Cases

As far as I know editors like VS Code look for the nearest tsconfig.json for proper TypeScript support. This make it hard to properly type files which are used for different environments (e.g. source code vs tests vs storybook examples and so on) in the same directory, which seems to be a common convention nowadays (e.g. src/file.ts, src/file.test.ts, src/file.stories.ts).

Most people seem to ignore this fact which makes test globals like describe available in source code files.

I once tweeted about this with a small example:

Examples

{
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "module": "esnext",
    "moduleResolution": "node",
    "noEmit": true,
    "strict": true,
    "skipLibCheck": true,
    "target": "es2017",
    "types": []
  },
  "overrides": [
    {
      "include": ["src/**/*"],
      "exclude": ["src/**/*.test.ts"],
      "compilerOptions": {
        "jsx": "react",
        "types": ["webpack-env"],
        "plugins": [
          {
            "name": "typescript-styled-plugin"
          }
        ]
      }
    },
    {
      "include": ["src/**/*.test.ts"],
      "compilerOptions": {
        "types": ["jest", "node"]
      }
    }
  ]
}

Checklist

My suggestion meets these guidelines:

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

AnyhowStep commented Sep 13, 2019

Maybe they shouldn't be mixing test and source files?

The extends, files, include and exclude options can be used in most cases,
https://www.typescriptlang.org/docs/handbook/tsconfig-json.html#configuration-inheritance-with-extends

#8435 (comment)

#8435 (comment)

@donaldpipowitch
Copy link
Contributor Author

Maybe they shouldn't be mixing test and source files?

That's what I suggest as well, but I'm not the majority as far as I can tell. The community seems to prefer it differently.

@donaldpipowitch
Copy link
Contributor Author

I understand the old comment from @mhegazy that a tsconfig.json matches one tsc call. Maybe we need a tsconfig-compose.json or something similar in that case. Something which editors can automatically pick up to get the correct project configuration. Even Microsofts React Starter mixed source and test files and now recommends CRA which does the same. (It's also not just tests and sources. It can be some configuration files, storybook stories and so on mixed in the same directory.)

@RyanCavanaugh RyanCavanaugh added 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 labels Sep 16, 2019
@leethree
Copy link

mixing test and source files

It's called "colocating" and it's a common pattern in the JavaScript world.

Exhibit A:

In a component-based architecture, we already combine templates, stylesheets, and JavaScript in one directory. As it makes sense to group these tightly-coupled files together, including unit tests in this selection is the natural extension of the underlying model.

https://islovely.co/posts/colocating-unit-tests

Exhibit B:

It’s so much nicer to colocate tests with sources (either separate folder or *.test.js files). Can’t see any reasons to do it any other way.

https://twitter.com/dan_abramov/status/762658867327172608?lang=en

@donaldpipowitch
Copy link
Contributor Author

FYI: I haven't tried it, but it could be that this problem can now be solved by having “Solution Style” tsconfig.json Files.

Something like this:

// tsconfig.json
{
    "files": [],
    "references": [
        { "path": "./tsconfig.src.json" },
        { "path": "./tsconfig.test.json" },
    ]
}

If that works it would probably make this issue obsolete.

@aleclarson
Copy link

I've tried the "solution project" approach without success:
https://gist.github.com/aleclarson/6ae9a3b6b9d06c492fb9a3b6ec395411

@tizmagik
Copy link

This would be super useful. Without this, we need separate tsconfig for source and test files, separate build, dev scripts etc etc. Meanwhile, our eslint config can stay in a single file and can use unified scripts because of the overrides capability allowed there.

@ThomasFindlay
Copy link

Does anyone know when a solution for the problem specified by the author will be available? Like some people here mentioned, it is a common pattern to put source and test files together. Another example where defining tsconfig for specific files only would be useful is Jest + Cypress. Both of these provide global definitions for methods like expect or describe. Now with the recent addition of Cypress Components I can imagine people using Jest for unit testing methods whilst Cypress Components, as the name suggests, for components in frameworks like Vue or React.

@ryankav
Copy link

ryankav commented Nov 3, 2021

I would also support the addition of this feature. Am currently migrating a codebase to typescript and it would be nice to specify strict compiler options on .ts files whilst having weaker rules on the .js ones. This way the check javascript flag can be enabled immediately without having to chose between being overwhelmed by errors in the javascript files or weaker compiler settings in the newly made typescript files.

As a concrete example it could be nice to allow typescript files to be written with strict mode true, yet have the javascript files include the strictFunctionTypes compiler setting so that we can check that any newly typed functions imported to the javascript files are being used correctly immediately. If there is already a way to do this then I would appreciate any advice as I can't seem to find an explanation or working implementation for how to do this.

@tavoyne
Copy link

tavoyne commented Jan 11, 2022

One pretty common use case I think would be in mono-repository architectures, where you have multiple packages all having their own src, test, etc. directories. You want to be able to enforce a specific config for ALL test files, ALL src files, etc. For example, you might want to enable isolatedModules for src files since they're compiled using Babel, but not for test files, in which it's not needed. In this scenario, the overrides syntax offered by ESLint (which would have suffered from the same limitation otherwise) comes in very handy indeed.

@slikts
Copy link

slikts commented Jan 19, 2022

Colocating test, source and other types of files has been widely adopted as a pattern because it just makes sense for discoverability to group files by their concern and not their type. Test files in particular could benefit from looser types, which is why I've long wished that TypeScript would support overrides for colocated files and not just different directories. Lacking support for it means, for example, that people sometimes work around it with ts-nocheck directives, losing even the benefits of loose typing.

@JacobMillner
Copy link

Another use case is when you have a very large project and would like to turn on a rule but can't just enable it globally. The best example would be enabling strict mode for single folders at a time, fixing the errors and then moving on to the next one over time.

@slikts
Copy link

slikts commented Feb 8, 2022

@JacobMillner If it's for separate directories, you should be able to do what you mentioned by adding a nested tsconfig.json that extends from the root config and enables strict mode just for that directory and subdirectories.

@JacobMillner
Copy link

@slikts That works perfectly thank you!

@steffenagger
Copy link

// tsconfig.json
{
    "files": [],
    "references": [
        { "path": "./tsconfig.src.json" },
        { "path": "./tsconfig.test.json" },
    ]
}

@donaldpipowitch: After seeing this suggestion I've tried out a modified structure, which we're currently using it in our latest project - just sharing our findings. Maybe it can unblock others 🤷 (but your initial suggestion would definitely be an improvement in my book).

I'm also interested in feedback on potential problems this setup might cause.

Click here to see full structure & findings

Into: We're using Vue3 (so adjust accordingly for other libs), with unit-tests running in Jest & e2e-tests + component-tests running in Cypress.
File patterns:

  • unit-tests: src/**/*.unit.ts
  • component-tests: src/**/*.ct.ts
  • e2e-tests: e2e/**/*.e2e.ts (not relevant in this context)

tsconfig.json contains our general setup, excluding all tests related - with the addition of "references":

{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "strict": true,
    "baseUrl": ".",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "useDefineForClassFields": true,
    "resolveJsonModule": true,
    "sourceMap": true,
    "lib": ["esnext", "dom"],
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": [
    "src/**/*.ts",
    "src/**/*.vue",
    "src/shims-vue.d.ts"
  ],
  "exclude": [
    "node_modules/cypress/types",
    "node_modules/@types/jest",
    "src/**/*.ct.ts",
    "src/**/*.e2e.ts",
    "src/**/*.unit.ts"
  ],
  "references": [
    { "path": "./tsconfig.jest.json" },
    { "path": "./tsconfig.cypress.json" }
  ]
}

tsconfig.cypress.json:

{
  "extends": "./tsconfig",
  "compilerOptions": {
    "composite": true,
    "types": ["cypress"]
  },
  "include": [
    "src/**/*.ts",
    "src/**/*.vue",
    "e2e/**/*.e2e.ts",
    "test-data/**/*.json",
    "src/shims-vue.d.ts"
  ],
  "exclude": [
    "node_modules/@types/jest",
    "src/**/*.unit.ts"
  ]
}

tsconfig.jest.json::

{
  "extends": "./tsconfig",
  "compilerOptions": {
    "composite": true,
    "types": ["jest"]
  },
  "include": [
    "src/**/*.ts",
    "src/**/*.vue",
    "test-data/**/*.json",
    "src/shims-vue.d.ts"
  ],
  "exclude": [
    "node_modules/cypress/types",
    "src/**/*.ct.ts",
    "src/**/*.e2e.ts",
  ]
}

Now this fixes most problems... BUT, some tools in our tool-chain (for type-checks) does not yet support "incremental mode", triggered by defining "references" (as far as my understanding goes). This tooling is only used for actual production code (not tests), so I ended up writing a pre/post script for these tasks, essentially taking tsconfig.json removing references and temporarily saving it as tsconfig.prod.json (which is specified for said tools) - as so:

Pre script temp-tsconfig.prod.json-create.js:

const fs = require('fs');
const tsconfig = require('../tsconfig.json');

// remove references before creating temp tsconfig.prod.json
delete tsconfig.references;

fs.writeFileSync('tsconfig.prod.json', JSON.stringify(tsconfig), 'utf8');

Post script temp-tsconfig.prod.json-cleanup.js:

const fs = require('fs');

// cleanup after temp-tsconfig.prod.json-create.js
if (fs.existsSync('tsconfig.prod.json')) {
  fs.rmSync('tsconfig.prod.json');
}

@donaldpipowitch: thank for the suggestion.
Though it's not exactly what I would call "elegant" (feels like a hack), it could be a solution - for now.


(it's a bit long, so collapsed 🤷)

@redevill
Copy link

FYI: I haven't tried it, but it could be that this problem can now be solved by having “Solution Style” tsconfig.json Files.

Something like this:

// tsconfig.json
{
    "files": [],
    "references": [
        { "path": "./tsconfig.src.json" },
        { "path": "./tsconfig.test.json" },
    ]
}

If that works it would probably make this issue obsolete.

FWIW - Have also tried this "Project references / build mode" approach. However, found that "tsc -b" does not produce the same output as running tsc in the subproject directory. This was the case when one of the sub-projects tsconfig.json (extending the base tsconfig.base.json) was set to override the module to commonjs - from ESM. The output from tsc -b was ESM, and the tsc from the subproject directory came out commonjs.

@AndreiSoroka
Copy link

AndreiSoroka commented Dec 2, 2022

Hi guys. I'm not sure but maybe it can help anybody

Problem

In my situation, my project is Vue + Storybook

But I don't want to keep the storybook like part of a project, and I move storybooks packages/config/settings into subfolder
Screenshot 2022-12-02 at 23 01 01

where .stories.ts stay like part of project
image

So, I need to add rule only for *.storybook.ts files

{
  "extends": "@vue/tsconfig/tsconfig.web.json",
  "include": [
    "src/**/*.stories.ts", // <-- only for storybook files
  ],
  "compilerOptions": {
    "composite": true,
    "baseUrl": ".",
    "paths": {
      "@/*": [
        "./src/*"
      ],
      "*": [
        "./storybook/node_modules/*" // <-- add additional node_modules ['node_modules', 'storybook/node_modules']
      ]
    }
  }
}

But we don't have override rules for patterns for files, but can override include and exclude

I need to add to include all files "include": ["src/**/*"], because otherwise:
image
but I have collision rules, so we should fix in the reverse situation
image

Solution

tsconfig.json

{
  "files": [],
  "references": [
    {
      "path": "./tsconfig.config.json"
    },
    {
      "path": "./tsconfig.app.json" // for application files
    },
    {
      "path": "./tsconfig.vitest.json"
    },
    {
      "path": "./tsconfig.storybook.json" // for only storybook files
    }
  ]
}

tsconfig.app.json

{
  "extends": "@vue/tsconfig/tsconfig.web.json",
  "include": [
    "env.d.ts",
    "src/**/*",
    "src/**/*.vue"
  ],
  "exclude": [
    "src/**/__tests__/*",
    "src/**/*.stories.ts" // without storybook files
  ],
  "compilerOptions": {
    "composite": true,
    "baseUrl": ".",
    "paths": {
      "@/*": [
        "./src/*"
      ]
    }
  }
}

tsconfig.storybook.json

{
  "extends": "@vue/tsconfig/tsconfig.web.json",
  "include": [
    "src/**/*" // all files
  ],
  "compilerOptions": {
    "composite": true,
    "baseUrl": ".",
    "paths": {
      "@/*": [
        "./src/*"
      ],
      "*": [
        "./storybook/node_modules/*" // additional node_modules
      ]
    }
  }
}

How it works
when we use any files but not *.stories.ts then we apply tsconfig.app.json
image
when the previous pattern is wrong, and we use any file (without exclude) then we apply tsconfig.storybook.json
image

I think it is difficult for more combinations.

What I expected

I expect from slow Microsoft Typescript team this feature:

{
  "extends": "@vue/tsconfig/tsconfig.web.json",
  "include": [
    "src/**/*.ts",
    "src/**/*.vue"
  ],
  "compilerOptions": {
    "composite": true,
    "baseUrl": ".",
    "paths": {
      "@/*": [
        "./src/*"
      ]
    }
  },
  // Example 1
  "overridesFiles": {
    "src/**/*.stories.ts": {
      // storybook
      "paths": {
        "*": [
          "./storybook/node_modules/*"
        ]
      }
    },
    "src/**/*.test.ts": {
      // tests
      "paths": {
        "*": [
          "./test/node_modules/*"
        ]
      }
    }
  },
  // Example 2
  "overridesPattern": [
    {
      "patternFiles": [
        "src/**/*.magic.ts",
        "src/**/*.magic.vue"
      ],
      "override": {
        "paths": {
          "*": [
            "./test/node_modules/*"
          ]
        }
      }
    }
  ]
}

or any other solution like https://eslint.org/docs/latest/user-guide/configuring/rules#using-configuration-files-1

P.s. the author of this issue gave a very cool example

we always need to make crutches, and we get answers after 3-4 years

@mattidupre
Copy link

mattidupre commented Jun 19, 2023

@AndreiSoroka's answer ended up saving the day for me.

To clarify and simplify, a general tsconfig-dist.json reference glob seems to need to be called first so it has to be listed last. For example, assume one of each file.test.ts, file.stories.ts, file.etc.ts, file.ts (dist) containing the following code that checks for noImplicitAny:

// if noImplicityAny is set, (i) will error.
const fn = (i) => i;
fn('');

Assume one entry file tsconfig.json pointing to one tsconfig-*.json each matching their respective file above:

// tsconfig.json
{
  ...
  "references": [
    { "path": "./tsconfig-test.json" },
    { "path": "./tsconfig-stories.json" },
    { "path": "./tsconfig-etc.json" },
    { "path": "./tsconfig-dist.json" } // appears last, called first
  ]
}
// tsconfig-dist.json
{
  // called first
  // (i) errors
  ...
  "compilerOptions": { "noImplicitAny": true },
  "include": ["./**/*"], "exclude": ["/**/*.test.*", "/**/*.stories.*", "/**/*.etc.*"]
}
// tsconfig-etc.json
{
  // called second
  // (i) does not error
  ...
  "compilerOptions": { "noImplicitAny": false },
  "include": ["./**/*"], "exclude": ["/**/*.test.*", "/**/*.stories.*"]
}
// tsconfig-test.json
{
  // called third
  // (i) errors
  ...
  "compilerOptions": { "noImplicitAny": true },
  "include": ["./**/*"], "exclude": ["/**/*.stories.*", "/**/*.etc.*"]
}
// tsconfig-stories.json
{
  // called fourth
  // (i) does not error
  ...
  "compilerOptions": { "noImplicitAny": false },
  "include": ["./**/*"], "exclude": ["/**/*.test.*", "/**/*.etc.*"]
}

The reason we have to include: ['./**/*'] and work off exclude: [...] is because our stories and tests will include general dist files. If file.stories.ts tries to import file.ts but its tsconfig.json is include: ['./**/*.stories.*'], typescript will complain:

File '.../file.ts' is not listed within the file list of project '.../tsconfig-stories.json'.

So far this seems to work, but wouldn't one expect the order to be reversed? I agree with the general sentiment that this needs an explicit solution. Even something to the effect of an includeImports: [] to expand the scope of imports beyond include: [] while allowing those files to be excluded on first match would do it.

@DiFuks
Copy link

DiFuks commented Jun 20, 2024

Hello! I recently finished working on a TypeScript plugin that allows overriding config for files and folders. It's still pretty rough, but it works. I would be happy to get feedback :) https://github.com/DiFuks/ts-overrides-plugin

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