Event date: April 5, 2026 — vitest-dev/vitest 3.0.5
On this page
Vitest’s recent typecheck work completes the effort of treating type-level assertions as first-class test citizens. The expectTypeOf API — previously usable but awkwardly stitched onto runtime tests — now runs inline inside any test() or it() block, with a dedicated --typecheck.only flag for type-only runs. For teams maintaining parallel tsd suites, the headline is simple: a single runner can now cover both runtime and type assertions without spawning a second tsc process per file.
- Tool: vitest with
--typechecksupport - Flag:
--typecheck(enable) and--typecheck.only(skip runtime) - Config key:
test.typecheck.enabled: trueinvitest.config.ts - Peer dep: a recent TypeScript version is recommended for full diagnostics
- Replacement scope: tsd’s
expectType,expectError,expectAssignableall map to inline equivalents
What the typecheck integration actually ships
The relevant change documented in vitest’s CHANGELOG is narrow but load-bearing: inline expectTypeOf calls written inside a normal it() block are collected by the same reporter that prints runtime failures. Previously you had to put type assertions in a *.test-d.ts file or run them through a separate typecheck task that reported with a different formatter and produced its own exit code. Now vitest run --typecheck surfaces type errors and runtime errors in one tree.
Concretely, a test like the one below runs as part of your normal suite. The assertion has no runtime component — it compiles or it doesn’t — but it appears in the reporter alongside the runtime expect() calls above it:
See also TypeScript testing guide.
import { expectTypeOf, it } from 'vitest'
it('parseQuery returns a discriminated union', () => {
const result = parseQuery('?page=2')
expect(result.ok).toBe(true)
expectTypeOf(parseQuery)
.returns.toEqualTypeOf<{ ok: true; value: Query } | { ok: false; error: string }>()
})
Without the inline integration, the expectTypeOf line would either require a dedicated *.test-d.ts file or silently produce no reporter output if the assertion failed at the type level. With the --typecheck runner, the assertion is wired into the same diagnostic pipeline as runtime expectations, and failures print with the offending line and a type diff.

The screenshot is from the Testing Types page of the Vitest handbook. The visible section is headed “Run typecheck” and shows the exact package.json snippet the Vitest team recommends: a test:types script that calls vitest --typecheck.only. The yellow callout on screen warns that expect-type has no documentation and suggests reading the source file at node_modules/expect-type/dist/index.d.ts when you hit an edge case — which is still the fastest way to see every method on the builder (toEqualTypeOf, toMatchTypeOf, parameter(n), returns, resolves, items, guards, asserts, not).
tsd vs inline expectTypeOf, head to head
Both libraries solve the same core problem — assert that an inferred TypeScript type matches an expected one — but they differ on where the assertions live, how they run, and what the failure output looks like. The most important differences are mechanical rather than philosophical.
tsd spawns its own TypeScript compiler with a pre-configured tsconfig and scans *.test-d.ts files. It exposes expectType, expectNotType, expectAssignable, expectNotAssignable, and expectError. Crucially, expectError works by asserting the body produces a compile error — if the code type-checks cleanly, the assertion fails. That’s a different mental model from runtime expect().toThrow(), and historically it has tripped up contributors who forget that removing the error also breaks the test.
Related: tests that catch real bugs.
Vitest’s expectTypeOf is backed by the expect-type library. It uses a builder pattern: expectTypeOf(value).toEqualTypeOf<T>(). There is no expectError — instead you use // @ts-expect-error comments, which TypeScript validates natively. The builder exposes the whole type structure, so you can write expectTypeOf(fn).parameter(0).toBeString() or expectTypeOf(promise).resolves.toEqualTypeOf<User>().
The practical trade-offs:
- Where tests live: tsd requires separate
*.test-d.tsfiles. InlineexpectTypeOflives in the same*.test.tsfile as your runtime tests, next to the function they cover. - Error surface: tsd’s
expectErroris coarse — any error in the block passes.@ts-expect-errorwith inline expectTypeOf requires the specific line to error, which catches regressions where the error moves. - Runner overhead: tsd runs its own compiler invocation. Vitest with
--typecheckreuses the projecttsconfig.jsonand runstsc --noEmitin a child process, but only once per file batch. - Config surface: tsd has its own
package.json“tsd” block with its owncompilerOptions. Inline expectTypeOf inherits your project config, which means there’s no risk of the two diverging.

The chart compares cold type-check time on a representative repository with a sizeable type-test suite. The left bar is npx tsd — one process per run, spinning up its own TypeScript program. The right bar is vitest run --typecheck.only, which reuses a single tsc --noEmit pass over the whole project. The delta is mostly program-construction cost: tsd re-reads and re-parses node_modules declaration files on every invocation, while Vitest amortises that across the entire suite. Warm re-runs narrow the gap, but cold CI runs — which is the case you actually care about for pipeline time — remain meaningfully faster with the inline approach.
Migrating a tsd suite to inline expectTypeOf
The mechanical migration is largely a sed job plus a config flip. First, enable the typecheck runner in vitest.config.ts:
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
typecheck: {
enabled: true,
tsconfig: './tsconfig.test.json',
include: ['**/*.{test,spec}.{ts,tsx}'],
},
},
})
Then rewrite the assertions. The mappings are straightforward:
I wrote about Jest-to-Vitest migration guide if you want to dig deeper.
// tsd (before)
import { expectType, expectError, expectAssignable } from 'tsd'
expectType<string>(greet('alex'))
expectAssignable<{ id: number }>({ id: 1, extra: 'ok' })
expectError(greet(42))
// vitest inline (after)
import { expectTypeOf } from 'vitest'
expectTypeOf(greet('alex')).toEqualTypeOf<string>()
expectTypeOf<{ id: 1; extra: 'ok' }>().toMatchTypeOf<{ id: number }>()
// @ts-expect-error greet expects string, not number
greet(42)
The expectError → @ts-expect-error conversion is the one that catches people. A single @ts-expect-error comment suppresses only the immediately following line. If your original tsd block contained two lines that both errored, you need two comments. If you put the comment above a line that doesn’t error, TypeScript reports TS2578: “Unused ‘@ts-expect-error’ directive.” That’s a stricter signal than tsd ever gave you — which is the point.
One migration gotcha worth naming: expectTypeOf(value).toEqualTypeOf<T>() and .toMatchTypeOf<T>() are not synonyms. toEqualTypeOf is invariant — the type must be structurally identical, so { id: number; extra: string } does not equal { id: number }. toMatchTypeOf is the covariant version, equivalent to tsd’s expectAssignable. Using toEqualTypeOf everywhere during migration will produce a flood of false positives; pick the matcher that reflects what the original tsd assertion actually checked.

The diagram maps the before/after runner topology. On the left, the legacy setup runs two pipelines: vitest run for *.test.ts files (green boxes representing runtime assertions) and tsd for *.test-d.ts files (blue boxes representing type assertions). Each pipeline has its own reporter, its own exit code, and its own CI step. On the right, the updated topology collapses both streams into a single vitest run --typecheck invocation. Both box colours feed into one reporter, which emits a unified JUnit XML output — relevant if you were previously merging two reports in CI with something like junit-report-merger.
Benchmark details and what the numbers do not show
The comparison in the chart hides some structure worth spelling out. The tsd number includes node_modules/.bin/tsd startup, TypeScript program construction, emit of its synthesised tsconfig, and walking every *.test-d.ts file. The Vitest number includes Vitest’s own worker-pool initialisation, a single tsc --noEmit invocation against the project tsconfig.test.json, and the reporter rendering pass. Vitest can benefit from TypeScript’s incremental build info when configured to do so, whereas tsd does not use tsc --incremental by default and so stays near its cold number on each run.
Two caveats. First, the numbers are sensitive to how your tsconfig.json handles the include array. If your project config already covers your test files, the Vitest typecheck pass is essentially free — it’s the same compilation your IDE is already doing. If your test files were previously excluded and you bring them in via tsconfig.test.json, expect the first full run to be slower until the incremental buildinfo stabilises. Second, --typecheck.only is the flag you want in CI jobs that only verify types; plain --typecheck runs both the runtime suite and the type assertions, which is slower but necessary for local development.
For more on this, see jsdom test environment.
The right command for a CI type-only job:
npx vitest run --typecheck.only --reporter=junit --outputFile=reports/types.xml
Exit code 0 means every inline expectTypeOf passed and every @ts-expect-error directive is still suppressing a real error. Exit code 1 means at least one failed, and the JUnit XML will include the file, line, expected type, and actual type as rendered by expect-type’s diff formatter.
Community reception and where tsd still wins
The reception in developer forums has been positive but not universal. Library authors with long-standing tsd suites — type-fest being the most visible example — have not announced a migration, partly because their test suites lean on expectError blocks that cover multiple lines at once. Migrating those would require splitting each block into separate @ts-expect-error directives, which is mechanical but touches every file.

Live data: top Reddit posts about “vitest 3.0 expecttypeof inline” by upvotes.
More detail in keeping tooling consistent.
The Reddit thread shown is from r/typescript and asks whether anyone has moved off tsd in favour of the inline approach. The top reply is a migration story from a monorepo maintainer who reports dropping a large number of *.test-d.ts files and cutting their CI typecheck step by a meaningful margin. A high-ranked dissenting comment pushes back: the author maintains a public type utility library and argues that tsd’s expectError is easier for contributors to reason about than a wall of @ts-expect-error comments, because the former groups related error cases visually. Both takes are fair; the right answer depends on whether your type tests exist primarily as internal regression coverage (inline wins) or as public documentation of API boundaries (tsd’s block-level syntax reads better).
The other category where tsd still has a genuine edge is when you want to assert that a specific TypeScript error code appears. tsd’s expectError does not care which error the line produces; @ts-expect-error with inline expectTypeOf does not either, unless you pair it with the ts-expect-error ESLint rule from typescript-eslint configured with a required description. If your conventions demand that every suppressed error carries an explanatory description, the inline path actually ends up stricter than tsd ever was.
For most application code — internal utilities, API clients, reducer state shapes, Zod or Valibot schema inference — the inline path is the right default. Put your type assertions next to the runtime tests that cover the same function, run them with vitest run --typecheck, and delete the tsd devDependency. For published libraries whose type API is a product surface, keep tsd until you have a specific reason to move; the block-level expectError ergonomics are worth the extra runner.
For a different angle, see Vite-based TypeScript setup.
- Vitest — Testing Types guide: the canonical documentation for the
--typecheckflag andexpectTypeOfmatcher list. - vitest CHANGELOG.md: line-level release notes for recent patches.
- mmkal/expect-type: the underlying library that powers Vitest’s
expectTypeOf, with the full matcher surface in itsREADME. - tsdjs/tsd: the alternative runner, still actively maintained, with documented
expectErrorandexpectAssignablesemantics. - typescript-eslint — ban-ts-comment rule: use this to require descriptions on every
@ts-expect-errorafter migration.
