DEV Community

Susisu
Susisu

Posted on

Restricting Function Arguments Using Type-Level Predicates

What are type-level predicates?

In the context of programming, a predicate usually refers to a function that returns a boolean (yes/no) value.

For example, these are predicates that are commonly used:

function isPositive(n: number): boolean {
  return n > 0;
}

function hasLength(s: string, l: string): boolean {
  return s.length === l;
}
Enter fullscreen mode Exit fullscreen mode

Both of the functions take one or more arguments and return yes or no.

Type-level predicates do the same thing at the type level. Here we define them as conditional types that take one or more arguments and return true or false type.

type IsPositive<N extends number> = /* snip */ ? true : false;

type HasLength<S extends string, L extends number> = /* snip */ ? true : false;
Enter fullscreen mode Exit fullscreen mode

(The implementations of these predicates are a bit complex, so let's skip for now.)

Restricting function arguments using types

As you already know, function arguments can be restricted using types.

declare function myFunc(n: number): void;

myFunc(42);    // OK
myFunc(0);     // OK
myFunc(-42);   // OK
myFunc("XXX"); // Error: "XXX" is not a number
Enter fullscreen mode Exit fullscreen mode

We can also provide a subset of number. This corresponds to defining a set by enumerating its members { 1, 2, 3 }.

type MyNumber = 1 | 2 | 3;

declare function myFunc(n: MyNumber): void;

myFunc(1); // OK
myFunc(2); // OK
myFunc(3); // OK
myFunc(0); // Error: 0 is not a member of 1 | 2 | 3
Enter fullscreen mode Exit fullscreen mode

But what if we want to constrain n to be a positive number? We cannot provide the set by enumeration, because it has an infinite number of members.

type Positive = 1 | 2 | 3 | ...;
Enter fullscreen mode Exit fullscreen mode

Can we define Positive using the type-level predicate IsPositive<N>, like a set comprehension { n | n is a number, n > 0}?

// Can we define like this?
type Positive = (N | N extends number, IsPositive<N>);
Enter fullscreen mode Exit fullscreen mode

But unfortunately, TypeScript does not provide such feature.

So simply using types to restrict arguments fails if there are an infinite number of (or too many) acceptable values, and we need another way for such case.

Restricting function arguments using type-level predicates

Here is the answer for the problem (playground):

type Positive<N extends number> = IsPositive<N> extends true ? N : never;

declare function myFunc<N extends number>(n: Positive<N>): void;
Enter fullscreen mode Exit fullscreen mode

How does this work? Let's take a look.

When we call the function, for instance myFunc(42), the compiler first tries to infer the type parameter N from the argument 42. In this case, it knows that the conditional type Positive<N> returns N | never = N whatever N is, and successfully infers N = 42.

(Note that N has an upper bound number, so widening (inferring N as N = number) does not occur.)

Next, Positive<N> is computed using the inferred N. Clearly N = 42 is a positive number, so Positive<N> = N = 42.

Finally, the compiler checks whether the argument 42 satisfies the type Positive<N>. As seen above, Positive<N> is 42, and it passes the typecheck.

myFunc(42); // OK
Enter fullscreen mode Exit fullscreen mode

When we call the function with a negative number, like myFunc(-42), the type parameter N is inferred as N = -42, and Positive<N> = never.
The argument -42 is not a member of never, of course, and it is rejected.

myFunc(-42); // Error
Enter fullscreen mode Exit fullscreen mode

The downside of this technique is that the type error is not very helpful. In the above case, the error message is Argument of type 'number' is not assignable to parameter of type 'never'., which does not contain what constraint was not satisfied nor what to do to fix the error. Maybe throw types can be a solution for this?

Appendix

The type-level predicates IsPositive<N> and HasLength<S, L> can be implemented like these:

type IsPositive<N extends number> =
    number extends N ? boolean
  : N extends unknown ? (
      N extends 0 ? false
    : `${N}` extends `-${infer _}`? false
    : true
  )
  : never;
Enter fullscreen mode Exit fullscreen mode
type HasLength<S extends string, L extends number> = Length<S> extends L ? true : false;

type Length<S extends string> =
    string extends S ? number
  : S extends unknown ? _Length<S>
  : never;

type _Length<S extends string, C extends unknown[] = []> =
    S extends "" ? C["length"]
  : S extends `${infer _}${infer R}` ? _Length<R, [...C, unknown]>
  : never;
Enter fullscreen mode Exit fullscreen mode

Top comments (0)