# expect-type: compile time tests for TypeScript types

> expect-type is a zero dependency TypeScript library that turns type assertions into compile time errors, so a type that has quietly widened to any or lost a property fails the type check rather than slipping through review. It works in any type checked file, is built into vitest, and covers exact matching, partial matching, subtype relationships and the any, unknown and never cases.

**mmkal/expect-type** — Compile-time tests for types. Useful to make sure types don't regress into being overly-permissive as changes go in over time.

- Repository: https://github.com/mmkal/expect-type
- Stars: 566 · Forks: 10
- Language: TypeScript
- License: NOASSERTION
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/mmkal-expect-type

## A type that widens to any is a silent failure

expect-type provides compile time tests for types, useful to make sure types do not regress into being overly permissive as changes go in over time.

That sentence describes the problem precisely. In TypeScript, a type can lose its specificity without anyone noticing. A function that returned a specific object type starts returning an inferred one, a generic parameter loses its constraint, an interface picks up an index signature, and suddenly any object is assignable to it. The code still compiles. The runtime behaviour is unchanged. And every consumer of that type has quietly lost the checking the type was there to provide.

No runtime test catches this, because the runtime never changed. The only place the problem is visible is in the type system, which is why the assertions have to be type level assertions.

The library is similar to expect, but with type awareness, and it gives you access to several type matchers that let you make assertions about the form of a reference or a generic type parameter. The first example in the readme shows the shape of it:

```ts
import {expectTypeOf} from 'expect-type'
import {foo, bar} from '../foo'

// make sure `foo` has type {a: number}
expectTypeOf(foo).toEqualTypeOf<{a: number}>()

// make sure `bar` is a function taking a string:
expectTypeOf(bar).parameter(0).toBeString()
expectTypeOf(bar).returns.not.toBeAny()
```

The design decision that makes this usable is that nothing happens at run time. The expectTypeOf method takes a single argument or a generic type parameter, and neither it nor the functions chained off its return value have any meaningful runtime behaviour. The assertions you write are compile time errors if they do not hold.

Which means the whole thing lives in your editor and in tsc.

## Three matchers, and the difference between them is the point

The matcher set is what gives the library its power, and the readme spends a lot of space on the difference between the three main ones.

toEqualTypeOf is exact equality. It checks an object's type against a type argument:

```typescript
expectTypeOf({a: 1}).toEqualTypeOf<{a: number}>()
```

It also works between two concrete objects, though the readme notes that when these assertions fail, the error messages can be less informative than the generic type argument form. Two objects with different values but the same type succeed, so {a: 1} equals {a: 2} as a type, while excess properties fail.

toMatchObjectType is the strict subset check, and it is what you want when comparing against a partial shape:

```typescript
expectTypeOf({a: 1, b: 1}).toMatchObjectType<{a: number}>()
```

It can also check partial matches on deeply nested objects, so a user object with an address containing a city can be asserted against a type that mentions only the name and the address city. That is the assertion you want for a public API that is allowed to return more than it promises.

toExtend checks the is-a relationship, that one type extends another:

```typescript
expectTypeOf('some string').toExtend<string | boolean>()
```

The readme is explicit that toExtend can be used with object types but that toMatchObjectType is usually the better choice for objects, because it is stricter. The example makes the difference concrete. With a Fruit type of a literal type field and an edible boolean, and an Apple type adding a name and narrowing edible to the literal true, toExtend succeeds from Apple to Fruit because Apple is a Fruit, while toEqualTypeOf fails from Apple to Fruit because they are not identical, and toExtend fails from Fruit to Apple because the relationship does not hold in that direction.

All three fail on missing properties, so a type that loses a field is caught by every one of them.

## Catching any, unknown and never, and inverting the assertion

The matchers that catch a type becoming overly permissive are the ones that matter most for the stated purpose of the library.

There are dedicated matchers for the three special types:

```typescript
expectTypeOf<unknown>().toBeUnknown()
expectTypeOf<any>().toBeAny()
expectTypeOf<never>().toBeNever()

// @ts-expect-error
expectTypeOf<never>().toBeNumber()
```

And toEqualTypeOf distinguishes between deeply nested any and unknown properties, which is a distinction the base type system will not make for you:

```typescript
expectTypeOf<{deeply: {nested: any}}>().not.toEqualTypeOf<{deeply: {nested: unknown}}>()
```

There is also a set of basic JavaScript type matchers, so you can assert that a value is a function, an object, an array, a string, a number or a boolean, and that a function returns void.

Every assertion can be inverted with .not, and the readme argues that this is often easier than relying on the ts-expect-error comment:

```typescript
type Fruit = {type: 'Fruit'; edible: boolean}
type Apple = {type: 'Fruit'; name: 'Apple'; edible: true}

expectTypeOf<Apple>().toExtend<Fruit>()

expectTypeOf<Fruit>().not.toExtend<Apple>()
expectTypeOf<Apple>().not.toEqualTypeOf<Fruit>()
```

The reasoning is sound. The ts-expect-error comment is a claim about the compiler that is only verified by the fact that the next line fails, and it breaks silently if the error goes away, which is exactly the situation where the assertion has become pointless. Writing the negative case directly is self verifying, because the assertion that the type does not extend is checked in the same way as the positive one.

The readme also flags that toMatchTypeOf is deprecated, and the table of contents has a section explaining why.

## No build step, no plugin, and vitest already has it

The adoption story is the strongest part of the project, and it is a matter of not asking anything of the reader.

It can be used in your existing test files, and it is actually built in to vitest, or it can be used in any other type checked file you like. It is built into existing tooling with no dependencies, and no extra build step, CLI tool, IDE extension or lint plugin is needed. You import the function and start writing tests, and failures appear in your IDE and when you run tsc.

Installation is one command:

```cli
npm install expect-type --save-dev
```

The import itself is the whole usage surface:

```typescript
import {expectTypeOf} from 'expect-type'
```

The package manifest backs up the zero dependency claim. The dependencies block does not exist, and the devDependencies are the tooling rather than the library. The package is published as dist/index.js with types at dist/index.d.ts, and the files list is dist plus the markdown files, so the published artifact is the compiled library and the documentation.

The build is tsc with a separate tsconfig.lib.json for the library, the type check is plain tsc, and the lint script runs tsc and then eslint with zero warnings allowed. The tests run with vitest, and the aretheypeswrong script runs the attw pack check, which verifies that the published type declarations are correctly shaped for the different module resolution modes. That last one is a detail that only matters if you publish a library with types, and it is the kind of thing a project gets wrong without noticing.

The versions show a project that is maintained. The recent releases are v1.4.0 from 2026-06-25, v1.3.0 from 2025-12-08 and v1.2.2 from 2025-07-06, with the last push on 2026-10-16. The repository carries a SECURITY.md, a renovate.json for dependency updates, an eslint config, and a pnpm workspace.

## Where it does not fit

There is a limitation section in the documentation, and the honest thing is to read it before you commit the library to your public API.

The structural limitation is the one that follows from the design. The assertions check types, not values, so a function that returns the right shape but the wrong contents is not caught here. And the type level view of a value is only as good as the type, so if a function is already annotated as returning any, asserting on it confirms the any rather than preventing it. The library catches a type that regresses from a specific type to a permissive one when you have an assertion on the specific type. If the only assertion is on the permissive one, it will pass.

The other limitation is about how the errors read. The readme has a whole section on error messages, with a subsection on the difference between concrete expected objects and type arguments, and the rule it gives is to prefer the generic type argument syntax because the diagnostics are better. That is a real ergonomic constraint: a library whose failures are hard to read is a library people stop using, and the readme is candid that the two forms do not produce equal quality output.

There is also a deprecation to be aware of. toMatchTypeOf is deprecated, with a section in the table of contents explaining why, so an older codebase that adopted it early may need to migrate.

And the project compares itself to similar projects in a comparison section, and keeps a table of the differences between them, so if you are choosing a type testing approach this is the place to see how it positions itself.

The last thing worth knowing is about the project rather than the library. The readme has a section on documenting limitations through tests, and the repository has a test directory with a usage test file that the documentation is generated from, using a codegen marker. The examples in the readme are therefore produced from the test suite rather than written by hand, which is why they cannot drift away from what the library actually does.

## Conclusion

Adopt expect-type if your TypeScript codebase has public types that other people depend on, because the failure it catches is a type that has silently become permissive, and that is the kind of regression no runtime test detects. Do not adopt it to replace runtime tests, because it asserts nothing at run time, and note that the error messages are less informative when you compare two concrete objects rather than a type argument. Before you commit, add a few assertions to a public type and run tsc to see the error output, and decide whether to use the generic type argument form, which gives the better diagnostics.

## FAQ

### What does expect-type do?

It provides compile time tests for types, useful to make sure types do not regress into being overly permissive as changes go in over time. It is similar to expect but with type awareness, giving you matchers to assert about the form of a reference or a generic type parameter, and neither expectTypeOf nor the chained functions have any meaningful runtime behaviour.

### What is the difference between toEqualTypeOf, toMatchObjectType and toExtend?

toEqualTypeOf is exact type equality, so Apple is not equal to Fruit because Apple narrows edible to true. toMatchObjectType is a strict check on the subset of keys present in the expected type, which is usually the better choice for objects. toExtend checks the is-a relationship, so Apple extends Fruit but Fruit does not extend Apple.

### How do I install expect-type and use it?

Run npm install expect-type --save-dev, then import expectTypeOf from expect-type in any type checked file. It works in existing test files, is built into vitest, and needs no extra build step, CLI tool, IDE extension or lint plugin, with failures showing up in your editor and when you run tsc.

### How do I catch a type that has become any or unknown?

Use the dedicated matchers toBeAny, toBeUnknown and toBeNever. toEqualTypeOf also distinguishes a deeply nested any property from a deeply nested unknown one, so expectTypeOf<{deeply: {nested: any}}>().not.toEqualTypeOf<{deeply: {nested: unknown}}>() passes when the nesting has been weakened.

### Is expect-type actively maintained?

The recent releases are v1.4.0 from 2026-06-25, v1.3.0 from 2025-12-08 and v1.2.2 from 2025-07-06, with the last push on 2026-10-16. The licence is recorded as Apache 2.0 in the package manifest, and the package requires Node 12 or newer.

## Sources

- [Issues](https://github.com/mmkal/expect-type/issues)
- [mmkal/expect-type on GitHub](https://github.com/mmkal/expect-type)
- [README](https://github.com/mmkal/expect-type/blob/main/README.md)
- [Releases](https://github.com/mmkal/expect-type/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/mmkal-expect-type
