Skip to content

Testing

🧪 The testing category holds utilities designed for writing tests — testType for asserting and inspecting types, and stub for producing values you only partially care about.

Both are imported from the type-plus/testing entry, not from type-plus, so they stay out of production code and out of its auto-import suggestions.

import { stub, testType } from 'type-plus/testing'

testType is the main testing API. Each method takes one type parameter for the type under test and one value argument holding the expected result of a predicate. If the type does not match, the argument fails to type check and the test fails at compile time.

import { testType } from 'type-plus/testing'
testType.equal<1 | 2, 1 | 2>(true)
testType.never<never>(true)
testType.string<'abc'>(true)
testType.number<'abc'>(false)

When a check fails, the error names the check, the actual type and the expected type:

testType.equal<string, number>(true)
// Argument of type 'true' is not assignable to parameter of type 'false | Failed<"equal", string, number>'.
testType.equal<string, string>(false)
// Argument of type 'false' is not assignable to parameter of type 'true | Failed<"not equal", string, string>'.

Each expected parameter is typed testType.Expectation<Result, Check, Actual, Expected>: the literal that passes, or the same testType.Failed a deferred check reports. A distributive check that resolves to boolean accepts both literals. The signatures below show only the Result predicate each check wraps.

Every method returns its argument asserted as the first type parameter, so you can capture it and let your editor resolve the type:

const t = testType.equal<SomeComplexType, SomeCompositeType>(true)
type T = typeof t // resolves to SomeComplexType

At runtime testType is a Proxy whose every method is the identity function — the whole value of the API is in the type signatures.

testType.equal<A, B>(expected: IsEqual<A, B>): A
testType.equal<A, B, C>(expected: IsEqual<A, B> & IsEqual<A, C>): A
testType.canAssign<A, B, $O extends $StrictOptions<$O, $Distributive.Options> = {}>(
expected: Assignable<A, B, $ForwardOptions<$O, Assignable.$Options>>
): A
testType.strictCanAssign<A, B, $O extends $StrictOptions<$O, $Distributive.Options> = {}>(
expected: Assignable<A, B, $MergeOptions<{ distributive: false }, $ForwardOptions<$O, Assignable.$Options>>>
): A

canAssign is distributive, so a union A can yield boolean, meaning both true and false pass. Use strictCanAssign when every branch of the union must be assignable.

testType.canAssign<123, number>(true)
testType.strictCanAssign<number | string, number | string>(true)
testType.strictCanAssign<number | string, number>(false)

The strict* variants use exact: true, so a literal fails the check while the widened primitive passes. The plain variants accept both.

testType.bigint<1n>(true)
testType.strictBigint<1n>(false)
testType.strictBigint<bigint>(true)
testType.string<'a'>(true)
testType.strictString<'a'>(false)
Method Checks
any<T> T is exactly any
unknown<T> T is exactly unknown
never<T> T is exactly never
void<T> T is exactly void
undefined<T> T is exactly undefined
null<T> T is exactly null
boolean<T> / strictBoolean<T> T is a boolean or boolean literal / exactly boolean
true<T> / false<T> T is exactly true / false
number<T> / strictNumber<T> T is a number or number literal / exactly number
bigint<T> / strictBigint<T> T is a bigint or bigint literal / exactly bigint
string<T> / strictString<T> T is a string or string literal / exactly string
symbol<T> T is a symbol
object<T> T is an object — note functions, arrays and tuples are also objects
array<T> T is exactly an array
tuple<T> T is a tuple
function<T> / strictFunction<T> T is a function / strictly a function

Every type check takes an optional second type parameter holding the behavioral options of the underlying IsXXX type:

testType.string<T, $O extends $StrictOptions<$O, testType.$Options> = {}>(expected): T

testType.$Options is { distributive?: boolean; exact?: boolean }. Selection and branching options are deliberately not part of the surface — testType always resolves its check as a predicate so that expected stays a true/false literal.

Options you pass are merged over the method’s own defaults, so omitting the type argument keeps the behavior each method has always had:

testType.string<'a'>(true) // default: not exact
testType.string<'a', { exact: true }>(false) // opt into exact comparison
testType.array<[string]>(false) // `array` defaults to `exact: true`
testType.array<[string], { exact: false }>(true) // and it can be turned off
testType.strictString<'a'>(false) // `strict*` bakes in `exact: true`
testType.strictString<'a', { exact: false }>(true) // which is also overridable

The checks default to distributive: false, so a union has to satisfy the check as a whole. Turn distribution on to evaluate each member separately, which widens the result to boolean when the members disagree:

testType.string<'a' | 1>(false)
testType.string<'a' | 1, { distributive: true }>(true) // `boolean` accepts either
testType.string<'a' | 1, { distributive: true }>(false)

canAssign and strictCanAssign take the same parameter as their third, and accept distributive only:

testType.canAssign<number | string, number, { distributive: false }>(false)
testType.strictCanAssign<number | string, number, { distributive: true }>(true)

any, unknown, never, equal and the has* family take no options — none of the types behind them has a distributive or exact dimension. (IsEqual.$Options is $Selection.$BaseOptions: branch overrides only.)

The has* methods check that a type contains a member, rather than being that member. They are the testType face of the HasUndefined, HasNull and HasVoid predicates.

testType.hasUndefined<T>(expected: HasUndefined<T>): T
testType.hasNull<T>(expected: HasNull<T>): T
testType.hasVoid<T>(expected: HasVoid<T>): T

They take no options type parameter: distributive is what the check is made of, and none of undefined, null or void has a literal subtype for exact to narrow.

type R = number | undefined
testType.hasUndefined<R>(true)
testType.undefined<R>(false)

Each union branch is checked on its own, so a union passes when any branch matches.

testType.hasNull<string | null>(true)
testType.hasVoid<string | void>(true)
testType.hasNull<string | undefined>(false)

This is not the same as passing { distributive: true } to the plain check. Distribution widens the result to boolean when the branches disagree, and boolean accepts both arguments — so the assertion stops asserting anything. has* folds the branches back into a single true or false:

testType.undefined<number | undefined, { distributive: true }>(true) // passes
testType.undefined<number | undefined, { distributive: true }>(false) // also passes
testType.hasUndefined<number | undefined>(true) // passes
testType.hasUndefined<number | undefined>(false) // fails

The special types follow the same rule as the plain checks above — any, unknown, never and void are not treated as containing undefined or null, and any, unknown and never are not treated as containing void.

testType.hasUndefined<any>(false)
testType.hasUndefined<void>(false)
testType.hasVoid<never>(false)

Like every other check, they have deferred forms — testType.defer.hasUndefined<T>() and testType.defer.not.hasNull<T>().

There is no hasAny, hasUnknown or hasNever. A union absorbs those types — T | any is any, T | unknown is unknown, and T | never is T — so they can never be one branch among several, and testType.any, testType.unknown and testType.never already answer the question.

property checks that a type has a key, like toHaveProperty in vitest’s expectTypeOf. It is the testType face of the HasKey predicate, resolved as testType.true<HasKey<T, K>>(expected).

testType.property<T, K extends PropertyKey>(expected: IsTrue<HasKey<T, K>, { distributive: false }>): T

An optional key counts as present. A union K passes only when T has every key in it, and a union T has only the keys its members share.

testType.property<{ a: 1 }, 'a'>(true)
testType.property<{ a?: 1 }, 'a'>(true)
testType.property<{ a: 1 }, 'b'>(false)
testType.property<{ a: 1; b: 2 }, 'a' | 'b'>(true)
testType.property<{ a: 1 }, 'a' | 'b'>(false)
testType.property<{ a: 1 } | { a: 2; b: 2 }, 'b'>(false)

The check reads keyof T, so an index signature has every key of its type, and any and never have every key: testType.property<never, 'a'>(true) passes. It takes no options type parameter.

callableWith checks that a function can be called with a list of argument types, and constructibleWith checks that a class or constructor can be called with new and a list of argument types. The arguments are a tuple type.

testType.callableWith<F, Args extends readonly unknown[]>(expected): F
testType.constructibleWith<F, Args extends readonly unknown[]>(expected): F

The check passes when one of the overloads of F accepts Args, as a call would pick it. equal cannot check this for an overloaded function, because Parameters<F> keeps only the last overload:

function f(value: string): string
function f(value: number, radix: number): string
testType.equal<Parameters<typeof f>, [value: number, radix: number]>(true) // the first overload is lost
testType.callableWith<typeof f, [string]>(true)
testType.callableWith<typeof f, [number, number]>(true)
testType.callableWith<typeof f, [number]>(false) // no overload takes one number
testType.callableWith<typeof f, [string | number]>(false) // nor either type in one call
testType.constructibleWith<DateConstructor, [number, number]>(true)
testType.constructibleWith<DateConstructor, [boolean]>(false)

Arity counts: an argument list with too few or too many arguments fails, while optional and rest parameters may be left out. Some cases have a fixed answer:

  • Up to ten overloads are read.
  • A generic overload is checked against the constraints of its type parameters, so <T extends string>(value: T) => T accepts [string] and rejects [number].
  • A union of functions must accept Args in every member.
  • any passes. never and a type with no call or construct signature fail, and so does an abstract class for constructibleWith.

Both are also on testType.of(value) and testType.defer:

testType.of(f).callableWith<[string]>(true)
testType.assert(testType.defer.not.constructibleWith<DateConstructor, [boolean]>())

testType.of(value) checks the type of a value. It binds the type under test to the type of value, so you can check an inline expression or the inferred result of a generic call without naming a variable and writing typeof:

testType.of([1, 2].map(String)).equal<string[]>(true)
testType.of(Promise.resolve(1)).equal<Promise<number>>(true)

It returns a testType.Subject<T>, which has every check above with its first type parameter already filled in. testType.of(value).equal<B>(true) is testType.equal<typeof value, B>(true), and the options move up one position: testType.of(value).string<{ exact: true }>(false).

The type is inferred as TypeScript infers any generic argument, so an inline literal widens. Write as const to keep it narrow:

testType.of('a').equal<string>(true)
testType.of('a' as const).equal<'a'>(true)

The value is never read.

parameters, returns and resolves bind a part of the subject as a new subject, so every check applies to it:

testType.of((a: number) => String(a)).parameters.equal<[a: number]>(true)
testType.of((a: number) => String(a)).returns.equal<string>(true)
testType.of(async () => 1).returns.resolves.equal<number>(true)
testType.of(Promise.resolve(1)).resolves.number(true)

parameters and returns work as Parameters<T> and ReturnType<T> do: a union of functions gives the union of their parameter tuples or return types, and an overloaded function gives the last overload. resolves unwraps one level of a PromiseLike.

They exist only on testType.of(value), not as top-level checks. On a subject that is not a function (or, for resolves, not a PromiseLike), the member is a testType.Failed with no checks, so any check on it fails to compile:

testType.of(1).returns.equal<number>(true)
// Property 'equal' does not exist on type 'Failed<"returns", number, AnyFunction>'.

Each testType.* check asserts immediately — the expectation is an argument, so the failure lands where the check is written. That is what stops a check from being extracted into a reusable helper: the helper body is checked once, against type parameters that are not yet resolved, so nothing there can either pass or fail.

testType.defer.* takes no argument and returns the result as a type instead. A helper returns the results it collected, and testType.assert() checks them at the call site, where the type parameters are resolved.

import { testType } from 'type-plus/testing'
function testMyType<T>() {
return [
testType.defer.string<T>(),
testType.defer.not.never<T>(),
]
}
it('blah', () => { testType.assert(testMyType<'a'>()) }) // passes
it('bruh', () => { testType.assert(testMyType<1>()) }) // fails here, not in the helper
testType.defer.<check><...>(): true | testType.Failed<Check, Actual, Expected>
testType.defer.not.<check><...>(): true | testType.Failed<Check, Actual, Expected>
testType.assert(...results: testType.Passed[]): void

Every check listed above has a deferred form, with the same type parameters. inspect does not — it is a development aid, not a check.

testType.assert() accepts results in any shape a helper finds convenient to return: a single result, an array, an object, or any nesting of those.

function testMyType<T>() {
return {
isString: testType.defer.string<T>(),
isNotNumber: testType.defer.not.number<T>(),
}
}
testType.assert(testMyType<'a'>())

testType.defer.not.* is the deferred form of passing false to the immediate check, so testType.defer.not.equal<A, B>() mirrors testType.equal<A, B>(false).

Deferred checks take the same options as their immediate counterparts, in the same position and merged over the same defaults:

testType.defer.string<'a', { exact: true }>() // fails — the literal is not exactly `string`
testType.defer.array<[string], { exact: false }>() // passes — `array`'s `exact: true` turned off
testType.defer.canAssign<number | string, number, { distributive: false }>()

so a helper can fix an option once and every call site inherits it:

function testExactly<T>() {
return testType.defer.string<T, { exact: true }>()
}
testType.assert(testExactly<string>()) // passes
testType.assert(testExactly<'a'>()) // fails here

A failing check resolves to testType.Failed<Check, Actual, Expected> rather than a bare false, so the compiler error names the check and the types it compared:

Argument of type '{ isString: testType.Failed<"string", 1, string>; isNotNumber: true; }'
is not assignable to parameter of type 'Passed'.
testType.inspect<T>(handler: (t: InspectedType<T>) => unknown): T

A scratchpad for figuring out how a type behaves. The handler is never called — it exists only to hold a value you can hover over.

testType.inspect<SomeType>(t => {
type T = typeof t.type // resolve the type
t.extends_boolean // result of `SomeType extends boolean`
t.extends<AnotherType>() // true or false
t.union<number>() // SomeType | number
t.intersect<string>() // SomeType & string
})

InspectedType<T> exposes three families of members, each with a generic form and a set of prebuilt cases against any, unknown, void, never, undefined, null, boolean, true, false, number, 1, bigint, 1n, string, 'a', symbol, object, Function, unknown[] and []:

  • extends<R>() and extends_* — the result of T extends X
  • union<R>() and union_* — the type T | X
  • intersect<R>() and intersect_* — the type T & X

Remove the inspect call once you have your answer; it is a development aid, not a test.

testType is the testing API for types. It belongs in test files: in code, use satisfies for a compile-time check, and isType or assertType with a validator for a runtime one.

The older helpers that overlapped it were removed in 8.0.0:

Removed In a test In code
canAssign<T>()(v) testType.canAssign<typeof v, T>(true) v satisfies T
canAssign<T>(false)(v) testType.canAssign<typeof v, T>(false)
isType<T>(v) (one argument) testType.canAssign<typeof v, T>(true) v satisfies T
assertType<T>(v) (no validator) and its members testType.* v satisfies T, or assertType(v, validator)
isType.t / .f / .never / .equal testType.true / .false / .never / .equal

stub below stays: it builds values rather than checking types.

function stub<T extends AnyFunction>(stub: T): T
function stub<T>(stub: RecursivePartial<NoInfer<T>>): T

Produce a value typed as T while only supplying the parts your test uses. Functions pass through as-is.

import { stub } from 'type-plus/testing'
type User = { id: string; name: string; profile: { email: string } }
const user = stub<User>({ name: 'Alice' })
user.name // string
stub.build<T>(init: RecursivePartial<T> | ((stub?: RecursivePartial<T>) => RecursivePartial<T>)): (stub?: RecursivePartial<T>) => T

Builds a stub factory for T from default values. Each call deep-merges the partial stub you pass over init, so a value you pass wins over the default.

import { stub } from 'type-plus/testing'
type User = { id: string; name: string; profile: { email: string } }
const createUser = stub.build<User>({ id: '1', name: 'Alice', profile: { email: 'alice@example.com' } })
createUser() // { id: '1', name: 'Alice', profile: { email: 'alice@example.com' } }
createUser({ name: 'Bob' }) // { id: '1', name: 'Bob', profile: { email: 'alice@example.com' } }

init can also be a function. It receives the partial stub passed to the call, or undefined, and returns the stub to use, which lets the defaults depend on what the caller passed. What the function returns is used as is, so it decides which values win: here ...s comes after the defaults.

const createUser = stub.build<User>((s) => ({
id: '1',
name: 'Alice',
...s,
profile: { email: `${s?.name ?? 'alice'}@example.com`, ...s?.profile },
}))
createUser({ name: 'bob' }) // { id: '1', name: 'bob', profile: { email: 'bob@example.com' } }

stub.build<T>(init) is the shorthand for stub.builder<T>(init).create().

stub.builder<T>(init: RecursivePartial<T> | ((stub?: RecursivePartial<T>) => RecursivePartial<T>))

Creates a builder for a stub factory of T, starting from init. The builder has two methods:

  • .with(init) adds another partial stub or handler function and returns a new builder.
  • .create() produces the stub factory, the same kind of function stub.build returns.
const s = stub.builder<{ a: number; b: string }>({ a: 1 }).with({ b: 'b' }).create()
s() // { a: 1, b: 'b' }
s({ a: 2 }) // { a: 2, b: 'b' }

The initializers run in order, starting from the partial stub passed to the call:

  • An object initializer is deep-merged over the earlier ones, so a later .with() value wins. Nested keys it does not set are kept.
  • The partial stub passed to the call wins over every object initializer.
  • A function initializer receives the stub so far: the partial stub passed to the call, merged with the initializers before it. What it returns is the next stub, used as is.
const s = stub.builder<{ a: number; b: string }>({ a: 1, b: 'b' }).with({ a: 2 }).create()
s() // { a: 2, b: 'b' }
s({ a: 3 }) // { a: 3, b: 'b' }

A function initializer sees the values merged so far, including the partial stub passed to the call:

const s = stub.builder<{ a: number; b: number }>({ a: 1 }).with((acc) => ({ ...acc, b: acc!.a! * 10 })).create()
s() // { a: 1, b: 10 }
s({ a: 3 }) // { a: 3, b: 30 }

Each .with() returns a new builder and leaves the original unchanged, so one base builder can branch into several factories.

const base = stub.builder<User>({ id: '1', profile: { email: 'user@example.com' } })
const createAlice = base.with({ name: 'Alice' }).create()
const createBob = base.with({ name: 'Bob' }).create()