Restricting Function Arguments Using Type-Level Predicates
Tags: typescript
Published: 2021-04-03 11:34:58

---
title: Restricting Function Arguments Using Type-Level Predicates
published: true
description: 
tags: TypeScript
//cover_image: https://direct_url_to_image.jpg
---

## 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:

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

function hasLength(s: string, l: string): boolean {
  return s.length === l;
}
```

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.

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

type HasLength<S extends string, L extends number> = /* snip */ ? true : false;
```

(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.

``` ts
declare function myFunc(n: number): void;

myFunc(42);    // OK
myFunc(0);     // OK
myFunc(-42);   // OK
myFunc("XXX"); // Error: "XXX" is not a number
```

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

``` ts
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
```

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.

``` ts
type Positive = 1 | 2 | 3 | ...;
```

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

``` ts
// Can we define like this?
type Positive = (N | N extends number, IsPositive<N>);
```

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](https://www.typescriptlang.org/play?#code/C4TwDgpgBAkgzgBQPZwJbFQNwgHgHJQQAewEAdgCZxRkCuAtgEYQBOAfFALwBQUfNDZi0IlyVKAQD8URkiQAbCAEMyvKAC4JI0pWq0yAazJIA7mSjSAFGv5biO8QAYLUAGZL5cCDY1QABgAkAN54AL5+2mLUfgC0wahkrqxQAPrh0u6e3raawCy02VAAlGqaZBDYLADc3NygkFDIaBjY+JG6AkysHJywiCjoWLh4HPZRUHkFLgRlFaw13BQQAMbySizQrvrLGEjm9CAAYtttYx10XeyWZJpNg60jRZqYSKgUCwfHZMuWACwATEUqnwAPQgqAAeQA0txPttLI4gfwwVAAKIsFhIFiwo7wmIApEo9GY7FAA)):

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

declare function myFunc<N extends number>(n: Positive<N>): void;
```

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.

``` ts
myFunc(42); // OK
```

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.

``` ts
myFunc(-42); // Error
```

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](https://github.com/microsoft/TypeScript/pull/40468) can be a solution for this?

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

``` ts
type IsPositive<N extends number> =
    number extends N ? boolean
  : N extends unknown ? (
      N extends 0 ? false
    : `${N}` extends `-${infer _}`? false
    : true
  )
  : never;
```

``` ts
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;
```