The idea
Blog post: "Typescript and validations at runtime boundaries" by @lorefnon
A value of type Type<A, O, I>
(called "codec") is the runtime representation of the static type A
.
Also a codec can
- decode inputs of type
I
(through decode
) - encode outputs of type
O
(through encode
) - be used as a custom type guard (through
is
)
class Type<A, O, I> {
readonly _A: A
readonly _O: O
readonly _I: I
constructor(
readonly name: string,
readonly is: (u: unknown) => u is A,
readonly validate: (input: I, context: Context) => Either<Errors, A>,
readonly encode: (a: A) => O
) {}
decode(i: I): Either<Errors, A>
}
Note. The Either
type is defined in fp-ts, a library containing implementations of
common algebraic types in TypeScript.
Example
A codec representing string
can be defined as
import * as t from 'io-ts'
const isString = (u: unknown): u is string => typeof u === 'string'
const string = new t.Type<string, string, unknown>(
'string',
isString,
(u, c) => (isString(u) ? t.success(u) : t.failure(u, c)),
t.identity
)
A codec can be used to validate an object in memory (for example an API payload)
const Person = t.type({
name: t.string,
age: t.number
})
Person.decode(JSON.parse('{"name":"Giulio","age":43}'))
Person.decode(JSON.parse('{"name":"Giulio"}'))
TypeScript compatibility
The stable version is tested against TypeScript 3.2.4.
io-ts version | required TypeScript version |
---|
1.6.x | 3.2.2+ |
1.5.3 | 3.0.1+ |
1.5.2- | 2.7.2+ |
Note. If you are running < typescript@3.0.1
you have to polyfill unknown
.
You can use unknown-ts as a polyfill.
Error reporters
A reporter implements the following interface
interface Reporter<A> {
report: (validation: Validation<any>) => A
}
This package exports a default PathReporter
reporter
Example
import { PathReporter } from 'io-ts/lib/PathReporter'
const result = Person.decode({ name: 'Giulio' })
console.log(PathReporter.report(result))
You can define your own reporter. Errors
has the following type
interface ContextEntry {
readonly key: string
readonly type: Decoder<any, any>
}
interface Context extends ReadonlyArray<ContextEntry> {}
interface ValidationError {
readonly value: unknown
readonly context: Context
}
interface Errors extends Array<ValidationError> {}
Example
import * as t from 'io-ts'
const getPaths = <A>(v: t.Validation<A>): Array<string> => {
return v.fold(errors => errors.map(error => error.context.map(({ key }) => key).join('.')), () => ['no errors'])
}
const Person = t.type({
name: t.string,
age: t.number
})
console.log(getPaths(Person.decode({})))
Custom error messages
You can set your own error message by providing a message
argument to failure
Example
const NumberFromString = new t.Type<number, string, unknown>(
'NumberFromString',
t.number.is,
(u, c) =>
t.string.validate(u, c).chain(s => {
const n = +s
return isNaN(n) ? t.failure(u, c, 'cannot parse to a number') : t.success(n)
}),
String
)
console.log(PathReporter.report(NumberFromString.decode('a')))
- io-ts-types - A collection of codecs and combinators for use with
io-ts
- io-ts-reporters - Error reporters for io-ts
- geojson-iots - codecs for GeoJSON as defined in rfc7946 made with
io-ts
- graphql-to-io-ts - Generate typescript and cooresponding io-ts types from a graphql
schema
TypeScript integration
codecs can be inspected
This library uses TypeScript extensively. Its API is defined in a way which automatically infers types for produced
values
Note that the type annotation isn't needed, TypeScript infers the type automatically based on a schema.
Static types can be extracted from codecs using the TypeOf
operator
type Person = t.TypeOf<typeof Person>
type Person = {
name: string
age: number
}
Implemented types / combinators
import * as t from 'io-ts'
Type | TypeScript | codec / combinator |
---|
null | null | t.null or t.nullType |
undefined | undefined | t.undefined |
void | void | t.void or t.voidType |
string | string | t.string |
number | number | t.number |
boolean | boolean | t.boolean |
unknown | unknown | t.unknown |
never | never | t.never |
object | object | t.object |
array of unknown | Array<unknown> | t.UnknownArray |
array of type | Array<A> | t.array(A) |
record of unknown | Record<string, unknown> | t.UnknownRecord |
record of type | Record<K, A> | t.record(K, A) |
function | Function | t.Function |
literal | 's' | t.literal('s') |
partial | Partial<{ name: string }> | t.partial({ name: t.string }) |
readonly | Readonly<A> | t.readonly(A) |
readonly array | ReadonlyArray<A> | t.readonlyArray(A) |
type alias | type T = { name: A } | t.type({ name: A }) |
tuple | [ A, B ] | t.tuple([ A, B ]) |
union | A | B | t.union([ A, B ]) or t.taggedUnion(tag, [ A, B ]) |
intersection | A & B | t.intersection([ A, B ]) |
keyof | keyof M | t.keyof(M) |
recursive types | ✘ | t.recursion(name, definition) |
branded types / refinements | ✘ | t.brand(A, predicate, brand) |
integer | ✘ | t.Int (built-in branded codec) |
exact types | ✘ | t.exact(type) |
strict | ✘ | t.strict({ name: A }) (an alias of t.exact(t.type({ name: A }))) |
Recursive types
Recursive types can't be inferred by TypeScript so you must provide the static type as a hint
interface Category {
name: string
categories: Array<Category>
}
const Category: t.RecursiveType<t.Type<Category>> = t.recursion('Category', () =>
t.type({
name: t.string,
categories: t.array(Category)
})
)
Mutually recursive types
interface Foo {
type: 'Foo'
b: Bar | undefined
}
interface Bar {
type: 'Bar'
a: Foo | undefined
}
const Foo: t.RecursiveType<t.Type<Foo>> = t.recursion('Foo', () =>
t.interface({
type: t.literal('Foo'),
b: t.union([Bar, t.undefined])
})
)
const Bar: t.RecursiveType<t.Type<Bar>> = t.recursion('Bar', () =>
t.interface({
type: t.literal('Bar'),
a: t.union([Foo, t.undefined])
})
)
const FooBar = t.taggedUnion('type', [Foo, Bar])
Tagged unions
If you are encoding tagged unions, instead of the general purpose union
combinator, you may want to use the
taggedUnion
combinator in order to get better performances
const A = t.type({
tag: t.literal('A'),
foo: t.string
})
const B = t.type({
tag: t.literal('B'),
bar: t.number
})
const U = t.taggedUnion('tag', [A, B])
Branded types / Refinements
You can brand / refine a codec (any codec) using the brand
combinator
interface PositiveBrand {
readonly Positive: unique symbol
}
const Positive = t.brand(
t.number,
(n): n is t.Branded<number, PositiveBrand> => n >= 0,
'Positive'
)
type Positive = t.TypeOf<typeof Positive>
Branded codecs can be merged with t.intersection
const PositiveInt = t.intersection([t.Int, Positive])
type PositiveInt = t.TypeOf<typeof PositiveInt>
Exact types
You can make a codec exact (which means that additional properties are stripped) using the exact
combinator
const Person = t.type({
name: t.string,
age: t.number
})
const ExactPerson = t.exact(Person)
Person.decode({ name: 'Giulio', age: 43, surname: 'Canti' })
ExactPerson.decode({ name: 'Giulio', age: 43, surname: 'Canti' })
Mixing required and optional props
You can mix required and optional props using an intersection
const A = t.type({
foo: t.string
})
const B = t.partial({
bar: t.number
})
const C = t.intersection([A, B])
type C = t.TypeOf<typeof C>
type C = {
foo: string
} & {
bar?: number | undefined
}
You can apply partial
to an already defined codec via its props
field
const Person = t.type({
name: t.string,
age: t.number
})
const PartialPerson = t.partial(Person.props)
type PartialPerson = t.TypeOf<typeof PartialPerson>
type PartialPerson = {
name?: string
age?: number
}
Custom types
You can define your own types. Let's see an example
import * as t from 'io-ts'
const DateFromString = new t.Type<Date, string, unknown>(
'DateFromString',
(u): u is Date => u instanceof Date,
(u, c) =>
t.string.validate(u, c).chain(s => {
const d = new Date(s)
return isNaN(d.getTime()) ? t.failure(u, c) : t.success(d)
}),
a => a.toISOString()
)
const s = new Date(1973, 10, 30).toISOString()
DateFromString.decode(s)
DateFromString.decode('foo')
Note that you can deserialize while validating.
Generic Types
Polymorphic codecs are represented using functions.
For example, the following typescript:
interface ResponseBody<T> {
result: T
_links: Links
}
interface Links {
previous: string
next: string
}
Would be:
import * as t from 'io-ts'
const ResponseBody = <RT extends t.Mixed>(type: RT) =>
t.interface({
result: type,
_links: Links
})
const Links = t.interface({
previous: t.string,
next: t.string
})
And used like:
const UserModel = t.type({
name: t.string
})
functionThatRequiresRuntimeType(ResponseBody(t.array(UserModel)), ...params)
Piping
You can pipe two codecs if their type parameters do align
const NumberDecoder = new t.Type<number, string, string>(
'NumberDecoder',
t.number.is,
(s, c) => {
const n = parseFloat(s)
return isNaN(n) ? t.failure(s, c) : t.success(n)
},
String
)
const NumberFromString = t.string.pipe(
NumberDecoder,
'NumberFromString'
)
Tips and Tricks
Is there a way to turn the checks off in production code?
No, however you can define your own logic for that (if you really trust the input)
import * as t from 'io-ts'
import { Either, right } from 'fp-ts/lib/Either'
const { NODE_ENV } = process.env
export function unsafeDecode<A, O, I>(value: I, type: t.Type<A, O, I>): Either<t.Errors, A> {
if (NODE_ENV !== 'production' || type.encode !== t.identity) {
return type.decode(value)
} else {
return right(value as any)
}
}
import { failure } from 'io-ts/lib/PathReporter'
export function unsafeGet<A, O, I>(value: I, type: t.Type<A, O, I>): A {
if (NODE_ENV !== 'production' || type.encode !== t.identity) {
return type.decode(value).getOrElseL(errors => {
throw new Error(failure(errors).join('\n'))
})
} else {
return value as any
}
}
Union of string literals
Use keyof
instead of union
when defining a union of string literals
const Bad = t.union([
t.literal('foo'),
t.literal('bar'),
t.literal('baz')
])
const Good = t.keyof({
foo: null,
bar: null,
baz: null
})
Benefits
- unique check for free
- better performance,
O(log(n))
vs O(n)