In TypeScript projects it’s common to have separate tsconfig files covering the entire project and
separate build contexts which exclude test files. This can lead to failing to check test files with tsc, so type errors in test files creep in
unnoticed only to be discovered later when the root problem is less clear. For example the main tsconfig.json file in a project might be similar to this: {
"compilerOptions": {
"target": "ES2023",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2023"],
"types": ["node"],
"strict": true,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src/**/*", "test/**/*", "typings/**/*.d.ts"]
}
And then there might be a tsconfig.build.json file that extends it but excludes test files from the
build: {
"extends": "./tsconfig.json",
"include": ["src/**/*", "typings/**/*.d.ts"],
"exclude": ["test", "**/*.test.ts", "**/*.spec.ts"]
}
In package.json you might then have a build script: {
"scripts": {
"build": "tsc -p tsconfig.build.json"
}
}
And you might be running the build as a check in GitHub Actions against each PR: ---
name: PR
on:
pull_request:
branches:
- main
jobs:
build:
name: Build
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm build
lint:
...
test:
...
This can end up giving a false sense of security. All PRs pass the checks, which apply the tsc
build, linters and tests to all changes. However, because the build script is specifying
-p tsconfig.build.json, it excludes test files from the tsc build. Test files are being executed
and linted, but not passed through the tsc compiler. ESLint won’t necessarily pick up a change that introduces a type error in a test file. A change
which introduces a type error in a test file might not change that test file itself. It might change
types elsewhere in the project, causing a type error in the test file which is silent due to test
files being excluded from the tsc build. Type errors in test files can creep into the project and we’ll only discover them later when we
happen to look at those test files. By that time, the cause of the type errors might be more
difficult to diagnose. We can avoid that with a specific check build that covers all files by specifying the main
tsconfig instead of the build-specific tsconfig: {
"scripts": {
"build": "tsc -p tsconfig.build.json",
"build:check": "tsc -p tsconfig.json --noEmit"
}
}
That will check all TS files included by the main tsconfig, which will pick up type errors in test
files. We can then use that in the GitHub Action to check PRs: build:
name: Build
steps:
...
- run: pnpm build:check
That combined with the linters and tests in the GitHub Action ensures we catch as many mistakes as
possible before merging each PR, including type errors in test files. It’s also helpful to have a check script in package.json: {
"scripts": {
"build": "tsc -p tsconfig.build.json",
"build:check": "tsc -p tsconfig.json --noEmit",
"lint": "eslint '{src,test}/**/*.ts' && prettier --check '{src,test}/**/*.{ts,json,md,css,html,yml,yaml}'",
"test:coverage": "vitest run --coverage",
"check": "pnpm lint && pnpm build:check && pnpm test:coverage"
}
}
Then running pnpm check covers all checks in one go. We can optionally add that as a Git pre-commit hook at .githooks/pre-commit: #!/usr/bin/env bash
pnpm run check
Then enable it: chmod +x .githooks/pre-commit
git config core.hooksPath .githooks
This prevents committing changes which introduce type errors in test files (as well as any other
mistakes that the tsc compiler, linters and tests can catch). Failing to run tsc against test files caused a bit of trouble for
the @kensio/smartass
package. In many projects, type errors in test files are not a major problem if the tests still pass and the
linters do not complain. For the @kensio/smartass package, though, type narrowing and type assertions are what the package
is all about. The correctness of compile-time types is at least as important as the runtime test
assertions. Before setting up these compiler checks for test files, an earlier change inadvertently brought in
type errors in test files. The change had to be reverted later and redone. Now that the package
applies tsc compiler checks to test files, that situation cannot reoccur. View post:
Preventing type errors in TypeScript test files with tsc -p --noEmit |