Jest, when configured for TypeScript, doesn’t actually run your TypeScript code directly. Instead, ts-jest acts as a transformer, converting your .ts files into JavaScript on the fly before Jest executes them. This means Jest itself only ever sees and runs JavaScript, making the setup surprisingly straightforward once you understand that intermediary step.
Let’s see ts-jest in action with a simple example.
Project Structure:
my-ts-project/
├── src/
│ └── math.ts
├── __tests__/
│ └── math.test.ts
├── package.json
├── jest.config.js
└── tsconfig.json
src/math.ts:
export function add(a: number, b: number): number {
return a + b;
}
__tests__/math.test.ts:
import { add } from '../src/math';
test('adds 1 + 2 to equal 3', () => {
expect(add(1, 2)).toBe(3);
});
tsconfig.json:
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}
package.json:
{
"name": "my-ts-project",
"version": "1.0.0",
"devDependencies": {
"@types/jest": "^29.5.12",
"jest": "^29.7.0",
"ts-jest": "^29.1.2",
"typescript": "^5.3.3"
},
"scripts": {
"test": "jest"
}
}
jest.config.js:
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
When you run npm test, Jest invokes ts-jest. ts-jest reads math.test.ts, sees the import statement, finds math.ts, compiles math.ts to JavaScript (e.g., function add(a, b) { return a + b; }), and then provides that JavaScript to Jest for execution. Jest then runs the test against the compiled JavaScript.
The core problem ts-jest solves is bridging the gap between Jest’s JavaScript execution environment and your TypeScript source files. Without it, Jest would try to interpret .ts files as JavaScript, leading to syntax errors. ts-jest ensures your TypeScript code is correctly transformed into a format Jest understands before Jest attempts to run it.
The jest.config.js is where the magic happens. The preset: 'ts-jest' line is the most crucial part. It tells Jest to load the ts-jest configuration, which automatically sets up the necessary transformers and module mappings. This preset is essentially a shorthand for configuring ts-jest’s defaults.
Inside tsconfig.json, you define how your TypeScript should be compiled. Jest will respect these settings when ts-jest performs the transformation. Common options like target (e.g., es2016 or esnext) and module (e.g., commonjs) directly influence the output JavaScript that Jest will execute. esModuleInterop: true is vital for allowing common JavaScript module interoperability, which is standard in most modern Node.js projects and often required for Jest’s module resolution.
The testEnvironment: 'node' in jest.config.js specifies that your tests should run in a Node.js environment, which is typical for backend or CLI applications. If you were testing a frontend React app, you might use 'jsdom'.
A common pitfall is forgetting to install the necessary dependencies: @types/jest, jest, ts-jest, and typescript. Without these, your project won’t have the tools to compile TypeScript or run Jest tests.
The jest.config.js is also where you’d typically configure Jest’s behavior for your project. For instance, you might want to tell Jest where to find your test files using roots: ['./src', './__tests__'] or specify which files to ignore with testPathIgnorePatterns: ['/node_modules/'].
The preset: 'ts-jest' is a convenience. You could achieve the same result by manually configuring transform and moduleNameMapper in jest.config.js, but the preset handles all of that for you. For example, a manual setup might look something like this (though the preset is preferred):
// jest.config.js (manual setup example - preset is better!)
module.exports = {
transform: {
'^.+\\.(ts|tsx)?$': 'ts-jest',
},
moduleFileExtensions: ['ts', 'js', 'json'],
testEnvironment: 'node',
};
The moduleFileExtensions tells Jest which file extensions to consider when looking for test files. Including 'ts' ensures that Jest can find and process your TypeScript test files directly.
When you’re dealing with complex Jest configurations, especially involving aliases or module mappings, ts-jest can sometimes interfere if not configured correctly. The isolatedModules: true compiler option in tsconfig.json is often recommended when using module transformers like ts-jest to prevent unexpected behavior related to module hoisting and compilation. However, ts-jest handles most of this automatically with its preset.
The skipLibCheck: true option in tsconfig.json is also a good practice for Jest projects. It tells the TypeScript compiler to skip checking type declarations for .d.ts files. This can significantly speed up your Jest runs, as Jest doesn’t need to fully type-check all your installed library definitions.
The next common challenge is integrating Jest with frontend frameworks, which often involves mocking module imports that are specific to the browser environment, or handling CSS/image imports.