| |

TypeScript 93 ๐Ÿ”ท TypeScript with Testing โ€” Jest and Vitest

Writing tests in TypeScript is not the same as writing them in JavaScript. The type system changes what you can assert, how you mock dependencies, and what happens when the test fails. A test that compiles is not necessarily a test that runs. A mock that satisfies the compiler may not satisfy the runtime. Jest and Vitest are the two test runners that dominate the TypeScript ecosystem, and they approach these problems differently.

Jest is the older framework. It was built for JavaScript, and TypeScript support arrived through a transformer โ€” ts-jest or Babel’s TypeScript preset. The setup works, but it adds a configuration layer that duplicates what your build tool is already doing. Vitest was built for Vite, and Vite already understands TypeScript. Vitest reuses the same transform pipeline, so there is no separate configuration for tests. The API is nearly identical โ€” describe, it, expect, vi.fn() instead of jest.fn() โ€” which makes migration mostly mechanical .

Key point: Jest and Vitest both type-check your tests only if you ask them to. Jest relies on ts-jest to transform and optionally type-check. Vitest uses Vite’s transform pipeline and separates runtime tests from type tests. Vitest’s type-checking mode runs tsc or vue-tsc and reports type errors as test failures, but only for files that match the typecheck.include pattern . Neither runner validates your types by default. The compiler is still the compiler.


Why testing frameworks matter for TypeScript

A TypeScript test has three layers: the test code itself, the code under test, and the types that connect them. The framework determines how the first two are compiled and executed. The types are checked separately.

The transform problem. Jest cannot execute TypeScript directly. It needs a transformer โ€” ts-jest, babel-jest with @babel/preset-typescript, or @swc/jest โ€” to convert .ts files to JavaScript before running them. This transformer is configured separately from your application’s build. If the transformer and the build disagree about which TypeScript features are enabled, the test and the application may behave differently . Vitest avoids this by reusing Vite’s transform. The same plugins, the same tsconfig.json, the same settings that build your application also run your tests .

The type-checking problem. TypeScript types are erased before execution. A test that passes a string where a number is expected will compile if you use as any, and it will run. The type error never reaches the runtime. To catch type errors in tests, you need a separate check. Vitest provides vitest typecheck (or vitest --typecheck) which runs the TypeScript compiler against .test-d.ts files and reports type mismatches as test failures . Jest does not have a built-in equivalent. The tsc --noEmit command runs separately.

The mocking problem. TypeScript’s type system and Jest’s mock system do not naturally align. jest.fn() returns a mock whose type is inferred from the implementation, or any if no implementation is given. To type a mock properly, you use jest.Mocked<T> or jest.MockedFunction<T>. Vitest has the same pattern with vi.fn() and Mocked<T> from vitest . The type definitions differ slightly, but the concept is the same.

The trade-off. Jest is mature, widely used, and has years of documentation and community plugins. Vitest is newer, faster in Vite projects, and has a more modern architecture. For a project already using Vite, Vitest is the natural choice. For a project using Jest successfully, migration is optional. The API compatibility means the cost of switching is low if you ever need to .


a. Setting Up TypeScript Tests

The setup for Jest and Vitest differs in how the TypeScript transform is configured.

Jest with ts-jest requires a transformer entry in jest.config.js. The ts-jest preset reads tsconfig.json and applies the settings to test files. The isolatedModules option speeds up transformation by skipping type checking, which is then done separately by tsc .

// jest.config.ts
import type { Config } from 'jest';

const config: Config = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['<rootDir>/src'],
  testMatch: ['**/*.test.ts'],
  transform: {
    '^.+\\.tsx?$': ['ts-jest', { isolatedModules: true }],
  },
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },
};

export default config;

The isolatedModules: true setting tells ts-jest to transform each file independently without type checking. This is faster but means type errors in tests are not reported by Jest. The tsc --noEmit command is still required to catch them.

Vitest needs no transformer configuration. It reads vite.config.ts and applies the same plugins. The test configuration lives in a test block within the Vite config, or in a separate vitest.config.ts that extends it .

// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    include: ['src/**/*.test.ts'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
    },
  },
});

The globals: true setting makes describe, it, and expect available without importing them, mimicking Jest’s default behavior. Vitest does not enable globals by default; the setting must be turned on explicitly. With globals disabled, tests import from vitest: import { describe, it, expect } from 'vitest' .

For TypeScript to recognize the globals, the tsconfig.json must include "types": ["vitest/globals"]. Without this, the compiler reports Cannot find name 'describe' in test files . The same applies to Jest: "types": ["jest"] is needed if globals are used and @types/jest is not auto-discovered.


b. Typed Mocks and Spies

The mock system is where TypeScript’s type system and the test runner’s runtime meet. A mock replaces a real dependency, and TypeScript must know what the replacement is allowed to do.

Jest’s jest.fn() creates a mock function. Without type arguments, the return type is jest.Mock<any, any>. The jest.Mocked<T> utility type wraps an object so that every method is a mock . The jest.MockedFunction<T> utility type does the same for a single function.

import { jest } from '@jest/globals';

interface UserService {
  getUser(id: string): Promise<User>;
  saveUser(user: User): Promise<void>;
}

const mockUserService: jest.Mocked<UserService> = {
  getUser: jest.fn(),
  saveUser: jest.fn(),
};

mockUserService.getUser.mockResolvedValue({ id: '1', name: 'Alice' });

The jest.Mocked<UserService> type ensures that getUser and saveUser are both mock functions. The mockResolvedValue method is available on getUser because jest.fn() returns a mock that knows the return type is Promise<User>.

Vitest’s vi.fn() follows the same pattern. The Mocked<T> type from vitest wraps an object. The MockedFunction<T> type wraps a function .

import { vi, type Mocked } from 'vitest';

interface UserService {
  getUser(id: string): Promise<User>;
  saveUser(user: User): Promise<void>;
}

const mockUserService: Mocked<UserService> = {
  getUser: vi.fn(),
  saveUser: vi.fn(),
};

mockUserService.getUser.mockResolvedValue({ id: '1', name: 'Alice' });

The difference is the namespace: jest becomes vi, and jest.Mocked becomes Mocked. The rest is identical. The codemod for migrating from Jest to Vitest handles these renames automatically, though vi.hoisted() is required for mocks that reference variables in their factory functions .

The hoisting trap. Both Jest and Vitest hoist mock() calls to the top of the file. This means the factory function cannot reference variables declared later in the file. In Jest, this works because the module registry is set up differently. In Vitest, the factory function is evaluated in a different scope, and referencing outer variables produces undefined errors. The fix is vi.hoisted(), which explicitly creates values that are available in the hoisted scope .

// โŒ Fails in Vitest
const mockUser = { id: '1' };
vi.mock('./user-service', () => ({
  getUser: () => mockUser, // mockUser is undefined at hoist time
}));

// โœ… Works in Vitest
const mockUser = vi.hoisted(() => ({ id: '1' }));
vi.mock('./user-service', () => ({
  getUser: () => mockUser,
}));

This is the single most common migration failure. The Vitest documentation states it plainly: roughly 40% of vi.mock() calls with factory functions need vi.hoisted() .


c. Type Testing with Vitest

Vitest has a feature that Jest does not: built-in type testing. You can write tests that assert on types rather than values, and Vitest runs them through tsc as part of the test suite .

Type tests live in .test-d.ts files. They use expectTypeOf or assertType from vitest. The assertions are checked by the TypeScript compiler, not by the runtime.

// user.test-d.ts
import { expectTypeOf, assertType } from 'vitest';
import { createUser } from './user';

test('createUser returns a User', () => {
  expectTypeOf(createUser).returns.toEqualTypeOf<User>();
});

test('createUser rejects invalid input', () => {
  // @ts-expect-error name is required
  assertType(createUser({}));
});

The expectTypeOf API provides matchers like toBeString(), toBeNumber(), toEqualTypeOf<T>(), and toExtend<T>(). When a type assertion fails, Vitest prints the type mismatch with the expected and actual types .

The @ts-expect-error directive is the standard way to assert that a type error should occur. If the line compiles without error, @ts-expect-error itself becomes an error, indicating that the type test failed.

Type tests are not run by the default vitest command. They require vitest typecheck or vitest --typecheck. The typecheck.include configuration option controls which files are treated as type tests. Since Vitest 2.1, runtime tests and type tests are reported as separate entries if they overlap .

The Jest alternative. Jest does not have built-in type testing. The equivalent is to write a separate TypeScript file that uses type assertions and run tsc --noEmit against it. The expect-type library provides expectTypeOf outside of Vitest, but it must be integrated into the build manually. Vitest’s advantage is that type tests and runtime tests share the same runner and the same report.


Complete Example Session

This session builds a TypeScript project with Jest configuration, Vitest configuration, typed mocks, and a type test.

// ============================================
// PART 1: THE CODE UNDER TEST
// ============================================

// src/calculator.ts
export type Operation = 'add' | 'subtract' | 'multiply' | 'divide';

export interface CalculatorInput {
  operation: Operation;
  a: number;
  b: number;
}

export interface CalculatorResult {
  operation: Operation;
  result: number;
}

export function calculate(input: CalculatorInput): CalculatorResult {
  let result: number;

  switch (input.operation) {
    case 'add':
      result = input.a + input.b;
      break;
    case 'subtract':
      result = input.a - input.b;
      break;
    case 'multiply':
      result = input.a * input.b;
      break;
    case 'divide':
      if (input.b === 0) {
        throw new Error('Division by zero is not allowed');
      }
      result = input.a / input.b;
      break;
  }

  return { operation: input.operation, result };
}

// ============================================
// PART 2: THE JEST CONFIGURATION
// ============================================

// jest.config.ts
import type { Config } from 'jest';

const config: Config = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['<rootDir>/src'],
  testMatch: ['**/*.test.ts'],
  transform: {
    '^.+\\.tsx?$': ['ts-jest', { isolatedModules: true }],
  },
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },
};

export default config;

// ============================================
// PART 3: THE VITEST CONFIGURATION
// ============================================

// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    include: ['src/**/*.test.ts'],
    typecheck: {
      include: ['src/**/*.test-d.ts'],
      tsconfig: './tsconfig.json',
    },
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
    },
  },
});

// ============================================
// PART 4: THE JEST TEST WITH TYPED MOCKS
// ============================================

// src/calculator.test.ts (Jest)
import { jest, describe, it, expect, beforeEach } from '@jest/globals';
import { calculate } from './calculator';

describe('calculate', () => {
  it('adds two numbers', () => {
    const result = calculate({ operation: 'add', a: 1, b: 2 });
    expect(result).toEqual({ operation: 'add', result: 3 });
  });

  it('throws on division by zero', () => {
    expect(() => calculate({ operation: 'divide', a: 1, b: 0 }))
      .toThrow('Division by zero is not allowed');
  });
});

// ============================================
// PART 5: THE VITEST TEST WITH TYPED MOCKS
// ============================================

// src/calculator.test.ts (Vitest)
import { describe, it, expect } from 'vitest';
import { calculate } from './calculator';

describe('calculate', () => {
  it('adds two numbers', () => {
    const result = calculate({ operation: 'add', a: 1, b: 2 });
    expect(result).toEqual({ operation: 'add', result: 3 });
  });

  it('throws on division by zero', () => {
    expect(() => calculate({ operation: 'divide', a: 1, b: 0 }))
      .toThrow('Division by zero is not allowed');
  });
});

// ============================================
// PART 6: THE TYPED MOCK WITH VI
// ============================================

// src/user-service.test.ts
import { describe, it, expect, vi, type Mocked } from 'vitest';

interface Database {
  insert(collection: string, data: unknown): Promise<number>;
  findById(collection: string, id: number): Promise<unknown>;
}

class UserService {
  constructor(private db: Database) {}

  async createUser(data: unknown): Promise<unknown> {
    const id = await this.db.insert('users', data);
    return { id, ...data as object };
  }
}

describe('UserService', () => {
  it('inserts user data', async () => {
    const mockDb: Mocked<Database> = {
      insert: vi.fn(),
      findById: vi.fn(),
    };
    mockDb.insert.mockResolvedValue(123);

    const service = new UserService(mockDb);
    const result = await service.createUser({ name: 'Alice' });

    expect(mockDb.insert).toHaveBeenCalledWith('users', { name: 'Alice' });
    expect(result).toEqual({ id: 123, name: 'Alice' });
  });
});

// ============================================
// PART 7: THE TYPE TEST
// ============================================

// src/calculator.test-d.ts
import { expectTypeOf, assertType } from 'vitest';
import { calculate, CalculatorInput, CalculatorResult } from './calculator';

test('calculate returns CalculatorResult', () => {
  expectTypeOf(calculate).returns.toEqualTypeOf<CalculatorResult>();
});

test('calculate accepts valid input', () => {
  const input: CalculatorInput = { operation: 'add', a: 1, b: 2 };
  assertType<CalculatorResult>(calculate(input));
});

test('calculate rejects invalid operation', () => {
  // @ts-expect-error 'power' is not a valid Operation
  calculate({ operation: 'power', a: 1, b: 2 });
});

// ============================================
// PART 8: THE MOCK HOISTING FIX
// ============================================

// โŒ Fails in Vitest
const mockUser = { id: '1', name: 'Alice' };
vi.mock('./user-service', () => ({
  getUser: () => mockUser,
}));

// โœ… Works in Vitest
const mockUser = vi.hoisted(() => ({ id: '1', name: 'Alice' }));
vi.mock('./user-service', () => ({
  getUser: () => mockUser,
}));

// ============================================
// PART 9: THE TSCONFIG FOR TESTS
// ============================================

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "types": ["vitest/globals"],
    "lib": ["ES2022"]
  },
  "include": ["src/**/*.ts"]
}

// For Jest, use:
// "types": ["jest"]

// ============================================
// PART 10: THE PACKAGE SCRIPTS
// ============================================

// package.json (Vitest)
{
  "scripts": {
    "test": "vitest",
    "test:run": "vitest run",
    "test:ui": "vitest --ui",
    "test:coverage": "vitest run --coverage",
    "typecheck": "vitest typecheck",
    "typecheck:tsc": "tsc --noEmit"
  }
}

// package.json (Jest)
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage",
    "typecheck": "tsc --noEmit"
  }
}

The ten parts cover the code under test, the Jest configuration, the Vitest configuration, the Jest test, the Vitest test, the typed mock, the type test, the mock hoisting fix, the tsconfig.json, and the package scripts.


Quick Reference

The Framework Comparison

FeatureJestVitest
Transformts-jest, Babel, SWCVite (esbuild)
Native ESMPartial, needs configYes
Native TypeScriptNeeds transformerYes
Mock namespacejestvi
Globals defaultEnabledDisabled
Type testingExternal (tsc)Built-in (vitest typecheck)
Coverage providerIstanbulv8 (default), Istanbul
UI modeThird-partyBuilt-in (vitest --ui)
Watch modeAvailableDefault, HMR-based

The Mock Utilities

JestVitestPurpose
jest.fn()vi.fn()Create mock function
jest.spyOn()vi.spyOn()Spy on method
jest.mock()vi.mock()Mock module
jest.requireActual()vi.importActual()Import original
jest.Mocked<T>Mocked<T>Typed mock object
jest.MockedFunction<T>MockedFunction<T>Typed mock function
jest.useFakeTimers()vi.useFakeTimers()Fake timers

The Type Testing API

MatcherPurpose
expectTypeOf(x).toBeString()Assert type is string
expectTypeOf(x).toBeNumber()Assert type is number
expectTypeOf(fn).returns.toEqualTypeOf<T>()Assert return type
expectTypeOf(x).parameter(0).toExtend<T>()Assert parameter type
assertType<T>(value)Assert value is T
@ts-expect-errorAssert that a type error occurs

The Migration Gotchas

GotchaFix
vi.mock() factory references outer variableUse vi.hoisted()
jest.fn() not renamedCodemod jest.* โ†’ vi.*
Globals not availableEnable globals: true or import from vitest
@testing-library/jest-dom missingUse @testing-library/jest-dom/vitest
moduleNameMapper regex failsUse Vite resolve.alias
done() callbackRefactor to async/await

Best Practices

โœ… Do This:

// Use typed mocks with Mocked<T>
const mockService: Mocked<Service> = { method: vi.fn() };       // โœ…
// Use vi.hoisted for factory references
const mock = vi.hoisted(() => ({ value: 1 }));                  // โœ…
// Add type testing for critical APIs
expectTypeOf(calculate).returns.toEqualTypeOf<CalculatorResult>(); // โœ…
// Run tsc --noEmit alongside tests
"typecheck": "tsc --noEmit"                                     // โœ…
// Enable globals and add types to tsconfig
"globals": true, "types": ["vitest/globals"]                    // โœ…

โŒ Don’t Do This:

// Don't use jest.* in Vitest
jest.fn()  // use vi.fn()                                        // โŒ
// Don't reference outer variables in vi.mock factory
const mock = { id: 1 };
vi.mock('./mod', () => ({ get: () => mock }));  // undefined     // โŒ
// Don't rely on Jest globals in Vitest without enabling
describe('test', () => {});  // fails without globals             // โŒ
// Don't assume type errors are caught by test runner
// They are not checked at runtime.                               // โŒ

Common Pitfalls

PitfallWhy It HappensFix
Cannot find name 'describe'Globals not enabled or types not configuredAdd globals: true and "types": ["vitest/globals"]
Mock factory returns undefinedVitest hoisting scopingUse vi.hoisted()
Type test not running.test-d.ts not in typecheck.includeAdd to config
Mock type is anyNo type annotation on vi.fn()Use Mocked<T> or MockedFunction<T>
Coverage missingProvider not installedInstall @vitest/coverage-v8 or @vitest/coverage-istanbul
Jest slow on ESMTransformer overheadUse Vitest or @swc/jest

Real-World Examples

1. Jest Configuration

const config: Config = { preset: 'ts-jest', testEnvironment: 'node' };

2. Vitest Configuration

export default defineConfig({ test: { globals: true, environment: 'node' } });

3. Typed Mock (Vitest)

const mockDb: Mocked<Database> = { insert: vi.fn(), findById: vi.fn() };

4. Mock with Resolved Value

mockDb.insert.mockResolvedValue(123);

5. Type Test

expectTypeOf(calculate).returns.toEqualTypeOf<CalculatorResult>();

6. Assert Type Error

// @ts-expect-error invalid operation
calculate({ operation: 'power', a: 1, b: 2 });

7. Hoisted Mock

const mock = vi.hoisted(() => ({ id: '1' }));
vi.mock('./service', () => ({ getUser: () => mock }));

8. Spy on Method

const spy = vi.spyOn(service, 'getUser').mockResolvedValue(user);

9. Coverage Command

vitest run --coverage

10. Type Check Command

vitest typecheck

Visual

The Jest Transform Pipeline

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  JEST + ts-jest                              โ”‚
โ”‚                                              โ”‚
โ”‚  test.ts โ”€โ”€> ts-jest โ”€โ”€> JS โ”€โ”€> Jest runtime โ”‚
โ”‚                    โ”‚                         โ”‚
โ”‚                    โ””โ”€ type checking          โ”‚
โ”‚                       (optional)             โ”‚
โ”‚                                              โ”‚
โ”‚  Configuration:                              โ”‚
โ”‚  jest.config.ts + tsconfig.json              โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The Vitest Transform Pipeline

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  VITEST + VITE                               โ”‚
โ”‚                                              โ”‚
โ”‚  test.ts โ”€โ”€> Vite (esbuild) โ”€โ”€> JS โ”€โ”€> run   โ”‚
โ”‚                    โ”‚                         โ”‚
โ”‚                    โ””โ”€ SAME pipeline as       โ”‚
โ”‚                       your app build         โ”‚
โ”‚                                              โ”‚
โ”‚  Configuration:                              โ”‚
โ”‚  vitest.config.ts (extends vite.config.ts)   โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The Mock Hoisting Difference

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  JEST HOISTING                               โ”‚
โ”‚                                              โ”‚
โ”‚  const mock = { id: 1 };                     โ”‚
โ”‚  jest.mock('./mod', () => ({                 โ”‚
โ”‚    get: () => mock  // works                 โ”‚
โ”‚  }));                                        โ”‚
โ”‚                                              โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  VITEST HOISTING                             โ”‚
โ”‚                                              โ”‚
โ”‚  const mock = { id: 1 };                     โ”‚
โ”‚  vi.mock('./mod', () => ({                   โ”‚
โ”‚    get: () => mock  // โŒ undefined           โ”‚
โ”‚  }));                                        โ”‚
โ”‚                                              โ”‚
โ”‚  โœ… Fix:                                     โ”‚
โ”‚  const mock = vi.hoisted(() => ({ id: 1 })); โ”‚
โ”‚  vi.mock('./mod', () => ({                   โ”‚
โ”‚    get: () => mock                           โ”‚
โ”‚  }));                                        โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Type Testing Flow

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  VITEST TYPE TESTING                         โ”‚
โ”‚                                              โ”‚
โ”‚  calculator.test-d.ts                        โ”‚
โ”‚    โ”‚                                         โ”‚
โ”‚    โ–ผ                                         โ”‚
โ”‚  vitest typecheck                            โ”‚
โ”‚    โ”‚                                         โ”‚
โ”‚    โ–ผ                                         โ”‚
โ”‚  tsc / vue-tsc                               โ”‚
โ”‚    โ”‚                                         โ”‚
โ”‚    โ–ผ                                         โ”‚
โ”‚  Type errors reported as test failures       โ”‚
โ”‚                                              โ”‚
โ”‚  The file is NOT executed.                   โ”‚
โ”‚  It is statically analyzed.                  โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ItemValue
Jest transformts-jest, Babel, or SWC
Vitest transformVite (esbuild)
Mock namespacejest vs vi
Typed mockjest.Mocked<T> vs Mocked<T>
Type testingVitest built-in (vitest typecheck)
Hoisting fixvi.hoisted()
Globals defaultJest: enabled, Vitest: disabled
CoverageIstanbul vs v8
MigrationCodemod jest.* โ†’ vi.*

Key takeaways:

  • Jest requires a transformer for TypeScript; Vitest reuses Vite’s pipeline. ts-jest, Babel, or SWC convert .ts files for Jest. Vitest uses the same esbuild transform that builds the application, eliminating configuration duplication .
  • The mock API is nearly identical, but the namespace changes. jest.fn() becomes vi.fn(), jest.mock() becomes vi.mock(), and jest.Mocked<T> becomes Mocked<T>. The codemod handles the mechanical renames, but vi.hoisted() is required for factory functions that reference outer variables .
  • Vitest has built-in type testing. .test-d.ts files are checked by tsc and reported as test failures. expectTypeOf and assertType provide matchers for asserting on types. Jest does not have an equivalent built-in feature .
  • Globals are disabled by default in Vitest. Jest enables describe, it, and expect globally. Vitest requires globals: true in the configuration and "types": ["vitest/globals"] in tsconfig.json to match Jest’s behavior .
  • Typed mocks use Mocked<T> to wrap the mock object. Without the type annotation, vi.fn() returns a mock whose type is inferred from the implementation or any. The Mocked<T> utility type ensures every method is a mock with the correct signature .
  • The hoisting difference is the most common migration failure. Vitest’s vi.mock() factory functions are evaluated in a different scope than Jest’s. Referencing outer variables produces undefined. vi.hoisted() explicitly creates values in the hoisted scope .
  • Neither runner type-checks by default. Jest with isolatedModules skips type checking during transform. Vitest does not type-check runtime tests. The tsc --noEmit command is still required to verify types, unless you use Vitest’s typecheck mode for type tests.

Remember: Jest and Vitest both run TypeScript tests, but they get there differently. Jest transforms TypeScript into JavaScript and runs the result. Vitest reuses the Vite pipeline that already understands TypeScript. The API is so similar that migration is mostly a find-and-replace, with one significant exception: vi.hoisted() for mock factories. Vitest adds type testing as a first-class feature. Jest relies on external tooling. For a Vite project, Vitest is the natural choice. For a Jest project, migration is optional. Either way, the types are checked by the compiler, not the runner.


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!