TypeScript tutorial by example: implementing a well-typed validation library
Tags: typescript, javascript, validation, tutorial
Published: 2020-01-08 00:57:00

Throughout this article, we will be examining and explaining some of [ValidTs](https://github.com/mskv/valid-ts/)'s code. Even an experienced TypeScript user may learn a trick or two. The reader is expected to have a general understanding of the language.

# TLDR

Here is a list of links to some interesting TypeScript features that we will be using:

- [`unknown` type](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-0.html#new-unknown-top-type)
- [tagged union](https://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions)
- [conditional types](https://www.typescriptlang.org/docs/handbook/advanced-types.html#conditional-types)
- [type guards](https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards)
- [assertion functions](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#assertion-functions)
- [`const` assertions](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions)
- [tuple type inference from generic rest parameters](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-0.html#generic-rest-parameters)
- [function overloads](https://www.typescriptlang.org/docs/handbook/functions.html#overloads)
- [index types](https://www.typescriptlang.org/docs/handbook/advanced-types.html#index-types-and-index-signatures)
- [mapped types](https://www.typescriptlang.org/docs/handbook/advanced-types.html#mapped-types)

# Validation in general

When we deal with any external data source, we can make no assumptions about the data received. It is quite common to see a web server's private API just cast the result of `JSON.parse` to some known type, or even leave it as `any`. An example explanation for doing that may go as follows: "This is a private API anyway and the same team works on the client-side code". It is quite convenient when you are just hacking around, but not very scalable. Best case scenario, invalid client requests end up as "cannot read X of undefined" in server-side error reporting. Worst case, something unexpected happens.

`JSON.parse` has always returned `any`. However, I would say that with [the introduction of `unknown`](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-0.html#new-unknown-top-type) type to TypeScript it seems that `unknown` would be a better-fitting return type for it. `any` encourages people to use something in any way, while `unknown` requires some work. If you want to see an example of how a statically typed language handles JSON parsing, take a gander at [Elm's JSON Decoders](https://guide.elm-lang.org/effects/json.html). The idea behind the [ValidTs](https://github.com/mskv/valid-ts/) library is to allow the user to easily define validators that safely turn `any` into concrete types.

# Result type

All validators return a [result](https://github.com/mskv/valid-ts/blob/fb2028a/src/result.ts). It is either a success or an error. We use a [tagged union](https://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions) to define it, as it is very easy for TypeScript to infer properly. 

```
enum ResultKind { Ok, Err }

type Ok<T> = { kind: ResultKind.Ok; value: T };
type AnyOk = Ok<any>;
type Err<T> = { kind: ResultKind.Err; value: T };
type AnyErr = Err<any>;

type Result<O, E> = Ok<O> | Err<E>;
type AnyResult = AnyOk | AnyErr;
```

Note that an `enum` defined like this will use integers in place of `Ok` and `Err`.

With the introduction of [conditional types](https://www.typescriptlang.org/docs/handbook/advanced-types.html#conditional-types), it is easy to turn `Ok<number> | Err<"invalid_number">` into `Ok<number>` with `FilterOk` or `Err<"invalid_number">` with `FilterErr`.

```
type FilterOk<T extends AnyResult> = Extract<T, { kind: ResultKind.Ok }>;
type FilterErr<T extends AnyResult> = Extract<T, { kind: ResultKind.Err }>;
```

We also define another helper that just turns `Ok<number>` into `number` or `Err<"invalid_number">` into `"invalid_number"`.

```
type UnwrapOk<O extends AnyOk> = O["value"];
type UnwrapErr<E extends AnyErr> = E["value"];
```

Instead of a comparison `result.kind === ResultKind.Ok` we might want to use a helper function. Here is the definition of our [type guard](https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards).

```
const isOk = <R extends AnyResult>(result: R): 
  result is FilterOk<R> => result.kind === ResultKind.Ok;
```

With [TypeScript 3.7](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#assertion-functions) we can also define analogous assertions.

```
function assertOk<R extends AnyResult>(result: R): 
  asserts result is FilterOk<R> {
    if (!isOk(result)) { throw new Error("Expected Ok"); }
  }
```

Armed with those helpers we can progress to our validators.

# Validator type

We define our [validator](https://github.com/mskv/valid-ts/blob/711c97f/src/validators/validator.ts) as a function that accepts any value and returns some result.

```
type Validator<I, O extends AnyResult> = (input: I) => O;
```

The idea about returning a `Result` instead of a `boolean` to indicate the result of the validation is that we want to allow our validators to change their input and return the result of that change as their successful output. This will make them more flexible by allowing casting/coercion of input to happen inside them.

Again, using [conditional types](https://www.typescriptlang.org/docs/handbook/advanced-types.html#conditional-types), we are able to get the input and output types of our validators whenever we need.

```
type ExtractValidatorI<V> = 
  V extends Validator<infer I, any> ? I : never;
type ExtractValidatorO<V> = 
  V extends Validator<any, infer O> ? O : never;
```

# Simple validators

Let us start by implementing a simple [equality validator](https://github.com/mskv/valid-ts/blob/fec2e16/src/validators/eq.ts). To implement any validator, all we need to do is to satisfy the `Validator<I, O>` interface listed above. The equality validator accepts any input. If the input matches the expected value, it returns `Ok<T>`. Otherwise, it will report `Err<"equality_error">`.

```
type EqOutput<T> = Ok<T> | Err<"equality_error">;

const eq = <T>(expectedValue: T): Validator<any, EqOutput<T>> =>
  (input) => input === expectedValue 
    ? ok(input) 
    : err("equality_error");
```

That is it. Now any value that succeeds the equality check will be correctly typed. For example:

```
const validator = eq("some_const_string" as const)
const validation = validator(<input>)

if (isOk(validation)) {
  // validation.value is correctly typed to "some_const_string"
} else {
  // validation.value is correctly typed to "equality_error"
}
```

Note the use of `as const` available from [Typescript 3.4](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions) onwards. Thanks to it, the expression `"some_const_string"` is typed as `"some_const_string"` instead of just `string`. It is a very useful tool for any constant value, not just strings.

Take a quick look at [`incl`](https://github.com/mskv/valid-ts/blob/fec2e165c759ffcf2b7078c637577adf14c80685/src/validators/incl.ts), [`number`, `string`, `boolean`](https://github.com/mskv/valid-ts/blob/fec2e165c759ffcf2b7078c637577adf14c80685/src/validators/primitives.ts), [`optional`](https://github.com/mskv/valid-ts/blob/fec2e165c759ffcf2b7078c637577adf14c80685/src/validators/optional.ts) and [`nullable`](https://github.com/mskv/valid-ts/blob/fec2e165c759ffcf2b7078c637577adf14c80685/src/validators/nullable.ts) to see other simple validator examples.

# Complex validators 

### "Or" validator

Let us try to tackle the [`or` validator](https://github.com/mskv/valid-ts/blob/41264c2/src/validators/or.ts) first. Here is the usage example:

```
const validator = or(string, number, boolean)
const validation = validator(<input>)

if (isOk(validation)) {
  // validation.value is correctly typed to `string | number | boolean`
} else {
  // validation.value is correctly typed to
  // {
  //   kind: "all_failed",
  //   errors: Array<
  //     "string_error" | "number_error" | "boolean_error"
  //   >
  // }
}
```

As we can see, `or` validator constructor is a variadic function - it has infinite arity. Its return type is a `Validator<OrInput, OrOutput>`. To type `OrInput` and `OrOutput`, we need to look at the validators passed to the constructor. 

Here is a trick: to turn the tuple `[boolean, string]` into a union type `boolean | string` (or an array `Array<boolean | string>` into `boolean | string`), you can pick `[number]` from it: `[boolean, string][number]`. We will use this to get combined `Ok` and `Err` types from all the different validators passed to `or`. 

Let us now define the `or` validator constructor:

```
const or = <Vs extends AnyValidator[]>(...validators: Vs):
  Validator<OrInput<Vs>, OrOutput<Vs>> => {
    // (...)
  }
```

As promised, it is a variadic function that returns a validator. Using the trick mentioned above and our `ExtractValidatorI` helper, we can define the input of the combined validator as an alternative of all the validator inputs passed to the constructor:

```
type OrInput<Vs extends AnyValidator[]> = 
  ExtractValidatorI<Vs[number]>;
```

Typing the output is a bit more complicated. We want an alternative of all the successes or all the errors wrapped in "all failed" error. We can take advantage of all the helpers defined above: `ExtractValidatorO`, `FilterOk`, `FilterErr` and `UnwrapErr`. Take a look at the final result:

```
type OrOutput<Vs extends AnyValidator[]> = 
  OrOutputOk<Vs> | OrOutputErr<Vs>;
type OrOutputOk<Vs extends AnyValidator[]> = 
  FilterOk<ExtractValidatorO<Vs[number]>>;
type OrOutputErr<Vs extends AnyValidator[]> =
  Err<
    {
      kind: "all_failed",
      errors: Array<
        UnwrapErr<FilterErr<ExtractValidatorO<Vs[number]>>>
      >,
    }
  >;
```

That is it! We have just defined a function that accepts an infinite number of arguments and correctly infers the input, success and error types of the generated validator based on those arguments. Note how nicely it composes with all the other validator functions we have. Also note that there is nothing stopping us from passing any custom validator to `or`, even an anonymous function.

### "And" validator

Our [`and` validator](https://github.com/mskv/valid-ts/blob/7ddc22d/src/validators/and.ts) works similarly to the `&&` operator. It constructs a validator that reports the first error encountered. If no error occurs, the output of the last validator is returned. Each validator feeds its output as input to the next one. I am not too well-versed in functional programming, but I would say that it works not unlike [Kleisli composition of the Either monad](https://hackage.haskell.org/package/base-4.6.0.1/docs/Control-Monad.html#v:-62--61--62-). Here is the usage example:

```
const validator = and(string, (str) => {
  // Note that `str` is typed as `string`
  const parsed = parseInt(str)

  return Number.isNan(parsed) 
    ? err("cast_integer_error" as const) 
    : ok(parsed)
})
const validation = validator("123")

if (isOk(validation)) {
  // validation.value is typed as `number` 
  // and has value of `123`
} else {
  // validation.value is typed as 
  // `"string_error" | "cast_integer_error"`
}
```

It is quite complicated to express the "each validator feeds its outputs as input to the next one" part. For instance, we want the hypothetical `and(string, moreThan(3))` to fail at compile time, assuming that `string` validator outputs a value of type `string` and `moreThan(3)` expects an input of type `number`. 

I have found no other way to achieve this other than by extensive use of [function overloads](https://www.typescriptlang.org/docs/handbook/functions.html#overloads) and defining each possible case separately for every arity:

```
interface And {
  // (...)
  // case for arity 4
  // case for arity 3
  // case for arity 2
  // case for infinite arity
}

export const and: And = (...validators: any) => {
  // (...)
}
```

Here is what I have done for arity of two:

```
<
  I1, 
  O1 extends AnyResult, 
  I2 extends UnwrapOk<FilterOk<O1>>, 
  O2 extends AnyResult
>(v1: Validator<I1, O1>, v2: Validator<I2, O2>): 
  Validator<I1, O2 | FilterErr<O1>>
```

The important parts to see are `I2 extends UnwrapOk<FilterOk<O1>>` (which ensures that the second validator expects to receive the successful output of the previous validator as its input) and `Validator<I1, O2 | FilterErr<O1>>` (which tells us what the resulting validator expects and returns).

We cannot define a case for every arity. I have defined a compromise catch-all case to handle infinite arity at the expense of validating the part that "the next validator expects to receive the successful output of the previous validator as its input".

```
<Vs extends AnyValidator[]>(...validators: Vs): Validator<
  ExtractValidatorI<Vs[0]>, 
  FilterOk<ExtractValidatorO<LastTupleElem<Vs>>> | 
    FilterErr<ExtractValidatorO<Vs[number]>>
>;
```

As you can see, we have replaced `I1` from the previous example with `ExtractValidatorI<Vs[0]>`. Since [TypeScript 3.0](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-0.html#generic-rest-parameters) generic variadic arguments are treated as tuples. In the example above, the generic `Vs` type gets inferred as a tuple and we can pick the first element from it: `Vs[0]`.

`O2 |` has been replaced with `FilterOk<ExtractValidatorO<LastTupleElem<Vs>>> |`. It takes the last element of the `Vs` tuple, extracts the output of that validator and filters its success. `LastTupleElem` is quite interesting here. To implement that, I have stolen a [trick from SimpleTyped library](https://github.com/andnp/SimplyTyped/blob/eb2a6f973005f5bde73f096499938f7ad286c3f8/src/types/numbers.ts#L23).

```
type Prev<T extends number> = [-1, 0, 1, 2, 3, 4, 5, 6, 7, (...)
type Length<T extends any[]> = T["length"];
type LastTupleElem<T extends any[]> = T[Prev<Length<T>>];
```

There we go! We have got a very powerful tool to express a set of validations that can also include casting and coercing. We can define a whole pipeline to be performed on a particular value.

### "Shape" validator

The last validator we will examine is the [`shape` validator](https://github.com/mskv/valid-ts/blob/fec2e16/src/validators/shape.ts). It allows defining a validator based on the given object shape. As always, the type of successful and erroneous validation all get correctly inferred. For example:

```
const validator = shape({
  name: string,
  age: and(string, (str) => {
    const parsed = parseInt(str)

    return Number.isNan(parsed) 
      ? err("cast_integer_error" as const) 
      : ok(parsed)
  })
})
const validation = validator(<anything>)

if (isOk(validation)) {
  // validation.value is typed as `{ name: string, age: number}`
} else {
  // validation.value is typed as
  // {
  //   kind: "shape_error",
  //   errors: Array<
  //     { field: "name", error: "string_error" },
  //     { field: "age", error: "string_error" | 
  //       "cast_integer_error" },
  //   >
  // }
}
```

As seen from the usage, all revolves around the schema definition. We will soon find out what its type is. However, let us first define the `shape` validator constructor as a function that accepts a schema and returns a validator with its output inferred from the schema:

```
const shape = <S extends Schema>(schema: S): 
  Validator<any, ShapeOutput<S>> => (input) => { (...) }
```

As we see above, a `Schema` is just a mapping from field to field's validator. We can achieve that with an [index type](https://www.typescriptlang.org/docs/handbook/advanced-types.html#index-types-and-index-signatures):

```
type Schema = { [field: string]: AnyValidator };
```

The `ShapeOutput` is defined as a union of `ShapeOutputOk` and `ShapeOutputErr`:

```
type ShapeOutput<S extends Schema> = 
  ShapeOutputOk<S> | ShapeOutputErr<S>;
```

The definition of `ShapeOutputOk` takes advantage of the helper functions we already know and [mapped types](https://www.typescriptlang.org/docs/handbook/advanced-types.html#mapped-types):

```
type ShapeOutputOk<S extends Schema> = Ok<
  { [K in keyof S]: UnwrapOk<FilterOk<ExtractValidatorO<S[K]>>> }
>;
```

What we do with `ShapeOutputErr` is more complicated. Let us start with the end result:

```
type ShapeOutputErr<S extends Schema> =
  Err<
    {
      kind: "shape_error",
      errors: Array<{
        [K in keyof S]: {
          field: K,
          error: UnwrapErr<FilterErr<ExtractValidatorO<S[K]>>>,
        }
      }[keyof S]>,
    }
  >
```

What happens is the following:

1. We have a schema:

```
{
  name: Validator<
    any, 
    Ok<string> | Err<"string_error">
  >,
  age: Validator<
    any, 
    Ok<number> | Err<"string_error"> | Err<"cast_integer_error">
  >,
}
```

2. We turn it into:

```
{
  name: { 
    field: "name", 
    error: "string_error" 
  },
  age: { 
    field: "name", 
    error: "string_error" | "cast_integer_error" 
  },
}
```

by utilising:

```
{
  [K in keyof S]: {
    field: K,
    error: UnwrapErr<FilterErr<ExtractValidatorO<S[K]>>>,
  }
}
```

3. Then we turn it into:

```
{ field: "name", error: "string_error" } |
  { field: "age", error: "string_error" | "cast_integer_error" }
```

by picking the fields with `[keyof S]`.

4. Lastly, we wrap it in `Array<T>`.

That would be all for this complicated case. With `or`, `eq` and `shape` you can do wonky stuff, for instance automatically infer a union type:

```
const reservationCommandValidator = or(
  shape({
    kind: eq("RequestTicketReservation" as const),
    ticketId: number
  }),
  shape({
    kind: eq("RevokeTicketReservation" as const),
    reservationId: string
  }),
  shape({
    kind: eq("ArchiveTicketReservation" as const),
    reservationId: string
  })
);
```

I can imagine a single backend endpoint handling a successfully validated reservation request with ease and confidence.

Check out some other complicated validators: [`all`](https://github.com/mskv/valid-ts/blob/fec2e16/src/validators/all.ts), [`array`](https://github.com/mskv/valid-ts/blob/fec2e16/src/validators/array.ts) and [`dict`](https://github.com/mskv/valid-ts/blob/fec2e16/src/validators/dict.ts).

# Postlude

I hope that this proves useful to someone. I find myself benefit from the features described above quite often. The more you manage to change `any` into a concrete type, or `string` into something like `"RequestTicketReservation"`, the more maintainable and bug-proof your codebase gets.