Typescript Type Level Custom Error Message, Move Runtime Error to Compile time Error!

Acid Coder
5 min readJun 26, 2023

--

Part2: Typescript Type Level Custom Error Message For Utility Types

Do you want a more descriptive error message rather than usual type A cannot assign to type B?

for example, take this function:

function (a, b, c) => {}

Requirement:
1. all arguments are number
2. if a is 1, b cannot be 2
3. if b is 2, c cannot be 3
4. if c is 3, a cannot be 1

Solutions:

declare function ABC<A extends number, B extends number, C extends number>(
a: 1 extends 1 ? C extends 3 ? A extends 1 ? “‘a’ cannot be 1 if ‘c’ is 3” : A : A : A,
b: 1 extends 1 ? A extends 1 ? B extends 2 ? “‘b’ cannot be 2 if ‘a’ is 1” : B : B : B,
c: 1 extends 1 ? B extends 2 ? C extends 3 ? “‘c’ cannot be 3 if ‘b’ is 2” : C : C : C
): void
ABC(1,2,4) // error at `b`
ABC(4,2,3) // error at `c`
ABC(1,4,3) // error at `a`

playground

How Does It Works?

We use a simple example to demonstrate, where the requirement is the argument cannot be 1.

declare function ABC<A extends number>(
a: A extends 1? "a cannot be 1": A
): void


ABC(1) // error
ABC(2)

playground

The formula is

condition ? {{type checking}} : Naked Generic Type Parameter

or

condition ? Naked Generic Type Parameter : {{type checking}}

As long as you place the Naked Generic Type Parameter in either true case or false case, Typescript can infer the type from the argument where the conditional type is.

It works regardless of a case is reachable or not:

declare function abc<A>(
a: A extends never ? A: A & 2
): void
abc(1) // error
abc(2)

playground

But it still bey the flow of condition, so this will not work:

declare function ABC<A extends number>(
a: A extends 1? A :"a cannot be 1"
): void

ABC(1) // no error (wrong!)
ABC(2) // error (wrong!)

playground

Object Literal Type

What if we want to restrict the type of object property?

Take this example where the requirement is property b cannot be 2 if property a is 1

declare function ABC<T extends { a: number, b: number }>(
a: { a: number, b: T['a'] extends 1 ? T['b'] extends 2 ? "'b' cannot be 2 if 'a' is 1" : T : T }
): void
ABC({a:1,b:1})
ABC({a:1,b:2})
ABC({a:1,b:3})
// bad, error at all cases

It does not work and the logic does not make sense.

solution:

declare function ABC<const T extends { a: number, b: number }>(
a: [T] extends [T] ? { a: number, b: T['a'] extends 1 ? T['b'] extends 2 ? "'b' cannot be 2 if 'a' is 1" : T['b'] : T['b'] } : T
): void
ABC({ a: 1, b: 1 }) // ok
ABC({ a: 1, b: 2 }) // no error??
ABC({ a: 1, b: 3 }) // ok

playground

formula:

[Naked Generic Type Parameter] extends [Naked Generic Type Parameter] 
? {{type checking }} : Naked Generic Type Parameter

Do note that below will fail:

Naked Generic Type Parameter extends Naked Generic Type Parameter 
? {{type checking }} : Naked Generic Type Parameter
declare function ABC<const T extends { a: number, b: number }>(
a: T extends T ? { a: number, b: T['a'] extends 1 ? T['b'] extends 2 ? "'b' cannot be 2 if 'a' is 1" : T['b'] : T['b'] } : T
): void
ABC({ a: 1, b: 1 }) // ok
ABC({ a: 1, b: 2 }) // no error!!!!
ABC({ a: 1, b: 3 }) // ok

this will fail too:

Naked Generic Type Parameter[] extends Naked Generic Type Parameter[] 
? {{type checking }} : Naked Generic Type Parameter
declare function ABC<const T extends { a: number, b: number }>(
a: T[] extends T[] ? { a: number, b: T['a'] extends 1 ? T['b'] extends 2 ? "'b' cannot be 2 if 'a' is 1" : T['b'] : T['b'] } : T
): void
ABC({ a: 1, b: 1 }) // ok
ABC({ a: 1, b: 2 }) // no error!!!!
ABC({ a: 1, b: 3 }) // ok

I am not sure why but probably related to this

Use Case

If you are curious why type level custom error messages are useful, take a look at how Firelordjs and FireSageJS make Firebase Firestore and Realtime Database much easier to use

--

--

Acid Coder
Acid Coder

Written by Acid Coder

Typescript Zombie. Youtube Pikachu On Acid. (Unrelated to programming but by watching it you become a good developer overnight)