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?