Testing#
Test runners are the JavaScript surface (see Testing); TypeScript adds typed mocks, typed fixtures, and type-only tests that assert on structures at compile time. The 2026 default is vitest, which runs TypeScript directly without a build step.
This page covers the TypeScript-specific testing patterns.
vitest with TypeScript#
vitest runs .test.ts files directly; no tsc pass
needed at test time.
$ npm install --save-dev vitest
import {describe, it, expect} from "vitest";
import {trim} from "./string-helpers.js";
describe("trim", () => {
it("removes trailing whitespace", () => {
expect(trim("hi ")).toBe("hi");
});
it("throws on null", () => {
expect(() => trim(null as unknown as string)).toThrow(TypeError);
});
});
For type-checked tests in CI, run tsc --noEmit alongside
vitest run.
Typed mocks#
vi.fn() accepts a generic for the function signature; the
mock then enforces the same structure.
import {vi} from "vitest";
const onSend = vi.fn<(msg: string) => void>();
onSend("hi"); // ok
onSend(42); // type error
expect(onSend).toHaveBeenCalledWith("hi");
For full-module mocks, vi.mock with a typed factory
preserves the export types.
vi.mock("./network.js", (): typeof import("./network.js") => ({
fetchUser: vi.fn().mockResolvedValue({id: "1", name: "rk"}),
}));
Type-only tests#
The operator catches type regressions at compile time with
expect-type (or tsd).
$ npm install --save-dev expect-type
import {expectTypeOf} from "expect-type";
expectTypeOf<User>().toMatchTypeOf<{id: string; name: string}>();
expectTypeOf<typeof load>().returns.toEqualTypeOf<Promise<User>>();
The assertions evaluate at compile time; a wrong type fails the
tsc pass.
Fixtures with types#
vitest’s test.extend produces a typed test function with
extra fixtures injected.
import {test as base, expect} from "vitest";
type Fixtures = {server: Server; client: Client};
const test = base.extend<Fixtures>({
server: async ({}, use) => {
const s = await Server.start();
await use(s);
await s.stop();
},
client: async ({server}, use) => {
await use(new Client(server.url));
},
});
test("ping", async ({server, client}) => {
expect(await client.ping()).toBe("pong");
});
Snapshots#
.toMatchSnapshot() writes the value on first run and
compares on subsequent runs. Inline snapshots
(.toMatchInlineSnapshot) live in the test source.
it("renders the report", () => {
expect(render(data)).toMatchInlineSnapshot();
});
Coverage#
@vitest/coverage-v8 produces V8 coverage. Configure in
vitest.config.ts.
// vitest.config.ts
import {defineConfig} from "vitest/config";
export default defineConfig({
test: {
coverage: {
provider: "v8",
reporter: ["text", "html"],
},
},
});
$ npx vitest run --coverage
Playwright (e2e, typed)#
End-to-end tests in TypeScript. The page and locator APIs are fully typed.
import {test, expect, type Page} from "@playwright/test";
async function login(page: Page, user: string, pass: string) {
await page.goto("/login");
await page.getByLabel("Username").fill(user);
await page.getByLabel("Password").fill(pass);
await page.getByRole("button", {name: "Sign in"}).click();
}
test("dashboard after login", async ({page}) => {
await login(page, "op", "***");
await expect(page).toHaveURL("/dashboard");
});
node:test with TypeScript#
Node’s stdlib runner works with tsx (or --loader).
$ npx tsx --test test/
import {test} from "node:test";
import assert from "node:assert/strict";
import {trim} from "../src/string-helpers.js";
test("trim", () => {
assert.equal(trim("hi "), "hi");
});