← thecodex.expert · The Codex Family of Knowledge
TypeScript

Testing TypeScript

A test suite can pass completely while type errors sit right next to it — with the most common setups, test runners transpile and run your code without necessarily type-checking it first.

TypeScript 6.0 Jest / Vitest Last verified:
Canonical Definition

TypeScript itself has no test runner — Jest and the newer Vitest (built on Vite, popular for its speed) are the dominant choices in the ecosystem. Vitest always runs .ts test files by transpiling them on the fly (via esbuild) with no separate tsc build step; Jest depends on which transform it's configured with — babel-jest and ts-jest's isolatedModules mode also just transpile, but ts-jest's default mode actually type-checks as it compiles, so a real type error there does fail the run. Where transpile-only applies, there's a crucial, often-missed detail: transpiling and type-checking are separate concerns, so a test suite can run and pass completely while the same code has real type errors that a full tsc run would catch — running tsc --noEmit as a separate CI step is the common way to close that gap regardless of which mode you're in.

A basic test with Vitest

Vitest's API closely mirrors Jest's, and Vitest runs .ts files directly — no separate compile step needed before running tests. (This page uses Vitest for its examples; Jest can be set up to behave the same way, but only with a transpile-only transform — its own default, ts-jest, type-checks as it goes.)

TypeScriptmath.test.ts
import { describe, it, expect } from "vitest";
import { add } from "./math";

describe("add", () => {
    it("adds two numbers", () => {
        expect(add(2, 3)).toBe(5);
    });
});

Type-checking is a SEPARATE step from running tests

This is the detail newcomers most often miss: with Vitest (and with Jest configured for speed via babel-jest or ts-jest's isolatedModules), a test suite can pass in full while the surrounding code has genuine type errors, since the runner only transpiles (strips types) rather than fully type-checking each file in the context of the whole project. Running tsc --noEmit as its own CI step closes this gap — and is worth doing even with ts-jest's type-checking default, since ts-jest only sees the files it actually compiles, not necessarily your whole project.

TypeScriptpackage.json
{
  "scripts": {
    "test": "vitest run",
    "typecheck": "tsc --noEmit",
    "ci": "npm run typecheck && npm run test"
  }
}

Typing mocks and test doubles

Type-safe mocking keeps a mocked dependency's shape in sync with the real one — if the real function's signature changes, a typed mock that no longer matches becomes a visible compile error, rather than a silent runtime mismatch discovered only when the test happens to fail.

TypeScriptuser_service.test.ts
import { vi, it, expect } from "vitest";

interface UserApi {
    getUser(id: number): Promise<{ name: string }>;
}

const mockApi: UserApi = {
    getUser: vi.fn().mockResolvedValue({ name: "Alice" }),   // must match UserApi's shape
};

it("fetches a user", async () => {
    const user = await mockApi.getUser(1);
    expect(user.name).toBe("Alice");
});

Sources

1
Vitest documentation, vitest.dev, and Jest documentation, jestjs.io, "Getting Started with TypeScript".