Composing io-ts Codecs
This post assumes that you know what io-ts is. If you want to learn about it I recommend reading this page and the docs.
It is pretty common to want to do something like take a value with an unknown
type, check that it is a string, check that the string is valid JSON, parse it if it is, and then check that that parsed value has the type that we want. For example we might want to define a function that has the type
import * as t from "io-ts";
type unknownToJsonToPerson = (
value: unknown
) => t.Validation<{ name: string; age: number }>;
We could use io-ts
and io-ts-types
to fill in the function body like this
import * as t from "io-ts";
import { flow } from "fp-ts/function";
import { JsonFromString } from "io-ts-types";
import { chain } from "fp-ts/lib/Either";
const unknownToJsonToPerson: (
value: unknown
) => t.Validation<{ name: string; age: number }> = flow(
t.string.decode,
chain(JsonFromString.decode),
chain(t.strict({ name: t.string, age: t.number }).decode)
);
Which is... OK, but we can do better. Because, in my experience, something one needs to do a lot when using io-ts
it would be nice if we could have a compose
function that we could use to compose codecs. We could then use that to do something like
import * as t from "io-ts";
import { pipe } from "fp-ts/function";
import { JsonFromString, Json } from "io-ts-types";
const unknownToJsonToPerson = pipe(
t.string,
compose(JsonFromString),
compose<unknown, Json, { name: string; age: number }>(
t.strict({ name: t.string, age: t.number })
)
);
Handily, I've written that compose
function and it looks like this!
export const compose =
<I2, A extends I2, B>(Codec2: t.Type<B, A, I2>) =>
<O1, I1, Name extends string = string>(
Codec1: t.Type<A, O1, I1>,
name?: Name
): t.Type<B, O1, I1> => {
return new t.Type<B, O1, I1>(
name || `${Codec1.name} -> ${Codec2.name}`,
(u): u is B => Codec2.is(u),
(a, c) =>
pipe(
a,
(a) => Codec1.validate(a, c),
chain((b) => Codec2.validate(b, c))
),
(b) =>
pipe(
b,
(b) => Codec2.encode(b),
(a) => Codec1.encode(a)
)
);
};
Un-handily, and as you might have noticed from my usage example, after piping to compose
once you do have to explicitly define the type of your second invocation. I don't understand why this is the case, but if you can work out a way around this leave a comment explaining how, and ideally explaining why it was a problem in the first place, and I will be very pleased with you.