skip to Main Content

Here is the directory structure:

root/
  tsconfig.json
  ts/
    index.ts
    index.spec.ts

I want separate tsconfig.json settings for spec files and production code. For example, I want "noImplicitAny": true for my production code and "noImplicitAny": false for my test code.

I also want Visual Studio Code to be in sync with tsc, eslint, etc. In other words, if Visual Studio Code reports an error, I want my build process to report the same error, and vice versa.

Although I’ve found solutions that get me 90% of the way, I’ve yet to find a solution that completely works. Usually, it’s VS Code reporting errors that aren’t actually there.

How do I meet all these requirements?

2

Answers


  1. Chosen as BEST ANSWER

    Don't use this: compiles and builds, but doesn't work (see my other answer for details)

    The "trick" is to use project references. Although the linked documentation states exactly that and it's not exactly hidden, it is missing some crucial example configurations. It's also written under the assumption you want separate test and source directories, which is not the case here.

    Two new tsconfigs are added to the project at the root:

    root/
      tsconfig.src.json # new
      tsconfig.spec.json  # new
      tsconfig.json
      ts/
        index.ts
        index.spec.ts
    

    Here is the root tsconfig.json. Visual Studio Code only reads tsconfig.json files with this name, so it has to function as a sort of base configuration:

    {
        "compilerOptions": {
            "incremental": true,
            "target": "es2020",
            "module": "es2020",
            "isolatedModules": true,
            "forceConsistentCasingInFileNames": true
        },
        "references": [
            { "path": "tsconfig.spec.json" },
            { "path": "tsconfig.src.json" }
        ],
        "exclude": ["."] // by default tsconfig, includes most files. This prevent that.
    }
    

    tsconfig.src.json

    {
        "compilerOptions": {
            "composite": true,
            "outDir": "dist",
            "rootDir": "src",
            "noImplicitAny": true // note this
        },
        "exclude": [ "src/**/*.spec.ts" ],
        "extends": "./tsconfig.json",
        "include": [ "src" ]
    }
    

    tsconfig.spec.json

    {
        "compilerOptions": {
            "allowSyntheticDefaultImports": true,
            "esModuleInterop": true,
            "isolatedModules": false,
            "moduleResolution": "node",
            "types": ["jest"],
            "noImplicitAny": false, // note this
        },
        "exclude": [],
        "extends": "./tsconfig.json",
        "include": [ "src/**/*.spec.ts"]
    }
    

    index.ts

    function fn(s) { // Error: Parameter 's' implicitly has an 'any' type.
      console.log(s.subtr(3));
    }
    fn(42);
    

    index.spec.ts

    export {};
    
    test("there is no compilation error", () => {
      function fn(s) { // NO ERROR: Parameter 's' implicitly has an 'any' type.
        console.log(s.subtr(3));
      }
      fn(42);
    
      expect(true).not.toBe(false);
    });
    
    

  2. I realized after the fact that there were issues with my original answer. For one, the tsconfig.spec.json did not have composite set. Technically, that’s an error, but it’s only reported if another tsconfig references it AND that other tsconfig includes > 0 files.

    Perhaps even more importantly, the tsconfig.spec.json should have referenced the tsconfig.src.json. That didn’t appear as an error because the files in my previous example didn’t import across reference boundaries.

    I’m keeping the first answer around for prosperity. It’s a good (bad?) example demonstrating how your tsconfigs can be error-free and build, even when they’re in an invalid state. (The irony isn’t lost on me.)


    Here is a correct example. I use // @ts-ignore to ignore compile time errors that should (and do) occur; they prove tsconfig.src.json files can’t import tsconfig.spec.json files.

    Project Structure

    .
    ├── package.json
    ├── src1/
    │   └── subdir1/
    │       ├── foo.spec.ts
    │       └── foo.ts
    ├── src2/
    │   └── subdir2/
    │       ├── bar.spec.ts
    │       └── bar.ts
    ├── tsconfig.json
    ├── tsconfig.spec.json
    └── tsconfig.src.json
    

    ./package.json

    {
      "name": "project-reference-example",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "scripts": {
        "build": "tsc --build"
      },
      "keywords": [],
      "author": "",
      "license": "ISC",
      "devDependencies": {
        "typescript": "^5.1.6"
      }
    }
    
    

    ./src1/subdir1/foo.spec.ts

    import { PROD_FOO } from './foo'
    console.log(PROD_FOO);
    
    export const TEST_FOO = 1;
    

    ./src1/subdir1/foo.ts

    // @ts-ignore File './src1/subdir1/foo.spec.ts' is not listed within the file list of project './tsconfig.src.json'. Projects must list all files or use an 'include' pattern. ts(6307)
    import { TEST_FOO } from './foo.spec'
    console.log(TEST_FOO);
    
    export const PROD_FOO = 1;
    

    ./src2/subdir2/bar.spec.ts

    import { PROD_BAR } from './bar'
    console.log(PROD_BAR);
    
    export const TEST_BAR = 1;
    

    ./src2/subdir2/bar.ts

    // @ts-ignore File './src1/subdir1/bar.spec.ts' is not listed within the file list of project './tsconfig.src.json'. Projects must list all files or use an 'include' pattern. ts(6307)
    import { TEST_BAR } from './bar.spec'
    console.log(TEST_BAR);
    
    export const PROD_BAR = 1;
    

    ./tsconfig.json

    {
        "compilerOptions": {
            "target": "es2018",
            "module": "es2015",
            "lib": [
                "es2020",
                "dom",
                "DOM.Iterable"
            ],
            "allowJs": false,
            "sourceMap": true,
            "outDir": "tscbuild",
            "importHelpers": true,
            "downlevelIteration": true,
            "strict": true,
            "noImplicitAny": true,
            "strictNullChecks": true,
            "strictFunctionTypes": true,
            "strictBindCallApply": true,
            "strictPropertyInitialization": true,
            "alwaysStrict": true,
            "noImplicitReturns": true,
            "noFallthroughCasesInSwitch": true,
            "moduleResolution": "node",
            "esModuleInterop": true,
            "forceConsistentCasingInFileNames": true,
            "noErrorTruncation": true,
        },
        "references": [
            {
                "path": "./tsconfig.src.json"
            },
            {
                "path": "./tsconfig.spec.json"
            },
        ],
        "include": [],
    }
    

    ./tsconfig.spec.json

    {
        "extends": "./tsconfig.json",
        "compilerOptions": {
            "composite": true,
            "noEmit": true,
            "noImplicitAny": false,
            "module": "commonjs"
        },
        "include": [
            "./src*/**/*.spec.ts",
        ],
        "references": [
            {
                "path": "./tsconfig.src.json"
            }
        ]
    }
    

    ./tsconfig.src.json

    {
        "extends": "./tsconfig.json",
        "compilerOptions": {
            "composite": true,
        },
        "include": [
            "./src*/**/*.ts",
        ],
        "exclude": [
            "./src*/**/*.spec.ts",
        ],
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search