- Model
- Built-in primitive decoders
- Combinators
- Extracting static types from decoders
- Built-in error reporter
interface Decoder<I, A> {
readonly decode: (i: I) => E.Either<DecodeError, A>
}Example
A decoder representing string can be defined as
import * as D from 'io-ts/Decoder'
export const string: D.Decoder<unknown, string> = {
decode: (u) => (typeof u === 'string' ? D.success(u) : D.failure(u, 'string'))
}and we can use it as follows:
import { isRight } from 'fp-ts/Either'
console.log(isRight(string.decode('a'))) // => true
console.log(isRight(string.decode(null))) // => falseMore generally the result of calling decode can be handled using fold along with pipe (which is similar to the pipeline operator)
import { pipe } from 'fp-ts/function'
import { fold } from 'fp-ts/Either'
console.log(
pipe(
string.decode(null),
fold(
// failure handler
(errors) => `error: ${JSON.stringify(errors)}`,
// success handler
(a) => `success: ${JSON.stringify(a)}`
)
)
)
// => error: {"_tag":"Of","value":{"_tag":"Leaf","actual":null,"error":"string"}}string: Decoder<unknown, string>number: Decoder<unknown, number>boolean: Decoder<unknown, boolean>UnknownArray: Decoder<unknown, Array<unknown>>UnknownRecord: Decoder<unknown, Record<string, unknown>>
We can combine these primitive decoders through combinators to build composite types which represent entities like domain models, request payloads etc. in our applications.
The literal constructor describes one or more literals.
export const MyLiteral: D.Decoder<unknown, 'a'> = D.literal('a')
export const MyLiterals: D.Decoder<unknown, 'a' | 'b'> = D.literal('a', 'b')The nullable combinator describes a nullable value
export const NullableString: D.Decoder<unknown, null | string> = D.nullable(D.string)The struct combinator describes an object with required fields.
export const Person = D.struct({
name: D.string,
age: D.number
})
console.log(isRight(Person.decode({ name: 'name', age: 42 }))) // => true
console.log(isRight(Person.decode({ name: 'name' }))) // => falseThe struct combinator will strip additional fields while decoding
console.log(Person.decode({ name: 'name', age: 42, rememberMe: true }))
// => { _tag: 'Right', right: { name: 'name', age: 42 } }The partial combinator describes an object with optional fields.
export const Person = D.partial({
name: D.string,
age: D.number
})
console.log(isRight(Person.decode({ name: 'name', age: 42 }))) // => true
console.log(isRight(Person.decode({ name: 'name' }))) // => trueThe partial combinator will strip additional fields while decoding
console.log(Person.decode({ name: 'name', rememberMe: true }))
// => { _tag: 'Right', right: { name: 'name' } }The record combinator describes a Record<string, ?>
export const MyRecord: D.Decoder<unknown, Record<string, number>> = D.record(D.number)
console.log(isRight(MyRecord.decode({ a: 1, b: 2 }))) // => trueThe array combinator describes an array Array<?>
export const MyArray: D.Decoder<unknown, Array<number>> = D.array(D.number)
console.log(isRight(MyArray.decode([1, 2, 3]))) // => trueThe tuple combinator describes a n-tuple
export const MyTuple: D.Decoder<unknown, [string, number]> = D.tuple(D.string, D.number)
console.log(isRight(MyTuple.decode(['a', 1]))) // => trueThe tuple combinator will strip additional components while decoding
console.log(MyTuple.decode(['a', 1, true])) // => { _tag: 'Right', right: [ 'a', 1 ] }The intersect combinator is useful in order to mix required and optional props
export const Person = pipe(
D.struct({
name: D.string
}),
D.intersect(
D.partial({
age: D.number
})
)
)
console.log(isRight(Person.decode({ name: 'name' }))) // => true
console.log(isRight(Person.decode({}))) // => falseThe sum combinator describes tagged unions (aka sum types)
export const MySum: D.Decoder<
unknown,
| {
type: 'A'
a: string
}
| {
type: 'B'
b: number
}
// v--- tag name
> = D.sum('type')({
// +----- all union members in the dictionary must own a field named like the chosen tag ("type" in this case)
// |
// v v----- this value must be equal to its corresponding dictionary key ("A" in this case)
A: D.struct({ type: D.literal('A'), a: D.string }),
// v----- this value must be equal to its corresponding dictionary key ("B" in this case)
B: D.struct({ type: D.literal('B'), b: D.number })
})non-string tag values
In case of non-string tag values, the respective key must be enclosed in brackets
export const MySum: D.Decoder<
unknown,
| {
type: 1 // non-`string` tag value
a: string
}
| {
type: 2 // non-`string` tag value
b: number
}
> = D.sum('type')({
[1]: D.struct({ type: D.literal(1), a: D.string }),
[2]: D.struct({ type: D.literal(2), b: D.number })
})The union combinator describes untagged unions
const MyUnion = D.union(D.string, D.number)
console.log(isRight(MyUnion.decode('a'))) // => true
console.log(isRight(MyUnion.decode(1))) // => true
console.log(isRight(MyUnion.decode(null))) // => falseThe lazy combinator allows to define recursive and mutually recursive decoders
Recursive
interface Category {
title: string
subcategory: null | Category
}
const Category: D.Decoder<unknown, Category> = D.lazy('Category', () =>
D.struct({
title: D.string,
subcategory: D.nullable(Category)
})
)Mutually recursive
interface Foo {
foo: string
bar: null | Bar
}
interface Bar {
bar: number
foo: null | Foo
}
const Foo: D.Decoder<unknown, Foo> = D.lazy('Foo', () =>
D.struct({
foo: D.string,
bar: D.nullable(Bar)
})
)
const Bar: D.Decoder<unknown, Bar> = D.lazy('Bar', () =>
D.struct({
bar: D.number,
foo: D.nullable(Foo)
})
)The refine combinator allows to define refinements, for example a branded type
import { pipe } from 'fp-ts/function'
export interface PositiveBrand {
readonly Positive: unique symbol
}
export type Positive = number & PositiveBrand
export const Positive: D.Decoder<unknown, Positive> = pipe(
D.number,
D.refine((n): n is Positive => n > 0, 'Positive')
)
console.log(isRight(Positive.decode(1))) // => true
console.log(isRight(Positive.decode(-1))) // => falseThe parse combinator is more powerful than refine in that you can change the output type
import { pipe } from 'fp-ts/function'
import { isRight } from 'fp-ts/Either'
export const NumberFromString: D.Decoder<unknown, number> = pipe(
D.string,
D.parse((s) => {
const n = parseFloat(s)
return isNaN(n) ? D.failure(s, 'NumberFromString') : D.success(n)
})
)
console.log(isRight(NumberFromString.decode('1'))) // => true
console.log(isRight(NumberFromString.decode('a'))) // => falseStatic types can be extracted from decoders using the TypeOf and InputOf operators
export const Person = D.struct({
name: D.string,
age: D.number
})
export type Person = D.TypeOf<typeof Person>
/*
type Person = {
name: string;
age: number;
}
*/
type PersonInputType = D.InputOf<typeof Person>
/*
type PersonInputType = unknown
*/Note that you can define an interface instead of a type alias
export interface Person extends D.TypeOf<typeof Person> {}import { isLeft } from 'fp-ts/Either'
export const Person = D.struct({
name: D.string,
age: D.number
})
const result = Person.decode({})
if (isLeft(result)) {
console.log(D.draw(result.left))
}
/*
required property "name"
└─ cannot decode undefined, should be string
required property "age"
└─ cannot decode undefined, should be number
*/