Typescript Type Level Custom Error Message, Move Runtime Error to Compile time Error!
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`
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)
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)
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!)
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
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