The idea
Blog post: "Typescript and validations at runtime boundaries" by @lorefnon
A value of type Type<A, O, I>
(called "runtime type") is the runtime representation of the static type A
.
Also a runtime type can
- decode inputs of type
I
(through decode
) - encode outputs of type
O
(through encode
) - be used as a custom type guard (through
is
)
export type mixed = object | number | string | boolean | symbol | undefined | null
class Type<A, O = A, I = mixed> {
readonly _A: A
readonly _O: O
readonly _I: I
constructor(
readonly name: string,
readonly is: (v: mixed) => v 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 runtime type representing string
can be defined as
import * as t from 'io-ts'
export class StringType extends t.Type<string> {
readonly _tag: 'StringType' = 'StringType'
constructor() {
super(
'string',
(m): m is string => typeof m === 'string',
(m, c) => (this.is(m) ? t.success(m) : t.failure(m, c)),
t.identity
)
}
}
export const string = new StringType()
A runtime type 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 2.9.x+
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 } from 'io-ts/lib/PathReporter'
import { ThrowReporter } from 'io-ts/lib/ThrowReporter'
const result = Person.decode({ name: 'Giulio' })
console.log(PathReporter.report(result))
ThrowReporter.report(result)
- io-ts-types - A collection of runtime types and combinators for use with
io-ts
- io-ts-reporters - Error reporters for io-ts
- geojson-iots - Runtime types 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
Runtime types 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 runtime types using the TypeOf
operator
type IPerson = t.TypeOf<typeof Person>
type IPerson = {
name: string
age: number
}
Implemented types / combinators
import * as t from 'io-ts'
Type | TypeScript | Runtime type / 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 |
any | any | t.any |
never | never | t.never |
object | object | t.object |
integer | ✘ | t.Integer |
array of any | Array<mixed> | t.Array |
array of type | Array<A> | t.array(A) |
dictionary of any | { [key: string]: mixed } | t.Dictionary |
dictionary of type | { [K in A]: B } | t.dictionary(A, B) |
function | Function | t.Function |
literal | 's' | t.literal('s') |
partial | Partial<{ name: string }> | t.partial({ name: t.string }) |
readonly | Readonly<T> | t.readonly(T) |
readonly array | ReadonlyArray<number> | t.readonlyArray(t.number) |
type alias | type A = { name: string } | t.type({ name: t.string }) |
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 | see Recursive types | t.recursion(name, definition) |
refinement | ✘ | t.refinement(A, predicate) |
exact types | ✘ | t.exact(type) |
strict types (deprecated) | ✘ | t.strict({ name: t.string }) |
Recursive types
Recursive types can't be inferred by TypeScript so you must provide the static type as a hint
interface ICategory {
name: string
categories: Array<ICategory>
}
const Category = t.recursion<ICategory>('Category', Category =>
t.type({
name: t.string,
categories: t.array(Category)
})
)
Mutually recursive types
interface IFoo {
type: 'Foo'
b: IBar | undefined
}
interface IBar {
type: 'Bar'
a: IFoo | undefined
}
const Foo: t.RecursiveType<t.Type<IFoo>, IFoo> = t.recursion<IFoo>('Foo', _ =>
t.interface({
type: t.literal('Foo'),
b: t.union([Bar, t.undefined])
})
)
const Bar: t.RecursiveType<t.Type<IFoo>, IBar> = t.recursion<IBar>('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])
Refinements
You can refine a type (any type) using the refinement
combinator
const Positive = t.refinement(t.number, n => n >= 0, 'Positive')
const Adult = t.refinement(Person, person => person.age >= 18, 'Adult')
Exact types
You can make a runtime type alias exact (which means that only the given properties are allowed) 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' })
Strict types (deprecated)
Note. This combinator is deprecated, use exact
instead.
You can make a runtime type strict (which means that only the given properties are allowed) using the strict
combinator
const Person = t.type({
name: t.string,
age: t.number
})
const StrictPerson = t.strict(Person.props)
Person.decode({ name: 'Giulio', age: 43, surname: 'Canti' })
StrictPerson.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 CT = t.TypeOf<typeof C>
type CT = {
foo: string
bar?: 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>(
'DateFromString',
(m): m is Date => m instanceof Date,
(m, c) =>
t.string.validate(m, c).chain(s => {
const d = new Date(s)
return isNaN(d.getTime()) ? t.failure(s, 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.
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>(value: t.mixed, type: t.Type<A, O>): Either<t.Errors, A> {
if (NODE_ENV !== 'production' || type.encode !== t.identity) {
return type.decode(value)
} else {
return right(value as A)
}
}
import { failure } from 'io-ts/lib/PathReporter'
export function unsafeGet<A, O>(value: t.mixed, type: t.Type<A, O>): 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 A
}
}
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
- quick info stays responsive
Known issues
VS Code might display weird types for nested types
const NestedInterface = t.type({
foo: t.string,
bar: t.type({
baz: t.string
})
})
type NestedInterfaceType = t.TypeOf<typeof NestedInterface>
Solution: the clean
and alias
functions
The pattern
const _NestedInterface = t.type({
foo: t.string,
bar: t.type({
baz: t.string
})
})
export interface NestedInterface extends t.TypeOf<typeof _NestedInterface> {}
export const NestedInterface = t.clean<NestedInterface, NestedInterface>(_NestedInterface)
export const NestedInterface = t.alias(_NestedInterface)<NestedInterface, NestedInterface>()
interface NestedInterfaceProps extends t.PropsOf<typeof _NestedInterface> {}
export const NestedInterface = t.alias(_NestedInterface)<NestedInterface, NestedInterface, NestedInterfaceProps>()