DEV Community

Acid Coder
Acid Coder

Posted on • Edited on

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

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) => {}
Enter fullscreen mode Exit fullscreen mode

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`
Enter fullscreen mode Exit fullscreen mode

Image description

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)
Enter fullscreen mode Exit fullscreen mode

Image description

playground

The formula is

condition ? {{type checking}} : Naked Generic Type Parameter
Enter fullscreen mode Exit fullscreen mode

or

condition ? Naked Generic Type Parameter : {{type checking}}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Image description

playground

But it still obey 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!)
Enter fullscreen mode Exit fullscreen mode

Image description
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
Enter fullscreen mode Exit fullscreen mode

Image description

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 }) // error!
ABC({ a: 1, b: 3 }) // ok
Enter fullscreen mode Exit fullscreen mode

Image description

playground

formula:

[Naked Generic Type Parameter] extends [Naked Generic Type Parameter] 
? {{type checking }} : Naked Generic Type Parameter
Enter fullscreen mode Exit fullscreen mode

Do note that below will fail:

Naked Generic Type Parameter extends Naked Generic Type Parameter 
? {{type checking }} : Naked Generic Type Parameter
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

Image description
this will fail too:

Naked Generic Type Parameter[] extends Naked Generic Type Parameter[] 
? {{type checking }} : Naked Generic Type Parameter
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

Image description

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

Top comments (0)