The idea
A value of type Type<T>
(called "runtime type") is the runtime representation of the static type T
:
class Type<T> {
constructor(public readonly name: string, public readonly validate: Validate<T>) {}
is(x: any): x is T
}
where Validate<T>
is a specific validation function for T
type Validate<T> = (value: any, context: Context) => Either<Array<ValidationError>, T>;
Example
A runtime type representing string
can be defined as
import { Right, Left } from 'fp-ts/lib/Either'
import * as t from 'io-ts'
const string = new t.Type<string>(
'string',
(value, context) => typeof value === 'string' ? new Right(value) : new Left([{ value, context }])
)
A runtime type can be used to validate an object in memory (for example an API payload)
const Person = t.interface({
name: t.string,
age: t.number
})
t.validate(JSON.parse('{"name":"Giulio","age":43}'), Person)
t.validate(JSON.parse('{"name":"Giulio"}'), Person)
Error reporters
A reporter implements the following interface
interface Reporter<A> {
report: (validation: Validation<any>) => A;
}
This package exports two default reporters
PathReporter: Reporter<Array<string>>
ThrowReporter: Reporter<void>
Example
import { PathReporter, ThrowReporter } from 'io-ts/reporters/default'
const validation = t.validate({"name":"Giulio"}, Person)
console.log(PathReporter.report(validation))
ThrowReporter.report(validation)
TypeScript integration
Runtime types can be inspected
![instrospection](https://github.com/gcanti/io-ts/raw/HEAD/docs/images/introspection.png)
This library uses TypeScript extensively. Its API is defined in a way which automatically infers types for produced values
![inference](https://github.com/gcanti/io-ts/raw/HEAD/docs/images/inference.png)
Note that the type annotation isn't needed, TypeScript infers the type automatically based on a schema.
Static types can be extracted from runtime types with the TypeOf
operator
type IPerson = t.TypeOf<typeof Person>
type IPerson = {
name: string,
age: number
}
Note that recursive types can't be inferred
type ICategory = {
name: string,
categories: Array<ICategory>
}
const Category = t.recursion<ICategory>('Category', self => t.object({
name: t.string,
categories: t.array(self)
}))
Implemented types / combinators
import * as t from 'io-ts'
Type | TypeScript annotation syntax | Runtime type / combinator |
---|
null | null | t.null |
undefined | undefined | t.undefined |
string | string | t.string |
number | number | t.number |
boolean | boolean | t.boolean |
any | any | t.any |
never | never | t.never |
integer | ✘ | t.Integer |
generic array | Array<any> | t.Array |
generic dictionary | { [key: string]: any } | t.Dictionary |
function | Function | t.Function |
instance of C | C | t.instanceOf(C) |
arrays | Array<A> | t.array(A) |
literal | 's' | t.literal('s') |
maybe | `A | null` |
partial | Partial<{ name: string }> | t.partial({ name: t.string }) |
readonly | Readonly<{ name: string }> | t.readonly({ name: t.string }) |
dictionaries | { [key: A]: B } | t.dictionary(A, B) |
refinement | ✘ | t.refinement(A, predicate) |
interface | { name: string } | t.interface({ name: t.string }) |
tuple | [A, B] | t.tuple([A, B]) |
union | `A | B` |
intersection | A & B | t.intersection([A, B]) |
keyof | keyof M | t.keyof(M) |
recursive types | | t.recursion(name, definition) |
Custom types
You can define your own types. Let's see some examples
import * as t from 'io-ts'
import { pathReporterFailure } from 'io-ts/lib/reporters/default'
const ISODate = new t.Type<Date>(
'ISODate',
(v, c) => t.string.validate(v, c).chain(s => {
const d = new Date(s)
return isNaN(d.getTime()) ? t.failure<Date>(s, c) : t.success(d)
})
)
const s = new Date(1973, 10, 30).toISOString()
t.validate(s, ISODate).fold(pathReporterFailure, x => [String(x)])
t.validate('foo', ISODate).fold(pathReporterFailure, x => [String(x)])
Custom combinators
You can define your own combinators. Let's see some examples
The maybe
combinator
export function maybe<RT extends t.Any>(type: RT, name?: string): t.UnionType<[RT, typeof t.null], t.TypeOf<RT> | null> {
return t.union([type, t.null], name)
}
The brand
combinator
export function brand<T, B extends string>(type: t.Type<T>, brand: B): t.Type<T & { readonly __brand: B }> {
return type as any
}
Known issues
Due to an upstream bug, VS Code might display weird types for nested interfaces
const NestedInterface = t.interface({
foo: t.interface({
bar: t.string
})
});
type NestedInterfaceType = t.TypeOf<typeof NestedInterface>;