Search code examples
typescriptvisual-studio-codetsconfig

In a TypeScript project, can the source and test files share directories and use different tsconfig.jsons, all while being understood by VS Code?


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?


Solution

  • 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",
        ],
    }