ts-jest Guide: TypeScript + Jest Configuration, Path Aliases, and Mocking
ts-jest is a Jest transformer that lets you run TypeScript tests without a separate compile step. It integrates with your existing tsconfig, supports path aliases via moduleNameMapper, and provides typed mock utilities. This guide covers everything from initial setup to advanced configuration including monorepo path aliases, module mocking patterns, and performance tuning with isolatedModules.
Key Takeaways
preset: 'ts-jest' is the starting point. It registers the TypeScript transformer for .ts and .tsx files. You rarely need more than this plus a tsconfig reference.
Path aliases need moduleNameMapper. If your tsconfig uses paths (like @/components/*), Jest won't resolve them. Map each alias in jest.config.ts using regex patterns.
isolatedModules: true is a performance win. It disables cross-file type checking during transformation. Type errors are caught by tsc --noEmit separately. Large projects see 2–5x faster test execution.
jest.mock() must be at the top level. Calls inside describe or it blocks are hoisted by Babel but NOT by ts-jest. Put all jest.mock() calls at the module level.
Use jest.Mocked<T> for fully typed mock objects. It converts all methods on an interface to jest.MockedFunction, so every .mockReturnValue() call is type-checked.
What ts-jest Does
Node.js cannot run TypeScript files. Jest, at its core, runs JavaScript. ts-jest acts as a bridge: it intercepts .ts file imports, compiles them through the TypeScript compiler, and passes the resulting JavaScript to Jest.
This happens in memory, per-file, on demand — no separate tsc build step is required.
The alternative approaches:
- babel-jest with @babel/preset-typescript — faster, but strips types without checking them. Type errors don't fail tests.
- SWC (via
@swc/jest) — faster than ts-jest, same trade-off as Babel (no type checking) - ts-jest — slower compilation, but TypeScript errors cause test failures
If type safety in tests matters to you, ts-jest is the correct choice. If you need maximum speed and run tsc --noEmit separately in CI, SWC is a valid alternative.
Installation
npm install --save-dev jest ts-jest @types/jest typescriptFor TypeScript configuration file support (jest.config.ts):
npm install --save-dev ts-nodeBasic Configuration
jest.config.ts:
import type { Config } from 'jest';
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/*.test.ts', '**/*.spec.ts'],
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/**/*.test.ts',
],
};
export default config;tsconfig.json minimum for ts-jest:
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"strict": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"]
}The "module": "CommonJS" is important. Jest uses CommonJS by default. If you set "module": "ESNext" for your production code, create a separate tsconfig.test.json that overrides it to CommonJS.
Separate tsconfig for Tests
// tsconfig.test.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "CommonJS",
"types": ["jest", "node"]
},
"include": ["src/**/*.ts"]
}Point ts-jest to it:
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'node',
globals: {
'ts-jest': {
tsconfig: 'tsconfig.test.json',
},
},
};Path Aliases
If your tsconfig uses path mapping:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@utils/*": ["src/utils/*"]
}
}
}Jest will not resolve these automatically. You must mirror them in moduleNameMapper:
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'node',
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'^@components/(.*)$': '<rootDir>/src/components/$1',
'^@utils/(.*)$': '<rootDir>/src/utils/$1',
},
};The regex patterns must match exactly how you use the aliases in imports. Test your regex against real import paths — ^@/(.*)$ matches @/services/user but not @services/user.
Monorepo Path Aliases
In monorepos with Turborepo or Nx, each package has its own tsconfig paths. Use pathsToModuleNameMapper from ts-jest to generate the mapper automatically:
import { pathsToModuleNameMapper } from 'ts-jest';
import { compilerOptions } from './tsconfig.json';
const config: Config = {
preset: 'ts-jest',
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, {
prefix: '<rootDir>/',
}),
};Module Mocking
Auto-mocking a Module
// user-service.ts
export class UserService {
async getUser(id: string): Promise<User> {
return fetch(`/api/users/${id}`).then(r => r.json());
}
async deleteUser(id: string): Promise<void> {
await fetch(`/api/users/${id}`, { method: 'DELETE' });
}
}// user-controller.test.ts
import { UserService } from './user-service';
import { UserController } from './user-controller';
jest.mock('./user-service'); // Must be at module level
const MockUserService = UserService as jest.MockedClass<typeof UserService>;
describe('UserController', () => {
let controller: UserController;
beforeEach(() => {
MockUserService.mockClear();
controller = new UserController(new UserService());
});
it('returns the user from the service', async () => {
MockUserService.prototype.getUser.mockResolvedValue({
id: '1',
name: 'Alice',
email: 'alice@example.com',
});
const result = await controller.getUser('1');
expect(result.name).toBe('Alice');
});
});Using jest.Mocked<T> for Interfaces
When mocking against an interface (not a class), create a factory function:
import type { IEmailService } from './email-service';
function createMockEmailService(): jest.Mocked<IEmailService> {
return {
send: jest.fn(),
sendBulk: jest.fn(),
getDeliveryStatus: jest.fn(),
};
}
describe('NotificationService', () => {
it('sends email on user registration', async () => {
const emailService = createMockEmailService();
emailService.send.mockResolvedValue({ messageId: 'msg_123' });
const service = new NotificationService(emailService);
await service.onUserRegistered({ email: 'alice@example.com' });
expect(emailService.send).toHaveBeenCalledWith({
to: 'alice@example.com',
subject: 'Welcome to our service',
});
});
});Partial Mocks with jest.spyOn
For spying on individual methods without mocking the whole module:
import * as mathUtils from './math-utils';
it('calls roundToDecimal with correct precision', () => {
const spy = jest.spyOn(mathUtils, 'roundToDecimal').mockReturnValue(3.14);
const result = calculatePrice(10, 0.314);
expect(spy).toHaveBeenCalledWith(3.14, 2);
spy.mockRestore();
});Always call mockRestore() after spying — or use afterEach(() => jest.restoreAllMocks()).
Performance: isolatedModules
For projects with 200+ test files, ts-jest compilation can become slow. Enable isolatedModules to skip cross-file type checking during transformation:
globals: {
'ts-jest': {
isolatedModules: true,
tsconfig: 'tsconfig.test.json',
},
},This trades type-checking completeness for speed. The recommended workflow:
- Run
tsc --noEmitin CI to catch type errors across the project - Run
jest --isolatedModules(or via config) for fast test execution
They complement each other. Type checking and test execution become separate concerns.
Common Errors
"Cannot find module './foo' or its corresponding type declarations" The module exists but TypeScript can't find its types. Either install @types/foo or add a declaration file.
"SyntaxError: Cannot use import statement outside a module" Your code uses ES module syntax but Jest expects CommonJS. Add transformIgnorePatterns or switch to CommonJS in tsconfig.test.json.
"TypeError: Cannot read properties of undefined" Usually a mock setup issue — the mock isn't set up before the test runs, or jest.mock() was called inside a describe block instead of at module level.
"No tests found matching..." Check testMatch in your config. The default pattern **/__tests__/**/*.ts might not match your file structure. Use testMatch: ['**/*.test.ts'] to match files anywhere.
Running ts-jest
npx jest # run all tests
npx jest --watch <span class="hljs-comment"># interactive watch
npx jest --coverage <span class="hljs-comment"># with coverage report
npx jest --testPathPattern=user <span class="hljs-comment"># files matching 'user'
npx jest --testNamePattern=<span class="hljs-string">"getUser" <span class="hljs-comment"># tests matching 'getUser'
npx jest --verbose <span class="hljs-comment"># detailed output per testWhat ts-jest Doesn't Cover
ts-jest runs your code in a simulated Node.js environment. It can't test:
- Real browser behavior (use Playwright or Cypress for that)
- Network calls to external APIs (mock them or use integration tests)
- Database interactions (use an in-memory database or test containers)
- What your deployed application actually does
For monitoring your production TypeScript application, HelpMeTest runs automated tests against your live system 24/7. The free plan covers 10 tests — no infrastructure required.