Reference
Expect API

Expect API

TSTyche includes the expect flavoured assertions to compare types.

Type Inference Caveats

If an expression is passed as the target or the source of an assertion, TSTyche will use the TypeScript's type checker to infer its type. This section is a reminder of some aspects of the inference process (like type widening or excess property checks) that may be misinterpreted.

TSTyche sees exactly the same types which you see in code editor's hover messages. To learn more, see the Introduction page.

Type Widening

In mutable context a literal type is inferred (opens in a new tab) as its base type:

import { expect } from "tstyche";
 
const one = 1;
//    ^^^ The type is '1'
 
expect(1).type.toBe<1>();
 
const two = { a: 2, b: [3, 3] };
//    ^^^ The type is '{ a: number; b: number[] }'
 
expect({ a: 2, b: [3, 3] }).type.toBe<{ a: number; b: number[] }>();

To tell to the type checker that a type should not be widened, use type assertions (opens in a new tab):

import { expect } from "tstyche";
 
const six = { a: 6 as const, b: [9, 9] as 9[] };
//    ^^^ The type is '{ a: 6; b: 9[] }'
 
expect({ a: 6 as const, b: [9, 9] as 9[] }).type.toBe<{ a: 6; b: 9[] }>();

It is also possible to use the const type assertion to convert the entire object, but in this case a different type is created:

import { expect } from "tstyche";
 
const ten = { a: 6, b: [9, 9] } as const;
//    ^^^ The type is '{ readonly a: 6; readonly b: readonly [9, 9] }'
 
expect({ a: 6, b: [9, 9] } as const).type.toBe<{
  readonly a: 6;
  readonly b: readonly [9, 9];
}>();

Excess Property Checks

If passed as arguments, the object literals undergo the excess property checks (opens in a new tab):

import { expect } from "tstyche";
 
let config: { timeout?: number } = {};
 
config = { silent: true, timeout: 800 };
//         ~~~~~~ Error: Object literal may only specify known properties,
//                and 'silent' does not exist in type '{ timeout?: number; }'.
 
// Just like above, object literals may only specify known properties
expect.fail({ silent: true, timeout: 800 }).type.toBeAssignableTo<{ timeout?: number }>();
expect.fail<{ timeout?: number }>().type.toBeAssignableWith({ silent: true, timeout: 800 });
 
// But object types are allowed to have excess properties
expect<{ silent: true; timeout: 800 }>().type.toBeAssignableTo<{ timeout?: number }>();
expect<{ timeout?: number }>().type.toBeAssignableWith<{ silent: true; timeout: 800 }>();

Generally these failures prevent outdated assertions hanging around in your tests. If the excess property checks is not what you need, follow the above link to learn how to get around them. For example, you can pass a reference instead of an object literal:

import { expect } from "tstyche";
 
const configSample = { silent: true, timeout: 800 };
 
expect(configSample).type.toBeAssignableTo<{ timeout?: number }>();
expect<{ timeout?: number }>().type.toBeAssignableWith(configSample);

expect()

The expect() function builds an assertion. It must be followed by the .type modifier and a matcher. Optionally a run mode flag can be appended. expect() can be nested inside code.


Signatures. The source type can be passed as a type argument or inferred from an expression:

expect<Source>()
expect(source: unknown)

Run Mode Flags

To select assertions to run, append the run mode flags to the expect() function. The flags are inherited from the parent.

.fail

Marks an assertion as supposed to fail. The test runner will make sure that the assertion actually fails. If it is passing, an error will be raised.

Use .only.fail to focus on or .skip.fail to skip a supposed to fail assertion.

.only

Marks an assertion as focused. When there are focused groups, tests or assertions in a file, only those will run.

.skip

Marks an assertion as skipped. The test runner will ignore anything skipped and will suppress any type errors raised by the expressions or types passed to the expect() or a matcher function.

Matchers

A matcher is the last element of the assertion chain. It holds an assumption to check.

.not

To negate the condition, prepend .not before any matcher:

import { expect } from "tstyche";
 
expect<string>().type.toBeString();
expect<number>().type.not.toBeString();

.toAcceptProps()

Checks if the JSX component accepts props of the given type.

This is a work in progress feature. Generic components are not yet supported.

import { expect, test } from "tstyche";
 
interface ButtonProps {
  text: string;
  type?: "reset" | "submit";
}
 
function Button({ text, type }: ButtonProps) {
  return <button type={type}>{text}</button>;
}
 
test("accepts props?", () => {
  expect(Button).type.toAcceptProps({ text: "Send" });
  expect(Button).type.toAcceptProps({ text: "Clear", type: "reset" as const });
 
  expect(Button).type.not.toAcceptProps({ text: "Download", type: "button" as const });
  expect(Button).type.not.toAcceptProps({});
});

Signatures. The props type can be passed as a type argument or inferred from an expression:

.toAcceptProps<Target>()
.toAcceptProps(target: unknown)

.toBe()

Checks if the source type is identical to the target type.

import { expect, test } from "tstyche";
 
type MethodLike = (...args: any) => any;
 
type MethodLikeKeys<T> = keyof {
  [K in keyof T as Required<T>[K] extends MethodLike ? K : never]: T[K];
};
 
interface Sample {
  description: string;
  getLength: () => number;
  getWidth?: () => number;
}
 
test("MethodLikeKeys", () => {
  expect<MethodLikeKeys<Sample>>().type.toBe<"getLength" | "getWidth">();
});

Signatures. The target type can be passed as a type argument or inferred from an expression:

.toBe<Target>()
.toBe(target: unknown)

.toBeAssignableTo()

Checks if the source type is assignable to the target type. The opposite of the .toBeAssignableWith() matcher.

import { expect, test } from "tstyche";
 
test("Set", () => {
  expect(new Set(["abc"])).type.toBeAssignableTo<Set<string>>();
  expect(new Set([123])).type.toBeAssignableTo<Set<number>>();
 
  expect(new Set([123, "abc"])).type.not.toBeAssignableTo<Set<string>>();
  expect(new Set([123, "abc"])).type.not.toBeAssignableTo<Set<number>>();
});

Signatures. The target type can be passed as a type argument or inferred from an expression:

.toBeAssignableTo<Target>()
.toBeAssignableTo(target: unknown)

.toBeAssignableWith()

Checks if the source type is assignable with the target type. The opposite of the .toBeAssignableTo() matcher.

import { expect, test } from "tstyche";
 
type Awaitable<T> = T | PromiseLike<T>;
 
test("Awaitable", () => {
  expect<Awaitable<string>>().type.toBeAssignableWith("abc");
  expect<Awaitable<string>>().type.toBeAssignableWith(Promise.resolve("abc"));
 
  expect<Awaitable<string>>().type.not.toBeAssignableWith(123);
  expect<Awaitable<string>>().type.not.toBeAssignableWith(Promise.resolve(123));
});

Signatures. The target type can be passed as a type argument or inferred from an expression:

.toBeAssignableWith<Target>()
.toBeAssignableWith(target: unknown)

.toHaveProperty()

Checks if a property key exists on the source type.

import { expect } from "tstyche";
 
type Worker<T> = {
  [K in keyof T as Exclude<K, "setup" | "teardown">]: T[K];
};
 
interface Sample {
  isBusy?: boolean | undefined;
  runTest: (a: string, b: number) => void;
  setup: () => void;
  teardown: () => void;
}
 
test("Worker", () => {
  expect<Worker<Sample>>().type.toHaveProperty("isBusy");
  expect<Worker<Sample>>().type.toHaveProperty("runTest");
 
  expect<Worker<Sample>>().type.not.toHaveProperty("setup");
  expect<Worker<Sample>>().type.not.toHaveProperty("teardown");
});

.toHaveProperty() requires the source to be of an object type. For example, in TypeScript the any type can have any property, but it is not an object:

import { expect } from "tstyche";
 
// Error: A type argument for 'Source' must be of an object type
expect<any>().type.toHaveProperty("nope");

Signature. The key argument can be passed as a string, number or symbol:

.toHaveProperty(key: string | number | symbol)

.toMatch()

Checks if the target type is a subtype of the source type. In other words, .toMatch() is useful to test partial match in complex types.

For example, recursive types:

import { expect, test } from "tstyche";
 
interface Sample {
  nested: Sample;
  optional?: string;
  required: number;
}
 
test("Partial", () => {
  expect<Partial<Sample>>().type.toMatch<{ nested?: Sample }>();
  expect<Partial<Sample>>().type.toMatch<{ optional?: string }>();
  expect<Partial<Sample>>().type.toMatch<{ required?: number }>();
});
 
test("Readonly", () => {
  expect<Readonly<Sample>>().type.toMatch<{ readonly nested: Sample }>();
  expect<Readonly<Sample>>().type.toMatch<{ readonly optional?: string }>();
  expect<Readonly<Sample>>().type.toMatch<{ readonly required: number }>();
});

.toMatch() is similar to .toBeAssignableTo() with several exceptions:

import { expect } from "tstyche";
 
enum Direction {
  Up = 1,
  Down,
}
 
expect<any>().type.toBeAssignableTo<string>();
// But all types are not subtypes of the 'any' type
expect<any>().type.not.toMatch<string>();
 
expect<number>().type.toBeAssignableTo<Direction>();
// But an enum type is not a subtype of a number
expect<number>().type.not.toMatch<Direction>();
 
expect<{ a: string }>().type.toBeAssignableTo<{ a: string; b?: number }>();
// But an object type with an optional property is not a subtype
// of the same object type without that particular property
expect<{ a: string }>().type.not.toMatch<{ a: string; b?: number }>();
 
expect<{ readonly a: string }>().type.toBeAssignableTo<{ a: string }>();
// But an object type with a particular property is not a subtype
// of the same object type with that property marked 'readonly'
expect<{ readonly a: string }>().type.not.toMatch<{ a: string }>();

Signatures. The target type can be passed as a type argument or inferred from an expression:

.toMatch<Target>()
.toMatch(target: unknown)

.toRaiseError()

Checks if the source type raises an error.

import { expect, test } from "tstyche";
 
interface Matchers<R, T = unknown> {
  [key: string]: (expected: T) => R;
}
 
test("Matchers", () => {
  expect<Matchers<void, string>>().type.not.toRaiseError();
 
  expect<Matchers<void>>().type.not.toRaiseError();
 
  expect<Matchers>().type.toRaiseError("requires between 1 and 2 type arguments");
});

It is recommended to pass a substring of an error message to the matcher. Alternatively a numeric error code can be provided. If the source type raises several errors, the matcher will require an argument for each of them.

Avoid the // @ts-expect-error comments in your type tests, instead always use .toRaiseError(). The comment suppresses syntax errors, hence it turns typos into falsy passing tests. For example, the following is passing despite two syntax errors:

interface Matchers<R, T = unknown> {
  [key: string]: (expected: T) => R;
}
 
// @ts-expect-error: requires a type argument
type M = Matcher<>;

Signature. The target argument is optional. It can be one or more substrings or numeric codes:

.toRaiseError(...target: Array<string | number>)

Primitive Type Matchers

The primitive type matchers are shorthands which make your tests easier to read:

import { expect, test } from "tstyche";
 
test("is any?", () => {
  expect<any>().type.toBeAny();
  // Equivalent to '.toBe<any>()'
});
 
test("is string?", () => {
  expect<string>().type.toBeString();
  // Equivalent to '.toBe<string>()'
});
 
test("is number?", () => {
  expect<number>().type.toBeNumber();
  // Equivalent to '.toBe<number>()'
});

.toBeAny()

Checks if the source type is any.

.toBeBigInt()

Checks if the source type is bigint.

.toBeBoolean()

Checks if the source type is boolean.

.toBeNever()

Checks if the source type is never.

.toBeNull()

Checks if the source type is null.

.toBeNumber()

Checks if the source type is number.

.toBeString()

Checks if the source type is string.

.toBeSymbol()

Checks if the source type is symbol.

.toBeUndefined()

Checks if the source type is undefined.

.toBeUniqueSymbol()

Checks if the source type is unique symbol.

.toBeUnknown()

Checks if the source type is unknown.

.toBeVoid()

Checks if the source type is void.