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
Section titled “testType”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 SomeComplexTypeAt runtime testType is a Proxy whose every method is the identity function — the whole value of the
API is in the type signatures.
Equality and assignability
Section titled “Equality and assignability”testType.equal<A, B>(expected: IsEqual<A, B>): AtestType.equal<A, B, C>(expected: IsEqual<A, B> & IsEqual<A, C>): AtestType.canAssign<A, B, $O extends $StrictOptions<$O, $Distributive.Options> = {}>( expected: Assignable<A, B, $ForwardOptions<$O, Assignable.$Options>>): AtestType.strictCanAssign<A, B, $O extends $StrictOptions<$O, $Distributive.Options> = {}>( expected: Assignable<A, B, $MergeOptions<{ distributive: false }, $ForwardOptions<$O, Assignable.$Options>>>): AcanAssign 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)Type checks
Section titled “Type checks”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 |
Options
Section titled “Options”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): TtestType.$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 exacttestType.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 overridableThe 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 eithertestType.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.)
Union membership
Section titled “Union membership”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>): TtestType.hasNull<T>(expected: HasNull<T>): TtestType.hasVoid<T>(expected: HasVoid<T>): TThey 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) // passestestType.undefined<number | undefined, { distributive: true }>(false) // also passes
testType.hasUndefined<number | undefined>(true) // passestestType.hasUndefined<number | undefined>(false) // failsThe 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 }>): TAn 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.
Calling and constructing
Section titled “Calling and constructing”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): FtestType.constructibleWith<F, Args extends readonly unknown[]>(expected): FThe 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): stringfunction f(value: number, radix: number): string
testType.equal<Parameters<typeof f>, [value: number, radix: number]>(true) // the first overload is losttestType.callableWith<typeof f, [string]>(true)testType.callableWith<typeof f, [number, number]>(true)testType.callableWith<typeof f, [number]>(false) // no overload takes one numbertestType.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) => Taccepts[string]and rejects[number]. - A union of functions must accept
Argsin every member. anypasses.neverand a type with no call or construct signature fail, and so does an abstract class forconstructibleWith.
Both are also on testType.of(value) and testType.defer:
testType.of(f).callableWith<[string]>(true)testType.assert(testType.defer.not.constructibleWith<DateConstructor, [boolean]>())Checking a value
Section titled “Checking a value”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.
Checking a function
Section titled “Checking a function”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>'.Deferred checks
Section titled “Deferred checks”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'>()) }) // passesit('bruh', () => { testType.assert(testMyType<1>()) }) // fails here, not in the helpertestType.defer.<check><...>(): true | testType.Failed<Check, Actual, Expected>testType.defer.not.<check><...>(): true | testType.Failed<Check, Actual, Expected>testType.assert(...results: testType.Passed[]): voidEvery 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 offtestType.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>()) // passestestType.assert(testExactly<'a'>()) // fails hereA 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
Section titled “testType.inspect”testType.inspect<T>(handler: (t: InspectedType<T>) => unknown): TA 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>()andextends_*— the result ofT extends Xunion<R>()andunion_*— the typeT | Xintersect<R>()andintersect_*— the typeT & X
Remove the inspect call once you have your answer; it is a development aid, not a test.
Migrating to testType
Section titled “Migrating to testType”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): Tfunction stub<T>(stub: RecursivePartial<NoInfer<T>>): TProduce 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 // stringstub.build
Section titled “stub.build”stub.build<T>(init: RecursivePartial<T> | ((stub?: RecursivePartial<T>) => RecursivePartial<T>)): (stub?: RecursivePartial<T>) => TBuilds 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
Section titled “stub.builder”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 functionstub.buildreturns.
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()