- 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))) // => false
More 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 type
combinator describes an object with required fields.
export const Person = D.type({
name: D.string,
age: D.number
})
console.log(isRight(Person.decode({ name: 'name', age: 42 }))) // => true
console.log(isRight(Person.decode({ name: 'name' }))) // => false
The type
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' }))) // => true
The 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 }))) // => true
The 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]))) // => true
The 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]))) // => true
The 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.type({
name: D.string
}),
D.intersect(
D.partial({
age: D.number
})
)
)
console.log(isRight(Person.decode({ name: 'name' }))) // => true
console.log(isRight(Person.decode({}))) // => false
The 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.type({ type: D.literal('A'), a: D.string }),
// v----- this value must be equal to its corresponding dictionary key ("B" in this case)
B: D.type({ type: D.literal('B'), 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))) // => false
The 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.type({
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.type({
foo: D.string,
bar: D.nullable(Bar)
})
)
const Bar: D.Decoder<unknown, Bar> = D.lazy('Bar', () =>
D.type({
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))) // => false
The 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'))) // => false
Static types can be extracted from decoders using the TypeOf
operator
export const Person = D.type({
name: D.string,
age: D.number
})
export type Person = D.TypeOf<typeof Person>
/*
type Person = {
name: string;
age: number;
}
*/
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.type({
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
*/