🎓 A1Learner
🔍
0 of 37 lessons completed

TypeScript Type-Level Tests

A type-level test is a type alias that fails to compile when two types differ. The Expect and Equal builds turn type equality into a compile error, so a wrong build breaks the build.

Equal and Expect check one pair

Equal compares two types through identical function wrappers, and the same union written in a different order still matches. Expect passes the result on, and the witness prints after a clean compile.

type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;

type Expect<T extends true> = T;

type SamePair = Expect<Equal<'pen' | 'book', 'book' | 'pen'>>;

const witness: SamePair = true;
console.log(witness);

The wrapper separates types that extends joins

A plain check treats any as matching string, because any fits every test. Equal reports false for that pair, and the suite asserts exactly that.

type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;

type Expect<T extends true> = T;

type AnyIsNotString = Expect<Equal<Equal<string, any>, false>>;
type StringIsString = Expect<Equal<string, string>>;

const firstCheck: AnyIsNotString = true;
const secondCheck: StringIsString = true;
console.log(firstCheck);
console.log(secondCheck);

A suite over the builds of this course

Three assertions pin TupleToUnion, Length, and Concat to their expected results. Every assertion must hold, or the file stops compiling.

type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;

type Expect<T extends true> = T;

type TupleToUnion<T extends readonly unknown[]> = T[number];

type Length<T extends readonly unknown[]> = T['length'];

type Concat<A extends readonly unknown[], B extends readonly unknown[]> = [...A, ...B];

type CheckUnion = Expect<Equal<TupleToUnion<['a', 'b', 3]>, 'a' | 'b' | 3>>;
type CheckLength = Expect<Equal<Length<['x', 'y']>, 2>>;
type CheckConcat = Expect<Equal<Concat<[1], [2, 3]>, [1, 2, 3]>>;

const unionCheck: CheckUnion = true;
const lengthCheck: CheckLength = true;
const concatCheck: CheckConcat = true;
console.log(unionCheck);
console.log(lengthCheck);
console.log(concatCheck);

A failing assertion is a compile error

This assertion claims a two-member union equals a one-member union. Equal reports false, Expect refuses it, and compilation stops at the alias.

type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;

type Expect<T extends true> = T;

type Check = Expect<Equal<'a' | 'b', 'a'>>;
console.log('never printed');

Example

type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;

type Expect<T extends true> = T;

type Push<T extends readonly unknown[], V> = [...T, V];

type Reverse<T extends readonly unknown[]> = T extends readonly [infer Head, ...infer Rest] ? [...Reverse<Rest>, Head] : [];

type Head<T extends readonly unknown[]> = T extends readonly [infer H, ...unknown[]] ? H : never;

type CheckPush = Expect<Equal<Push<[1], 'x'>, [1, 'x']>>;
type CheckReverse = Expect<Equal<Reverse<[1, 'a']>, ['a', 1]>>;
type CheckHead = Expect<Equal<Head<[9, 8]>, 9>>;

const pushCheck: CheckPush = true;
const reverseCheck: CheckReverse = true;
const headCheck: CheckHead = true;
console.log(pushCheck);
console.log(reverseCheck);
console.log(headCheck);

Key points

  • Equal wraps each side in a generic function signature and checks whether the two signatures are identical.
  • The function wrapper makes the check stricter than a plain extends test, which only asks whether one side fits the other.
  • Expect accepts only the literal type true, so an Expect assertion compiles only when Equal reports true.
  • A suite of assertions can pin down the results of TupleToUnion, Length, and Concat from this course.
  • A failing assertion reports error TS2344 at the alias that broke.

In this example, a closing suite pins the Push, Reverse, and Head builds from this course to their expected types, and changing one expected member would turn the whole file into a compile error.

Note

Type assertions produce no runtime code, and a printed line in the exhibits is only a witness that the file ran.

Try it Yourself

Edit the code, then press Run.

Running is simulated · TypeScript Playground

Exercise

What happens when an Expect assertion is compiled and the two types differ?