'Typescript generic type constraint with conditional type, avoid circular type constraint

Suppose the following example class

class X<T> {
  constructor(readonly t: Extract<'a' | 'b', T>) {}
}

The point is that the type function Extract turns out never if the constuctor is called with a wrong parameter, like in

const x = new X('z');

The Problem: The compiler puts a squiggle below 'z' telling that it is not assignable to never. Hovering over X( in vscode only tells us that that the parameter of the constructor should be never. There seems to be no indication that the type constraint is actually Extract(...).

Question: The error message on the "z" as the constructor parameter saying it should be never is not very helpful when you not also have the class definition in front of you. Is there a way to rephrase the definition of class X such that we better see why the "z" is a wrong parameter.

What I tried: First I had the type constraint on the generic parameter:

// THIS DOES NOT WORK
class X<T extends Extract<'a' | 'b', T> { ... }

Yet, this is not allowed due to a circular type reference.

Note: Extract is only used as an example. It could be any more elaborate type function that may return never for the actual type parameter. The actual type constraint I would like to use is the TrueStringLiterals constraint.



Solution 1:[1]

You can cook your own error messages

type IsAOrB<T> =
  T extends Extract<'a' | 'b', T> ? T
  : Constraint<`Extract<'a' | 'b', T>`>;

type Constraint<Message> = { [constraint]: Message };
declare const constraint: unique symbol;

class X<T> {
  constructor(readonly t: IsAOrB<T>) {}
}

const x = new X('z');
//              ---
// Argument of type 'string' is not assignable to
// parameter of type 'Constraint<"Extract<'a' | 'b', T>">'

The idea is that no type will match { [constraint]: Message } because constraint is a unique symbol, so it's functionally similar to returning never but you have your custom message.

I would like to add that it's not super clear what your use case was meant to represent and I don't necessarily advise to do this in the general case, but this pattern is pretty handy for rejecting inputs with a custom error.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1