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?