ts-jest
ts-jest is a Jest transformer that compiles TypeScript test and source files on the fly, so you can run .ts and .tsx tests without a build step. What separates it from the other TypeScript transformers is that by default it runs the real TypeScript compiler with your tsconfig, so a type error in a test file fails the suite instead of being stripped and ignored. It also produces source maps, so stack traces and coverage point at your TypeScript lines rather than compiled JavaScript. You wire it up through Jest's transform option, usually via the createDefaultPreset helper the package exports, and configure it per transform entry with a tsconfig path, diagnostic filters, and Babel or ESM settings.
If you are on Jest and want type errors to break the build in the same command that runs your tests, ts-jest is the only mature option and it is maintained. If you only need TypeScript syntax stripped, use @swc/jest and run tsc --noEmit on the side, because that combination is faster and simpler to configure.
Use it if
- Your project already runs on Jest with a large suite, and switching runners would mean rewriting mocks, custom environments, reporters, and CI wiring for no user-visible gain
- You want type errors to fail the test run: a mock whose shape drifted from the real module, or a fixture missing a required field, gets caught in the same command that runs the tests
- You depend on TypeScript compiler behavior that Babel's type-stripping cannot reproduce, such as const enums, emitDecoratorMetadata, or custom AST transformers plugged in through astTransformers
- Your tsconfig defines path aliases and you want the same aliases in Jest without hand-maintaining moduleNameMapper, which pathsToModuleNameMapper generates for you
- You need a non-standard TypeScript build: the compiler option lets you point ts-jest at a patched or forked compiler package instead of typescript
- You are starting a project today with no Jest history. Vitest handles TypeScript and ESM natively with no transformer to configure, and the assertion API is close enough that the learning cost is small
- Test suite wall time is your pain. Type checking every file through the transform is the expensive part, and the standard fix is to turn it off with isolatedModules, which removes the exact feature you installed ts-jest for. At that point @swc/jest does the same transpile-only job faster
- You expect ts-jest to replace tsc --noEmit. It only checks files that a test actually pulls in, so unused files, unreferenced modules, and anything outside the test graph never get looked at. Most teams end up running both
- Your project is ESM. Getting it working means running Jest with NODE_OPTIONS=--experimental-vm-modules, adding a moduleNameMapper to strip .js from relative import specifiers, and dealing with a warning that module: Node16 or NodeNext is only supported when isolatedModules is on
- You want version numbers to mean something. The README states outright that the project does not use semantic versioning and that the major number tracks Jest's, so a caret range tells you nothing about breaking changes
- You are on TypeScript 7. The supported path is a side-by-side compiler setup documented separately, not installing typescript directly, and the declared peer range stops below 7
Setup reality
npm i -D ts-jest @types/jest on top of an existing jest and typescript install, then npx ts-jest config:init writes a jest.config.js that calls createDefaultPreset(). The preset field in jest config is the old way; the current shape is spreading createDefaultPreset().transform into your transform map, which is why older tutorials showing preset: 'ts-jest' look different from what the CLI generates. The peer dependency list is long (jest, typescript, @jest/transform, @jest/types, jest-util, babel-jest, @babel/core) but most entries are marked optional, so npm will not nag unless you actually use Babel. Jest 30 ships @jest/globals, yet plain global test and expect still need @types/jest or an explicit import, and forgetting that produces TS2304 errors that look like a ts-jest bug. If you use ESM the setup grows: extensionsToTreatAsEsm, useESM: true, the vm-modules Node flag, and a mapper for .js specifiers. Node 14.15, 16.10, 18, or 20 and up.
Patterns
Let the CLI write the Jest configgenerate-starter-config
npx ts-jest config:init
// writes jest.config.js:
const { createDefaultPreset } = require('ts-jest');
const tsJestTransformCfg = createDefaultPreset().transform;
/** @type {import('jest').Config} **/
module.exports = {
testEnvironment: 'node',
transform: { ...tsJestTransformCfg },
};This is the current recommended shape. It does not set preset: 'ts-jest', which is the older style you will still see in blog posts and in some generated projects.
Confirm type checking is actually ontype-errors-fail-tests
// src/a.test.ts
const bad: number = 'nope';
test('works', () => expect(1 + 1).toBe(2));
// jest output:
// ● Test suite failed to run
// src/a.test.ts:1:7 - error TS2322: Type 'string' is not assignable to type 'number'.Only files reachable from a test get compiled, so ts-jest is not a replacement for tsc --noEmit over the whole project. Dead modules and files no test imports are never checked.
Trade type checking for a much faster runtranspile-only-for-speed
// tsconfig.json
{
"compilerOptions": {
"isolatedModules": true
}
}
// package.json
{
"scripts": {
"test": "jest",
"typecheck": "tsc --noEmit"
}
}isolatedModules belongs in tsconfig now; the top-level ts-jest option of the same name is deprecated. With it on, ts-jest transpiles each file in isolation and type errors stop failing tests, so add a separate typecheck step or you lose the safety net entirely.
Run ESM testsesm-config
// jest.config.mjs
import { createDefaultEsmPreset } from 'ts-jest';
export default {
testEnvironment: 'node',
...createDefaultEsmPreset(),
moduleNameMapper: { '^(\\.{1,2}/.*)\\.js$': '$1' },
};
// run with:
// NODE_OPTIONS=--experimental-vm-modules npx jestWithout the vm-modules flag Jest fails on the first import. The moduleNameMapper strips the .js suffix that Node16 module resolution requires in source but that Jest cannot resolve to a .ts file. With module set to Node16 or NodeNext ts-jest also warns TS151002 until you set isolatedModules: true.
Reuse tsconfig paths as Jest module mappingspath-aliases
const { pathsToModuleNameMapper } = require('ts-jest');
const { compilerOptions } = require('./tsconfig.json');
module.exports = {
roots: ['<rootDir>'],
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, {
prefix: '<rootDir>/',
}),
};
// { '@app/*': ['src/*'] } becomes { '^@app/(.*)$': '<rootDir>/src/$1' }require of tsconfig.json only works if the file has no comments, which trips up projects using the commented tsconfig that editors generate. The prefix must line up with the baseUrl your paths are relative to.
Point the transform at a test-only tsconfigseparate-test-tsconfig
module.exports = {
testEnvironment: 'node',
transform: {
'^.+\\.tsx?$': [
'ts-jest',
{
tsconfig: '<rootDir>/tsconfig.spec.json',
},
],
},
};tsconfig also accepts an inline compilerOptions object if you only need to override a couple of flags. A separate file is the usual way to keep test-only types and looser settings out of the production build.
Ignore specific TypeScript error codesfilter-diagnostics
transform: {
'^.+\\.tsx?$': [
'ts-jest',
{
diagnostics: {
ignoreCodes: [151002],
exclude: ['**/*.fixture.ts'],
warnOnly: false,
},
},
],
},warnOnly: true downgrades every diagnostic to a console warning, which is a useful step during a migration but silently keeps broken types shipping. Prefer ignoreCodes for known-noisy codes over turning diagnostics off completely.
Send .js files through TypeScript as wellcompile-javascript-too
const { createJsWithTsPreset } = require('ts-jest');
module.exports = {
testEnvironment: 'node',
...createJsWithTsPreset(),
};
// transform: { '^.+\\.[tj]sx?$': ['ts-jest', {}] }This needs allowJs: true in tsconfig or every .js file errors. It is the right preset for a codebase mid-migration, but it also means type errors in legacy JavaScript start failing tests, so pair it with checkJs: false at first.
Keep Babel for JavaScript, ts-jest for TypeScripthybrid-babel-pipeline
const { createJsWithBabelPreset } = require('ts-jest');
module.exports = {
testEnvironment: 'node',
...createJsWithBabelPreset(),
};
// transform: {
// '^.+\\.jsx?$': 'babel-jest',
// '^.+\\.tsx?$': ['ts-jest', {}],
// }@babel/core and babel-jest are optional peers, so npm installs ts-jest without them and the failure only appears when a .js test file first hits the transform. Install both explicitly.
Get typed auto-mocks in TypeScript teststyped-mocks
import { fetchUser } from './api';
jest.mock('./api');
const mockedFetchUser = jest.mocked(fetchUser);
test('returns the user', async () => {
mockedFetchUser.mockResolvedValue({ id: 1, name: 'Ada' });
await expect(load(1)).resolves.toEqual({ id: 1, name: 'Ada' });
});jest.mocked is a types-only cast, so it does not mock anything by itself; jest.mock is still required. Because ts-jest type checks, mockResolvedValue with a wrong shape fails the suite, which is most of the value of running the checker in tests.
Hook a TypeScript AST transformer into the compilecustom-ast-transformer
transform: {
'^.+\\.tsx?$': [
'ts-jest',
{
astTransformers: {
before: ['ts-jest-mock-import-meta'],
afterDeclarations: [],
},
},
],
},This is the escape hatch for things the transpile-only transformers cannot do, such as rewriting import.meta for CommonJS tests. It only runs when the full compiler path is active, so it is a no-op once you switch to isolatedModules.
Write the Jest config itself in TypeScriptconfig-in-typescript
// jest.config.ts
import type { Config } from 'jest';
import { createDefaultPreset } from 'ts-jest';
const config: Config = {
testEnvironment: 'node',
...createDefaultPreset(),
collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts'],
};
export default config;Jest reads a .ts config only when ts-node or ts-jest is resolvable at startup, and it compiles that file with your root tsconfig, not the test one. A config with a type error stops Jest before any test runs.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| @swc/jest | npm | You want the fastest possible Jest transform for TypeScript and are fine type checking separately with tsc --noEmit |
| vitest | npm | You can change runners: TypeScript and ESM work out of the box and there is no transformer configuration to maintain |
| babel-jest | npm | Your build already runs Babel and you want the test pipeline to use exactly the same plugins and output |