Repository: gcanti/io-ts
Pull request #440 (2020-04-07 09:46:14)

[RFC] split Type into Decoder / Encoder
# 2.2.0

- **Experimental**
  - add `Codec`, `Decoder`, `Encoder`, `Guard`, `Schema`, `Schemable`, `Tree` modules


**Note**. run `npm i gcanti/io-ts#2.2` to install the release candidate

anilanar: So in theory, we can have more instances of `Schemable` similar to https://github.com/sledorze/morphic-ts with a less complex API?
gcanti: > we can have more instances of Schemable

Yes, as a proof of concept I defined (branch `v3`) an istance for:

- `Eq` (`fp-ts` library)
- `Arbitrary` (`fast-check` library)
- `JsonSchema` (any json schema library, for example `ajv`)
- `Expression` (`TypeScript`)
- `TypeNode` (`TypeScript`)

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d520e58..c0826cd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,11 @@
 **Note**: Gaps between patch versions are faulty/broken releases. **Note**: A feature tagged as Experimental is in a
 high state of flux, you're at risk of it changing without notice.
 
+# 2.2.0
+
+- **Experimental**
+  - add `Codec`, `Decoder`, `Encoder`, `Guard`, `Schema`, `Schemable`, `Tree` modules (@gcanti)
+
 # 2.1.3
 
 - **Polish**
diff --git a/Codec.md b/Codec.md
new file mode 100644
index 0000000..f3cf40d
--- /dev/null
+++ b/Codec.md
@@ -0,0 +1,60 @@
+<!-- START doctoc generated TOC please keep comment here to allow auto update -->
+<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
+
+- [Codec interface](#codec-interface)
+- [Built-in primitive codecs](#built-in-primitive-codecs)
+- [Combinators](#combinators)
+
+<!-- END doctoc generated TOC please keep comment here to allow auto update -->
+
+# Codec interface
+
+```ts
+export interface Codec<A> extends Decoder<A>, Encoder<A> {}
+```
+
+A codec is just a decoder and an encoder packed together.
+
+The following laws must hold
+
+1. `pipe(codec.decode(u), E.fold(() => u, codec.encode) = u` for all `u` in `unknown`
+2. `codec.decode(codec.encode(a)) = E.right(a)` for all `a` in `A`
+
+You can build a new codec using the `make` helper
+
+**Example**
+
+```ts
+import * as C from 'io-ts/lib/Codec'
+import * as D from 'io-ts/lib/Decoder'
+import { left, right } from 'fp-ts/lib/Either'
+
+const NumberFromString: C.Codec<number> = C.make(
+  D.parse(D.string, (s) => {
+    const n = parseFloat(s)
+    return isNaN(n) ? left(`cannot decode ${JSON.stringify(s)}, should be parsable into a number`) : right(n)
+  }),
+  { encode: String }
+)
+```
+
+# Built-in primitive codecs
+
+- `string: Decoder<string>`
+- `number: Decoder<number>`
+- `boolean: Decoder<boolean>`
+- `UnknownArray: Decoder<Array<unknown>>`
+- `UnknownRecord: Decoder<Record<string, unknown>>`
+
+# Combinators
+
+- `literal`
+- `nullable`
+- `type`
+- `partial`
+- `record`
+- `array`
+- `tuple`
+- `intersection`
+- `sum`
+- `lazy`
diff --git a/Decoder.md b/Decoder.md
new file mode 100644
index 0000000..19e5e3f
--- /dev/null
+++ b/Decoder.md
@@ -0,0 +1,345 @@
+<!-- START doctoc generated TOC please keep comment here to allow auto update -->
+<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
+
+- [Decoder interface](#decoder-interface)
+- [Built-in primitive decoders](#built-in-primitive-decoders)
+- [Combinators](#combinators)
+  - [The `literal` constructor](#the-literal-constructor)
+  - [The `nullable` combinator](#the-nullable-combinator)
+  - [The `type` combinator](#the-type-combinator)
+  - [The `partial` combinator](#the-partial-combinator)
+  - [The `record` combinator](#the-record-combinator)
+  - [The `array` combinator](#the-array-combinator)
+  - [The `tuple` combinator](#the-tuple-combinator)
+  - [The `intersection` combinator](#the-intersection-combinator)
+  - [The `sum` combinator](#the-sum-combinator)
+  - [The `lazy` combinator](#the-lazy-combinator)
+  - [The `refinement` combinator](#the-refinement-combinator)
+  - [The `parse` combinator](#the-parse-combinator)
+- [Extracting static types from decoders](#extracting-static-types-from-decoders)
+- [Built-in error reporter](#built-in-error-reporter)
+
+<!-- END doctoc generated TOC please keep comment here to allow auto update -->
+
+# Decoder interface
+
+```ts
+export interface Decoder<A> {
+  readonly decode: (u: unknown) => E.Either<NonEmptyArray<Tree<string>>, A>
+}
+```
+
+**Example**
+
+A decoder representing `string` can be defined as
+
+```ts
+import * as D from 'io-ts/lib/Decoder'
+
+export const string: D.Decoder<string> = {
+  decode: (u) =>
+    typeof u === 'string' ? D.success(u) : D.failure(`cannot decode ${JSON.stringify(u)}, should be string`)
+}
+```
+
+and we can use it as follows:
+
+```ts
+import { isRight } from 'fp-ts/lib/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`](https://gcanti.github.io/fp-ts/modules/Either.ts.html#fold-function) along with `pipe` (which is similar to the pipeline operator)
+
+```ts
+import { pipe } from 'fp-ts/lib/pipeable'
+import { fold } from 'fp-ts/lib/Either'
+
+console.log(
+  pipe(
+    string.decode(null),
+    fold(
+      // failure handler
+      (errors) => console.error(errors),
+      // success handler
+      (a) => console.log(a)
+    )
+  )
+)
+// => [ { value: 'cannot decode null, should be string', forest: [] } ]
+```
+
+# Built-in primitive decoders
+
+- `never: Decoder<never>`
+- `string: Decoder<string>`
+- `number: Decoder<number>`
+- `boolean: Decoder<boolean>`
+- `UnknownArray: Decoder<Array<unknown>>`
+- `UnknownRecord: Decoder<Record<string, unknown>>`
+
+# Combinators
+
+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
+
+The `literal` constructor describes one or more literals.
+
+```ts
+export const MyLiteral: D.Decoder<'a'> = D.literal('a')
+export const MyLiterals: D.Decoder<'a' | 'b'> = D.literal('a', 'b')
+```
+
+## The `nullable` combinator
+
+The `nullable` combinator describes a nullable value
+
+```ts
+export const NullableString: D.Decoder<null | string> = D.nullable(D.string)
+```
+
+## The `type` combinator
+
+The `type` combinator describes an object with required fields.
+
+```ts
+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
+
+```ts
+console.log(Person.decode({ name: 'name', age: 42, remeberMe: true }))
+// => { _tag: 'Right', right: { name: 'name', age: 42 } }
+```
+
+## The `partial` combinator
+
+The `partial` combinator describes an object with optional fields.
+
+```ts
+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
+
+```ts
+console.log(Person.decode({ name: 'name', remeberMe: true }))
+// => { _tag: 'Right', right: { name: 'name' } }
+```
+
+## The `record` combinator
+
+The `record` combinator describes a `Record<string, ?>`
+
+```ts
+export const MyRecord: D.Decoder<Record<string, number>> = D.record(D.number)
+
+console.log(isRight(MyRecord.decode({ a: 1, b: 2 }))) // => true
+```
+
+## The `array` combinator
+
+The `array` combinator describes an array `Array<?>`
+
+```ts
+export const MyArray: D.Decoder<Array<number>> = D.array(D.number)
+
+console.log(isRight(MyArray.decode([1, 2, 3]))) // => true
+```
+
+## The `tuple` combinator
+
+The `tuple` combinator describes a `n`-tuple
+
+```ts
+export const MyTuple: D.Decoder<[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
+
+```ts
+console.log(MyTuple.decode(['a', 1, true])) // => { _tag: 'Right', right: [ 'a', 1 ] }
+```
+
+## The `intersection` combinator
+
+The `intersection` combinator is useful in order to mix required and optional props
+
+```ts
+export const Person = D.intersection(
+  D.type({
+    name: D.string
+  }),
+  D.partial({
+    age: D.number
+  })
+)
+
+console.log(isRight(Person.decode({ name: 'name' }))) // => true
+console.log(isRight(Person.decode({}))) // => false
+```
+
+## The `sum` combinator
+
+The `sum` combinator describes tagged unions (aka sum types)
+
+```ts
+export const MySum: D.Decoder<
+  | {
+      type: 'A'
+      a: string
+    }
+  | {
+      type: 'B'
+      b: number
+    }
+> = D.sum('type')({
+  A: D.type({ type: D.literal('A'), a: D.string }),
+  B: D.type({ type: D.literal('B'), b: D.number })
+})
+```
+
+## The `lazy` combinator
+
+The `lazy` combinator allows to define recursive and mutually recursive decoders
+
+**Recursive**
+
+```ts
+interface Category {
+  title: string
+  subcategory: null | Category
+}
+
+const Category: D.Decoder<Category> = D.lazy('Category', () =>
+  D.type({
+    title: D.string,
+    subcategory: D.nullable(Category)
+  })
+)
+```
+
+**Mutually recursive**
+
+```ts
+interface Foo {
+  foo: string
+  bar: null | Bar
+}
+
+interface Bar {
+  bar: number
+  foo: null | Foo
+}
+
+const Foo: D.Decoder<Foo> = D.lazy('Foo', () =>
+  D.type({
+    foo: D.string,
+    bar: D.nullable(Bar)
+  })
+)
+
+const Bar: D.Decoder<Bar> = D.lazy('Bar', () =>
+  D.type({
+    bar: D.number,
+    foo: D.nullable(Foo)
+  })
+)
+```
+
+## The `refinement` combinator
+
+The `refinement` combinator allows to define refinements, for example a branded type
+
+```ts
+export interface PositiveBrand {
+  readonly Positive: unique symbol
+}
+
+export type Positive = number & PositiveBrand
+
+export const Positive: D.Decoder<Positive> = D.refinement(D.number, (n): n is Positive => n > 0, 'Positive')
+
+console.log(isRight(Positive.decode(1))) // => true
+console.log(isRight(Positive.decode(-1))) // => false
+```
+
+## The `parse` combinator
+
+The `parse` combinator is more powerful than `refinement` in that you can change the output type
+
+```ts
+import { left, right } from 'fp-ts/lib/Either'
+
+export const NumberFromString: D.Decoder<number> = D.parse(D.string, (s) => {
+  const n = parseFloat(s)
+  return isNaN(n) ? left(`cannot decode ${JSON.stringify(s)}, should be NumberFromString`) : right(n)
+})
+
+console.log(isRight(NumberFromString.decode('1'))) // => true
+console.log(isRight(NumberFromString.decode('a'))) // => false
+```
+
+# Extracting static types from decoders
+
+Static types can be extracted from decoders using the `TypeOf` operator
+
+```ts
+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
+
+```ts
+export interface Person extends D.TypeOf<typeof Person> {}
+```
+
+# Built-in error reporter
+
+```ts
+import { isLeft } from 'fp-ts/lib/Either'
+import { draw } from 'io-ts/lib/Tree'
+
+export const Person = D.type({
+  name: D.string,
+  age: D.number
+})
+
+const result = Person.decode({})
+if (isLeft(result)) {
+  console.log(draw(result.left))
+}
+/*
+required property "name"
+└─ cannot decode undefined, should be string
+*/
+```
diff --git a/Encoder.md b/Encoder.md
new file mode 100644
index 0000000..bc63223
--- /dev/null
+++ b/Encoder.md
@@ -0,0 +1,41 @@
+<!-- START doctoc generated TOC please keep comment here to allow auto update -->
+<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
+
+- [Encoder interface](#encoder-interface)
+- [Combinators](#combinators)
+
+<!-- END doctoc generated TOC please keep comment here to allow auto update -->
+
+# Encoder interface
+
+```ts
+export interface Encoder<A> {
+  readonly encode: (a: A) => unknown
+}
+```
+
+**Example**
+
+An encoder representing a nullable value
+
+```ts
+import * as E from 'io-ts/lib/Encoder'
+
+export function nullable<A>(or: E.Encoder<A>): E.Encoder<null | A> {
+  return {
+    encode: (a) => (a === null ? a : or.encode(a))
+  }
+}
+```
+
+# Combinators
+
+- `nullable`
+- `type`
+- `partial`
+- `record`
+- `array`
+- `tuple`
+- `intersection`
+- `sum`
+- `lazy`
diff --git a/README.md b/README.md
index cf96605..a791eed 100644
--- a/README.md
+++ b/README.md
@@ -3,29 +3,11 @@
 ![npm downloads](https://img.shields.io/npm/dm/io-ts.svg)
 [![Minified Size](https://badgen.net/bundlephobia/minzip/io-ts)](https://bundlephobia.com/result?p=io-ts)
 
-Table of contents
-
 <!-- START doctoc generated TOC please keep comment here to allow auto update -->
 <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
 
 - [Installation](#installation)
-- [The idea](#the-idea)
-- [TypeScript integration](#typescript-integration)
-- [TypeScript compatibility](#typescript-compatibility)
-- [Error reporters](#error-reporters)
-- [Custom error messages](#custom-error-messages)
-- [Implemented types / combinators](#implemented-types--combinators)
-- [Recursive types](#recursive-types)
-  - [Mutually recursive types](#mutually-recursive-types)
-- [Branded types / Refinements](#branded-types--refinements)
-- [Exact types](#exact-types)
-- [Mixing required and optional props](#mixing-required-and-optional-props)
-- [Custom types](#custom-types)
-- [Generic Types](#generic-types)
-- [Piping](#piping)
-- [Community](#community)
-- [Tips and Tricks](#tips-and-tricks)
-  - [Union of string literals](#union-of-string-literals)
+- [Documentation](#documentation)
 
 <!-- END doctoc generated TOC please keep comment here to allow auto update -->
 
@@ -39,551 +21,19 @@ npm i io-ts fp-ts
 
 Note: [`fp-ts`](https://github.com/gcanti/fp-ts) is a peer dependency for `io-ts`
 
-# The idea
-
-A value of type `Type<A, O, I>` (called "codec") is the runtime representation of the static type `A`.
-
-A codec can:
-
-- decode inputs of type `I` (through `decode`)
-- encode outputs of type `O` (through `encode`)
-- be used as a custom [type guard](https://basarat.gitbooks.io/typescript/content/docs/types/typeGuard.html) (through `is`)
-
-```ts
-class Type<A, O, I> {
-  constructor(
-    /** a unique name for this codec */
-    readonly name: string,
-
-    /** a custom type guard */
-    readonly is: (u: unknown) => u is A,
-
-    /** succeeds if a value of type I can be decoded to a value of type A */
-    readonly validate: (input: I, context: Context) => Either<Errors, A>,
-
-    /** converts a value of type A to a value of type O */
-    readonly encode: (a: A) => O
-  ) {}
-
-  /** a version of `validate` with a default context */
-  decode(i: I): Either<Errors, A>
-}
-```
-
-The [`Either`](https://gcanti.github.io/fp-ts/modules/Either.ts.html) type returned by `decode` is defined in [fp-ts](https://github.com/gcanti/fp-ts), a library containing implementations of common algebraic types in TypeScript.
-
-The `Either` type represents a value of one of two possible types (a disjoint union). An instance of `Either` is either an instance of `Left` or `Right`:
-
-```ts
-type Either<E, A> =
-  | {
-      readonly _tag: 'Left'
-      readonly left: E
-    }
-  | {
-      readonly _tag: 'Right'
-      readonly right: A
-    }
-```
-
-Convention dictates that `Left` is used for **failure** and `Right` is used for **success**.
-
-**Example**
-
-A codec representing `string` can be defined as:
-
-```ts
-import * as t from 'io-ts'
-
-const string = new t.Type<string, string, unknown>(
-  'string',
-  (input: unknown): input is string => typeof input === 'string',
-  // `t.success` and `t.failure` are helpers used to build `Either` instances
-  (input, context) => (typeof input === 'string' ? t.success(input) : t.failure(input, context)),
-  // `A` and `O` are the same, so `encode` is just the identity function
-  t.identity
-)
-```
-
-and we can use it as follows:
-
-```ts
-import { isRight } from 'fp-ts/lib/Either'
-
-isRight(string.decode('a string')) // true
-isRight(string.decode(null)) // false
-```
-
-More generally the result of calling `decode` can be handled using [`fold`](https://gcanti.github.io/fp-ts/modules/Either.ts.html#fold-function) along with `pipe` (which is similar to the pipeline operator)
-
-```ts
-import * as t from 'io-ts'
-import { pipe } from 'fp-ts/lib/pipeable'
-import { fold } from 'fp-ts/lib/Either'
-
-// failure handler
-const onLeft = (errors: t.Errors): string => `${errors.length} error(s) found`
-
-// success handler
-const onRight = (s: string) => `No errors: ${s}`
-
-pipe(t.string.decode('a string'), fold(onLeft, onRight))
-// => "No errors: a string"
-
-pipe(t.string.decode(null), fold(onLeft, onRight))
-// => "1 error(s) found"
-```
-
-We can combine these codecs through [combinators](#implemented-types--combinators) to build composite types which represent entities like domain models, request payloads etc. in our applications:
-
-```ts
-import * as t from 'io-ts'
-
-const User = t.type({
-  userId: t.number,
-  name: t.string
-})
-```
-
-So this is equivalent to defining something like:
-
-```ts
-type User = {
-  userId: number
-  name: string
-}
-```
-
-The advantage of using `io-ts` to define the runtime type is that we can validate the type at runtime, and we can also extract the corresponding static type, so we don’t have to define it twice.
-
-# TypeScript integration
+# Documentation
 
-Codecs can be inspected:
+- [API Reference](https://gcanti.github.io/io-ts/modules/)
 
-![instrospection](images/introspection.png)
+## Usage
 
-This library uses TypeScript extensively. Its API is defined in a way which automatically infers types for produced
-values
-
-![inference](images/inference.png)
-
-Note that the type annotation isn't needed, TypeScript infers the type automatically based on a schema (and comments are preserved).
-
-Static types can be extracted from codecs using the `TypeOf` operator:
-
-```ts
-type User = t.TypeOf<typeof User>
-
-// same as
-type User = {
-  userId: number
-  name: string
-}
-```
-
-# TypeScript compatibility
-
-The stable version is tested against TypeScript 3.5.2
-
-| io-ts version | required TypeScript version |
-| ------------- | --------------------------- |
-| 2.x+          | 3.5.2+                      |
-| 1.6.x+        | 3.2.2+                      |
-| 1.5.3         | 3.0.1+                      |
-| 1.5.2-        | 2.7.2+                      |
-
-**Note**. This library is conceived, tested and is supposed to be consumed by TypeScript with the `strict` flag turned on.
-
-**Note**. If you are running `< typescript@3.0.1` you have to polyfill `unknown`.
-
-You can use [unknown-ts](https://github.com/gcanti/unknown-ts) as a polyfill.
-
-# Error reporters
-
-A reporter implements the following interface
-
-```ts
-interface Reporter<A> {
-  report: (validation: Validation<any>) => A
-}
-```
-
-This package exports a default `PathReporter` reporter
-
-Example
-
-```ts
-import { PathReporter } from 'io-ts/lib/PathReporter'
-
-const result = User.decode({ name: 'Giulio' })
-
-console.log(PathReporter.report(result))
-// => [ 'Invalid value undefined supplied to : { userId: number, name: string }/userId: number' ]
-```
-
-You can define your own reporter. `Errors` has the following type
-
-```ts
-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
-
-```ts
-import { pipe } from 'fp-ts/lib/pipeable'
-import { fold } from 'fp-ts/lib/Either'
-
-const getPaths = <A>(v: t.Validation<A>): Array<string> => {
-  return pipe(
-    v,
-    fold(
-      errors => errors.map(error => error.context.map(({ key }) => key).join('.')),
-      () => ['no errors']
-    )
-  )
-}
-
-console.log(getPaths(User.decode({}))) // => [ '.userId', '.name' ]
-```
-
-# Custom error messages
-
-You can set your own error message by providing a `message` argument to `failure`
-
-Example
-
-```ts
-import { either } from 'fp-ts/lib/Either'
-
-const NumberFromString = new t.Type<number, string, unknown>(
-  'NumberFromString',
-  t.number.is,
-  (u, c) =>
-    either.chain(t.string.validate(u, c), 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')))
-// => ['cannot parse to a number']
-```
+**Experimental features** (version `2.2+`)
 
-You can also use the [`withMessage`](https://gcanti.github.io/io-ts-types/modules/withMessage.ts.html) helper from [io-ts-types](https://github.com/gcanti/io-ts-types)
+- [`Decoder`](Decoder.md)
+- [`Encoder`](Encoder.md)
+- [`Codec`](Codec.md)
+- [`Schema` (advanced feature)](Schema.md)
 
-# Implemented types / combinators
+**Stable** (old)
 
-| 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`                                                          |
-| 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 ])`                                                  |
-| intersection                | `A & B`                     | `t.intersection([ A, B ])`                                           |
-| keyof                       | `keyof M`                   | `t.keyof(M)` (**only supports string keys**)                         |
-| 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
-
-```ts
-interface Category {
-  name: string
-  categories: Array<Category>
-}
-
-const Category: t.Type<Category> = t.recursion('Category', () =>
-  t.type({
-    name: t.string,
-    categories: t.array(Category)
-  })
-)
-```
-
-## Mutually recursive types
-
-```ts
-interface Foo {
-  type: 'Foo'
-  b: Bar | undefined
-}
-
-interface Bar {
-  type: 'Bar'
-  a: Foo | undefined
-}
-
-const Foo: t.Type<Foo> = t.recursion('Foo', () =>
-  t.type({
-    type: t.literal('Foo'),
-    b: t.union([Bar, t.undefined])
-  })
-)
-
-const Bar: t.Type<Bar> = t.recursion('Bar', () =>
-  t.type({
-    type: t.literal('Bar'),
-    a: t.union([Foo, t.undefined])
-  })
-)
-```
-
-# Branded types / Refinements
-
-You can brand / refine a codec (_any_ codec) using the `brand` combinator
-
-```ts
-// a unique brand for positive numbers
-interface PositiveBrand {
-  readonly Positive: unique symbol // use `unique symbol` here to ensure uniqueness across modules / packages
-}
-
-const Positive = t.brand(
-  t.number, // a codec representing the type to be refined
-  (n): n is t.Branded<number, PositiveBrand> => n >= 0, // a custom type guard using the build-in helper `Branded`
-  'Positive' // the name must match the readonly field in the brand
-)
-
-type Positive = t.TypeOf<typeof Positive>
-/*
-same as
-type Positive = number & t.Brand<PositiveBrand>
-*/
-```
-
-Branded codecs can be merged with `t.intersection`
-
-```ts
-// t.Int is a built-in branded codec
-const PositiveInt = t.intersection([t.Int, Positive])
-
-type PositiveInt = t.TypeOf<typeof PositiveInt>
-/*
-same as
-type PositiveInt = number & t.Brand<t.IntBrand> & t.Brand<PositiveBrand>
-*/
-```
-
-# Exact types
-
-You can make a codec exact (which means that additional properties are stripped) using the `exact` combinator
-
-```ts
-const ExactUser = t.exact(User)
-
-User.decode({ userId: 1, name: 'Giulio', age: 45 }) // ok, result is right({ userId: 1, name: 'Giulio', age: 45 })
-ExactUser.decode({ userId: 1, name: 'Giulio', age: 43 }) // ok but result is right({ userId: 1, name: 'Giulio' })
-```
-
-# Mixing required and optional props
-
-You can mix required and optional props using an intersection
-
-```ts
-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>
-
-// same as
-type C = {
-  foo: string
-} & {
-  bar?: number | undefined
-}
-```
-
-You can apply `partial` to an already defined codec via its `props` field
-
-```ts
-const PartialUser = t.partial(User.props)
-
-type PartialUser = t.TypeOf<typeof PartialUser>
-
-// same as
-type PartialUser = {
-  name?: string
-  age?: number
-}
-```
-
-# Custom types
-
-You can define your own types. Let's see an example
-
-```ts
-import { either } from 'fp-ts/lib/Either'
-
-// represents a Date from an ISO string
-const DateFromString = new t.Type<Date, string, unknown>(
-  'DateFromString',
-  (u): u is Date => u instanceof Date,
-  (u, c) =>
-    either.chain(t.string.validate(u, c), 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)
-// right(new Date('1973-11-29T23:00:00.000Z'))
-
-DateFromString.decode('foo')
-// left(errors...)
-```
-
-Note that you can **deserialize** while validating.
-
-# Generic Types
-
-Polymorphic codecs are represented using functions.
-For example, the following typescript:
-
-```ts
-interface ResponseBody<T> {
-  result: T
-  _links: Links
-}
-
-interface Links {
-  previous: string
-  next: string
-}
-```
-
-Would be:
-
-```ts
-// where `t.Mixed = t.Type<any, any, unknown>`
-const responseBody = <C extends t.Mixed>(codec: C) =>
-  t.type({
-    result: codec,
-    _links: Links
-  })
-
-const Links = t.type({
-  previous: t.string,
-  next: t.string
-})
-```
-
-And used like:
-
-```ts
-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
-
-```ts
-const NumberCodec = new t.Type<number, string, string>(
-  'NumberCodec',
-  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(NumberCodec, 'NumberFromString')
-```
-
-# Community
-- `io-ts@2.x`
-  - [io-ts-types](https://github.com/gcanti/io-ts-types) - A collection of codecs and combinators for use with
-    io-ts
-  - [io-ts-reporters](https://github.com/OliverJAsh/io-ts-reporters) - Error reporters for io-ts
-  - [io-ts-promise](https://github.com/aeirola/io-ts-promise) - Convenience library for using io-ts with promise-based APIs
-- `io-ts@1.x`
-  - [geojson-iots](https://github.com/pierremarc/geojson-iots) - codecs for GeoJSON as defined in rfc7946 made with
-    io-ts
-  - [graphql-to-io-ts](https://github.com/micimize/graphql-to-io-ts) - Generate typescript and corresponding io-ts types from a graphql schema
-
-
-# Tips and Tricks
-
-## Union of string literals
-
-Use `keyof` instead of `union` when defining a union of string literals
-
-```ts
-const Bad = t.union([
-  t.literal('foo'),
-  t.literal('bar'),
-  t.literal('baz')
-  // etc...
-])
-
-const Good = t.keyof({
-  foo: null,
-  bar: null,
-  baz: null
-  // etc...
-})
-```
-
-Benefits
-
-- unique check for free
-- better performance, `O(log(n))` vs `O(n)`
-
-Beware that `keyof` is designed to work with objects containing string keys. If you intend to define a numbers enumeration, you have to use an `union` of number literals :
-
-```ts
-const HttpCode = t.union([
-  t.literal(200),
-  t.literal(201),
-  t.literal(202)
-  // etc...
-])
-```
+- [`Type`](Type.md)
diff --git a/Schema.md b/Schema.md
new file mode 100644
index 0000000..b251723
--- /dev/null
+++ b/Schema.md
@@ -0,0 +1,23 @@
+`Schema` allows to define (through the `make` constructor) a generic schema once and then derive multiple concrete instances.
+
+**Example**
+
+```ts
+import * as S from 'io-ts/lib/Schema'
+import * as D from 'io-ts/lib/Decoder'
+import * as E from 'io-ts/lib/Encoder'
+import * as C from 'io-ts/lib/Codec'
+import * as G from 'io-ts/lib/Guard'
+
+export const Person = S.make((S) =>
+  S.type({
+    name: S.string,
+    age: S.number
+  })
+)
+
+export const PersonDecoder = Person(D.decoder)
+export const PersonEncoder = Person(E.encoder)
+export const PersonCodec = Person(C.codec)
+export const PersonGuard = Person(G.guard)
+```
diff --git a/Type.md b/Type.md
new file mode 100644
index 0000000..8640126
--- /dev/null
+++ b/Type.md
@@ -0,0 +1,572 @@
+<!-- START doctoc generated TOC please keep comment here to allow auto update -->
+<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
+
+
+- [The idea](#the-idea)
+- [TypeScript integration](#typescript-integration)
+- [TypeScript compatibility](#typescript-compatibility)
+- [Error reporters](#error-reporters)
+- [Custom error messages](#custom-error-messages)
+- [Implemented types / combinators](#implemented-types--combinators)
+- [Recursive types](#recursive-types)
+  - [Mutually recursive types](#mutually-recursive-types)
+- [Branded types / Refinements](#branded-types--refinements)
+- [Exact types](#exact-types)
+- [Mixing required and optional props](#mixing-required-and-optional-props)
+- [Custom types](#custom-types)
+- [Generic Types](#generic-types)
+- [Piping](#piping)
+- [Community](#community)
+- [Tips and Tricks](#tips-and-tricks)
+  - [Union of string literals](#union-of-string-literals)
+
+<!-- END doctoc generated TOC please keep comment here to allow auto update -->
+
+# The idea
+
+A value of type `Type<A, O, I>` (called "codec") is the runtime representation of the static type `A`.
+
+A codec can:
+
+- decode inputs of type `I` (through `decode`)
+- encode outputs of type `O` (through `encode`)
+- be used as a custom [type guard](https://basarat.gitbooks.io/typescript/content/docs/types/typeGuard.html) (through `is`)
+
+```ts
+class Type<A, O, I> {
+  constructor(
+    /** a unique name for this codec */
+    readonly name: string,
+
+    /** a custom type guard */
+    readonly is: (u: unknown) => u is A,
+
+    /** succeeds if a value of type I can be decoded to a value of type A */
+    readonly validate: (input: I, context: Context) => Either<Errors, A>,
+
+    /** converts a value of type A to a value of type O */
+    readonly encode: (a: A) => O
+  ) {}
+
+  /** a version of `validate` with a default context */
+  decode(i: I): Either<Errors, A>
+}
+```
+
+The [`Either`](https://gcanti.github.io/fp-ts/modules/Either.ts.html) type returned by `decode` is defined in [fp-ts](https://github.com/gcanti/fp-ts), a library containing implementations of common algebraic types in TypeScript.
+
+The `Either` type represents a value of one of two possible types (a disjoint union). An instance of `Either` is either an instance of `Left` or `Right`:
+
+```ts
+type Either<E, A> =
+  | {
+      readonly _tag: 'Left'
+      readonly left: E
+    }
+  | {
+      readonly _tag: 'Right'
+      readonly right: A
+    }
+```
+
+Convention dictates that `Left` is used for **failure** and `Right` is used for **success**.
+
+**Example**
+
+A codec representing `string` can be defined as:
+
+```ts
+import * as t from 'io-ts'
+
+const string = new t.Type<string, string, unknown>(
+  'string',
+  (input: unknown): input is string => typeof input === 'string',
+  // `t.success` and `t.failure` are helpers used to build `Either` instances
+  (input, context) => (typeof input === 'string' ? t.success(input) : t.failure(input, context)),
+  // `A` and `O` are the same, so `encode` is just the identity function
+  t.identity
+)
+```
+
+and we can use it as follows:
+
+```ts
+import { isRight } from 'fp-ts/lib/Either'
+
+isRight(string.decode('a string')) // true
+isRight(string.decode(null)) // false
+```
+
+More generally the result of calling `decode` can be handled using [`fold`](https://gcanti.github.io/fp-ts/modules/Either.ts.html#fold-function) along with `pipe` (which is similar to the pipeline operator)
+
+```ts
+import * as t from 'io-ts'
+import { pipe } from 'fp-ts/lib/pipeable'
+import { fold } from 'fp-ts/lib/Either'
+
+// failure handler
+const onLeft = (errors: t.Errors): string => `${errors.length} error(s) found`
+
+// success handler
+const onRight = (s: string) => `No errors: ${s}`
+
+pipe(t.string.decode('a string'), fold(onLeft, onRight))
+// => "No errors: a string"
+
+pipe(t.string.decode(null), fold(onLeft, onRight))
+// => "1 error(s) found"
+```
+
+We can combine these codecs through [combinators](#implemented-types--combinators) to build composite types which represent entities like domain models, request payloads etc. in our applications:
+
+```ts
+import * as t from 'io-ts'
+
+const User = t.type({
+  userId: t.number,
+  name: t.string
+})
+```
+
+So this is equivalent to defining something like:
+
+```ts
+type User = {
+  userId: number
+  name: string
+}
+```
+
+The advantage of using `io-ts` to define the runtime type is that we can validate the type at runtime, and we can also extract the corresponding static type, so we don’t have to define it twice.
+
+# TypeScript integration
+
+Codecs can be inspected:
+
+![instrospection](images/introspection.png)
+
+This library uses TypeScript extensively. Its API is defined in a way which automatically infers types for produced
+values
+
+![inference](images/inference.png)
+
+Note that the type annotation isn't needed, TypeScript infers the type automatically based on a schema (and comments are preserved).
+
+Static types can be extracted from codecs using the `TypeOf` operator:
+
+```ts
+type User = t.TypeOf<typeof User>
+
+// same as
+type User = {
+  userId: number
+  name: string
+}
+```
+
+# TypeScript compatibility
+
+The stable version is tested against TypeScript 3.5.2
+
+| io-ts version | required TypeScript version |
+| ------------- | --------------------------- |
+| 2.x+          | 3.5.2+                      |
+| 1.6.x+        | 3.2.2+                      |
+| 1.5.3         | 3.0.1+                      |
+| 1.5.2-        | 2.7.2+                      |
+
+**Note**. This library is conceived, tested and is supposed to be consumed by TypeScript with the `strict` flag turned on.
+
+**Note**. If you are running `< typescript@3.0.1` you have to polyfill `unknown`.
+
+You can use [unknown-ts](https://github.com/gcanti/unknown-ts) as a polyfill.
+
+# Error reporters
+
+A reporter implements the following interface
+
+```ts
+interface Reporter<A> {
+  report: (validation: Validation<any>) => A
+}
+```
+
+This package exports a default `PathReporter` reporter
+
+Example
+
+```ts
+import { PathReporter } from 'io-ts/lib/PathReporter'
+
+const result = User.decode({ name: 'Giulio' })
+
+console.log(PathReporter.report(result))
+// => [ 'Invalid value undefined supplied to : { userId: number, name: string }/userId: number' ]
+```
+
+You can define your own reporter. `Errors` has the following type
+
+```ts
+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
+
+```ts
+import { pipe } from 'fp-ts/lib/pipeable'
+import { fold } from 'fp-ts/lib/Either'
+
+const getPaths = <A>(v: t.Validation<A>): Array<string> => {
+  return pipe(
+    v,
+    fold(
+      (errors) => errors.map((error) => error.context.map(({ key }) => key).join('.')),
+      () => ['no errors']
+    )
+  )
+}
+
+console.log(getPaths(User.decode({}))) // => [ '.userId', '.name' ]
+```
+
+# Custom error messages
+
+You can set your own error message by providing a `message` argument to `failure`
+
+Example
+
+```ts
+import { either } from 'fp-ts/lib/Either'
+
+const NumberFromString = new t.Type<number, string, unknown>(
+  'NumberFromString',
+  t.number.is,
+  (u, c) =>
+    either.chain(t.string.validate(u, c), (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')))
+// => ['cannot parse to a number']
+```
+
+You can also use the [`withMessage`](https://gcanti.github.io/io-ts-types/modules/withMessage.ts.html) helper from [io-ts-types](https://github.com/gcanti/io-ts-types)
+
+# Implemented types / combinators
+
+| 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`                                                          |
+| 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 ])`                                                  |
+| intersection                | `A & B`                     | `t.intersection([ A, B ])`                                           |
+| keyof                       | `keyof M`                   | `t.keyof(M)` (**only supports string keys**)                         |
+| 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
+
+```ts
+interface Category {
+  name: string
+  categories: Array<Category>
+}
+
+const Category: t.Type<Category> = t.recursion('Category', () =>
+  t.type({
+    name: t.string,
+    categories: t.array(Category)
+  })
+)
+```
+
+## Mutually recursive types
+
+```ts
+interface Foo {
+  type: 'Foo'
+  b: Bar | undefined
+}
+
+interface Bar {
+  type: 'Bar'
+  a: Foo | undefined
+}
+
+const Foo: t.Type<Foo> = t.recursion('Foo', () =>
+  t.type({
+    type: t.literal('Foo'),
+    b: t.union([Bar, t.undefined])
+  })
+)
+
+const Bar: t.Type<Bar> = t.recursion('Bar', () =>
+  t.type({
+    type: t.literal('Bar'),
+    a: t.union([Foo, t.undefined])
+  })
+)
+```
+
+# Branded types / Refinements
+
+You can brand / refine a codec (_any_ codec) using the `brand` combinator
+
+```ts
+// a unique brand for positive numbers
+interface PositiveBrand {
+  readonly Positive: unique symbol // use `unique symbol` here to ensure uniqueness across modules / packages
+}
+
+const Positive = t.brand(
+  t.number, // a codec representing the type to be refined
+  (n): n is t.Branded<number, PositiveBrand> => n >= 0, // a custom type guard using the build-in helper `Branded`
+  'Positive' // the name must match the readonly field in the brand
+)
+
+type Positive = t.TypeOf<typeof Positive>
+/*
+same as
+type Positive = number & t.Brand<PositiveBrand>
+*/
+```
+
+Branded codecs can be merged with `t.intersection`
+
+```ts
+// t.Int is a built-in branded codec
+const PositiveInt = t.intersection([t.Int, Positive])
+
+type PositiveInt = t.TypeOf<typeof PositiveInt>
+/*
+same as
+type PositiveInt = number & t.Brand<t.IntBrand> & t.Brand<PositiveBrand>
+*/
+```
+
+# Exact types
+
+You can make a codec exact (which means that additional properties are stripped) using the `exact` combinator
+
+```ts
+const ExactUser = t.exact(User)
+
+User.decode({ userId: 1, name: 'Giulio', age: 45 }) // ok, result is right({ userId: 1, name: 'Giulio', age: 45 })
+ExactUser.decode({ userId: 1, name: 'Giulio', age: 43 }) // ok but result is right({ userId: 1, name: 'Giulio' })
+```
+
+# Mixing required and optional props
+
+You can mix required and optional props using an intersection
+
+```ts
+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>
+
+// same as
+type C = {
+  foo: string
+} & {
+  bar?: number | undefined
+}
+```
+
+You can apply `partial` to an already defined codec via its `props` field
+
+```ts
+const PartialUser = t.partial(User.props)
+
+type PartialUser = t.TypeOf<typeof PartialUser>
+
+// same as
+type PartialUser = {
+  name?: string
+  age?: number
+}
+```
+
+# Custom types
+
+You can define your own types. Let's see an example
+
+```ts
+import { either } from 'fp-ts/lib/Either'
+
+// represents a Date from an ISO string
+const DateFromString = new t.Type<Date, string, unknown>(
+  'DateFromString',
+  (u): u is Date => u instanceof Date,
+  (u, c) =>
+    either.chain(t.string.validate(u, c), (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)
+// right(new Date('1973-11-29T23:00:00.000Z'))
+
+DateFromString.decode('foo')
+// left(errors...)
+```
+
+Note that you can **deserialize** while validating.
+
+# Generic Types
+
+Polymorphic codecs are represented using functions.
+For example, the following typescript:
+
+```ts
+interface ResponseBody<T> {
+  result: T
+  _links: Links
+}
+
+interface Links {
+  previous: string
+  next: string
+}
+```
+
+Would be:
+
+```ts
+// where `t.Mixed = t.Type<any, any, unknown>`
+const responseBody = <C extends t.Mixed>(codec: C) =>
+  t.type({
+    result: codec,
+    _links: Links
+  })
+
+const Links = t.type({
+  previous: t.string,
+  next: t.string
+})
+```
+
+And used like:
+
+```ts
+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
+
+```ts
+const NumberCodec = new t.Type<number, string, string>(
+  'NumberCodec',
+  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(NumberCodec, 'NumberFromString')
+```
+
+# Community
+
+- `io-ts@2.x`
+  - [io-ts-types](https://github.com/gcanti/io-ts-types) - A collection of codecs and combinators for use with
+    io-ts
+  - [io-ts-reporters](https://github.com/OliverJAsh/io-ts-reporters) - Error reporters for io-ts
+  - [io-ts-promise](https://github.com/aeirola/io-ts-promise) - Convenience library for using io-ts with promise-based APIs
+- `io-ts@1.x`
+  - [geojson-iots](https://github.com/pierremarc/geojson-iots) - codecs for GeoJSON as defined in rfc7946 made with
+    io-ts
+  - [graphql-to-io-ts](https://github.com/micimize/graphql-to-io-ts) - Generate typescript and corresponding io-ts types from a graphql schema
+
+# Tips and Tricks
+
+## Union of string literals
+
+Use `keyof` instead of `union` when defining a union of string literals
+
+```ts
+const Bad = t.union([
+  t.literal('foo'),
+  t.literal('bar'),
+  t.literal('baz')
+  // etc...
+])
+
+const Good = t.keyof({
+  foo: null,
+  bar: null,
+  baz: null
+  // etc...
+})
+```
+
+Benefits
+
+- unique check for free
+- better performance, `O(log(n))` vs `O(n)`
+
+Beware that `keyof` is designed to work with objects containing string keys. If you intend to define a numbers enumeration, you have to use an `union` of number literals :
+
+```ts
+const HttpCode = t.union([
+  t.literal(200),
+  t.literal(201),
+  t.literal(202)
+  // etc...
+])
+```
diff --git a/docs/modules/Codec.ts.md b/docs/modules/Codec.ts.md
new file mode 100644
index 0000000..64571bf
--- /dev/null
+++ b/docs/modules/Codec.ts.md
@@ -0,0 +1,274 @@
+---
+title: Codec.ts
+nav_order: 1
+parent: Modules
+---
+
+# Codec overview
+
+Added in v2.2.0
+
+---
+
+<h2 class="text-delta">Table of contents</h2>
+
+- [Codec (interface)](#codec-interface)
+- [URI (type alias)](#uri-type-alias)
+- [URI](#uri)
+- [UnknownArray](#unknownarray)
+- [UnknownRecord](#unknownrecord)
+- [array](#array)
+- [boolean](#boolean)
+- [codec](#codec)
+- [intersection](#intersection)
+- [lazy](#lazy)
+- [literal](#literal)
+- [make](#make)
+- [nullable](#nullable)
+- [number](#number)
+- [partial](#partial)
+- [record](#record)
+- [refinement](#refinement)
+- [string](#string)
+- [sum](#sum)
+- [tuple](#tuple)
+- [type](#type)
+- [withExpected](#withexpected)
+
+---
+
+# Codec (interface)
+
+Laws:
+
+1. `pipe(codec.decode(u), E.fold(() => u, codec.encode) = u` for all `u` in `unknown`
+2. `codec.decode(codec.encode(a)) = E.right(a)` for all `a` in `A`
+
+**Signature**
+
+```ts
+export interface Codec<A> extends D.Decoder<A>, E.Encoder<A> {}
+```
+
+Added in v2.2.0
+
+# URI (type alias)
+
+**Signature**
+
+```ts
+export type URI = typeof URI
+```
+
+Added in v2.2.0
+
+# URI
+
+**Signature**
+
+```ts
+export declare const URI: 'Codec'
+```
+
+Added in v2.2.0
+
+# UnknownArray
+
+**Signature**
+
+```ts
+export declare const UnknownArray: Codec<unknown[]>
+```
+
+Added in v2.2.0
+
+# UnknownRecord
+
+**Signature**
+
+```ts
+export declare const UnknownRecord: Codec<Record<string, unknown>>
+```
+
+Added in v2.2.0
+
+# array
+
+**Signature**
+
+```ts
+export declare function array<A>(items: Codec<A>): Codec<Array<A>>
+```
+
+Added in v2.2.0
+
+# boolean
+
+**Signature**
+
+```ts
+export declare const boolean: Codec<boolean>
+```
+
+Added in v2.2.0
+
+# codec
+
+**Signature**
+
+```ts
+export declare const codec: Invariant1<'Codec'> & Schemable<'Codec'>
+```
+
+Added in v2.2.0
+
+# intersection
+
+**Signature**
+
+```ts
+export declare function intersection<A, B>(left: Codec<A>, right: Codec<B>): Codec<A & B>
+```
+
+Added in v2.2.0
+
+# lazy
+
+**Signature**
+
+```ts
+export declare function lazy<A>(id: string, f: () => Codec<A>): Codec<A>
+```
+
+Added in v2.2.0
+
+# literal
+
+**Signature**
+
+```ts
+export declare function literal<A extends ReadonlyArray<Literal>>(...values: A): Codec<A[number]>
+```
+
+Added in v2.2.0
+
+# make
+
+**Signature**
+
+```ts
+export declare function make<A>(decoder: D.Decoder<A>, encoder: E.Encoder<A>): Codec<A>
+```
+
+Added in v2.2.0
+
+# nullable
+
+**Signature**
+
+```ts
+export declare function nullable<A>(or: Codec<A>): Codec<null | A>
+```
+
+Added in v2.2.0
+
+# number
+
+**Signature**
+
+```ts
+export declare const number: Codec<number>
+```
+
+Added in v2.2.0
+
+# partial
+
+**Signature**
+
+```ts
+export declare function partial<A>(properties: { [K in keyof A]: Codec<A[K]> }): Codec<Partial<A>>
+```
+
+Added in v2.2.0
+
+# record
+
+**Signature**
+
+```ts
+export declare function record<A>(codomain: Codec<A>): Codec<Record<string, A>>
+```
+
+Added in v2.2.0
+
+# refinement
+
+**Signature**
+
+```ts
+export declare function refinement<A, B extends A>(
+  from: Codec<A>,
+  refinement: (a: A) => a is B,
+  expected: string
+): Codec<B>
+```
+
+Added in v2.2.0
+
+# string
+
+**Signature**
+
+```ts
+export declare const string: Codec<string>
+```
+
+Added in v2.2.0
+
+# sum
+
+**Signature**
+
+```ts
+export declare function sum<T extends string>(
+  tag: T
+): <A>(members: { [K in keyof A]: Codec<A[K] & Record<T, K>> }) => Codec<A[keyof A]>
+```
+
+Added in v2.2.0
+
+# tuple
+
+**Signature**
+
+```ts
+export declare function tuple<A extends ReadonlyArray<unknown>>(
+  ...components: { [K in keyof A]: Codec<A[K]> }
+): Codec<A>
+```
+
+Added in v2.2.0
+
+# type
+
+**Signature**
+
+```ts
+export declare function type<A>(properties: { [K in keyof A]: Codec<A[K]> }): Codec<A>
+```
+
+Added in v2.2.0
+
+# withExpected
+
+**Signature**
+
+```ts
+export declare function withExpected<A>(
+  codec: Codec<A>,
+  expected: (actual: unknown, nea: NonEmptyArray<Tree<string>>) => NonEmptyArray<Tree<string>>
+): Codec<A>
+```
+
+Added in v2.2.0
diff --git a/docs/modules/Decoder.ts.md b/docs/modules/Decoder.ts.md
new file mode 100644
index 0000000..1dcacbc
--- /dev/null
+++ b/docs/modules/Decoder.ts.md
@@ -0,0 +1,422 @@
+---
+title: Decoder.ts
+nav_order: 2
+parent: Modules
+---
+
+# Decoder overview
+
+Added in v2.2.0
+
+---
+
+<h2 class="text-delta">Table of contents</h2>
+
+- [Decoder (interface)](#decoder-interface)
+- [TypeOf (type alias)](#typeof-type-alias)
+- [URI (type alias)](#uri-type-alias)
+- [URI](#uri)
+- [UnknownArray](#unknownarray)
+- [UnknownRecord](#unknownrecord)
+- [alt](#alt)
+- [ap](#ap)
+- [apFirst](#apfirst)
+- [apSecond](#apsecond)
+- [array](#array)
+- [boolean](#boolean)
+- [decoder](#decoder)
+- [failure](#failure)
+- [failures](#failures)
+- [fromGuard](#fromguard)
+- [intersection](#intersection)
+- [lazy](#lazy)
+- [literal](#literal)
+- [map](#map)
+- [never](#never)
+- [nullable](#nullable)
+- [number](#number)
+- [parse](#parse)
+- [partial](#partial)
+- [record](#record)
+- [refinement](#refinement)
+- [string](#string)
+- [success](#success)
+- [sum](#sum)
+- [tree](#tree)
+- [tuple](#tuple)
+- [type](#type)
+- [union](#union)
+- [withExpected](#withexpected)
+
+---
+
+# Decoder (interface)
+
+**Signature**
+
+```ts
+export interface Decoder<A> {
+  readonly decode: (u: unknown) => Either<NonEmptyArray<Tree<string>>, A>
+}
+```
+
+Added in v2.2.0
+
+# TypeOf (type alias)
+
+**Signature**
+
+```ts
+export type TypeOf<D> = D extends Decoder<infer A> ? A : never
+```
+
+Added in v2.2.0
+
+# URI (type alias)
+
+**Signature**
+
+```ts
+export type URI = typeof URI
+```
+
+Added in v2.2.0
+
+# URI
+
+**Signature**
+
+```ts
+export declare const URI: 'Decoder'
+```
+
+Added in v2.2.0
+
+# UnknownArray
+
+**Signature**
+
+```ts
+export declare const UnknownArray: Decoder<unknown[]>
+```
+
+Added in v2.2.0
+
+# UnknownRecord
+
+**Signature**
+
+```ts
+export declare const UnknownRecord: Decoder<Record<string, unknown>>
+```
+
+Added in v2.2.0
+
+# alt
+
+**Signature**
+
+```ts
+export declare const alt: <A>(that: () => Decoder<A>) => (fa: Decoder<A>) => Decoder<A>
+```
+
+Added in v2.2.0
+
+# ap
+
+**Signature**
+
+```ts
+export declare const ap: <A>(fa: Decoder<A>) => <B>(fab: Decoder<(a: A) => B>) => Decoder<B>
+```
+
+Added in v2.2.0
+
+# apFirst
+
+**Signature**
+
+```ts
+export declare const apFirst: <B>(fb: Decoder<B>) => <A>(fa: Decoder<A>) => Decoder<A>
+```
+
+Added in v2.2.0
+
+# apSecond
+
+**Signature**
+
+```ts
+export declare const apSecond: <B>(fb: Decoder<B>) => <A>(fa: Decoder<A>) => Decoder<B>
+```
+
+Added in v2.2.0
+
+# array
+
+**Signature**
+
+```ts
+export declare function array<A>(items: Decoder<A>): Decoder<Array<A>>
+```
+
+Added in v2.2.0
+
+# boolean
+
+**Signature**
+
+```ts
+export declare const boolean: Decoder<boolean>
+```
+
+Added in v2.2.0
+
+# decoder
+
+**Signature**
+
+```ts
+export declare const decoder: Applicative1<'Decoder'> &
+  Alternative1<'Decoder'> &
+  Schemable<'Decoder'> &
+  WithUnion<'Decoder'>
+```
+
+Added in v2.2.0
+
+# failure
+
+**Signature**
+
+```ts
+export declare function failure<A = never>(message: string): Either<NonEmptyArray<Tree<string>>, A>
+```
+
+Added in v2.2.0
+
+# failures
+
+**Signature**
+
+```ts
+export declare function failures<A = never>(
+  message: string,
+  errors: NonEmptyArray<Tree<string>>
+): Either<NonEmptyArray<Tree<string>>, A>
+```
+
+Added in v2.2.0
+
+# fromGuard
+
+**Signature**
+
+```ts
+export declare function fromGuard<A>(guard: G.Guard<A>, expected: string): Decoder<A>
+```
+
+Added in v2.2.0
+
+# intersection
+
+**Signature**
+
+```ts
+export declare function intersection<A, B>(left: Decoder<A>, right: Decoder<B>): Decoder<A & B>
+```
+
+Added in v2.2.0
+
+# lazy
+
+**Signature**
+
+```ts
+export declare function lazy<A>(id: string, f: () => Decoder<A>): Decoder<A>
+```
+
+Added in v2.2.0
+
+# literal
+
+**Signature**
+
+```ts
+export declare function literal<A extends ReadonlyArray<Literal>>(...values: A): Decoder<A[number]>
+```
+
+Added in v2.2.0
+
+# map
+
+**Signature**
+
+```ts
+export declare const map: <A, B>(f: (a: A) => B) => (fa: Decoder<A>) => Decoder<B>
+```
+
+Added in v2.2.0
+
+# never
+
+**Signature**
+
+```ts
+export declare const never: Decoder<never>
+```
+
+Added in v2.2.0
+
+# nullable
+
+**Signature**
+
+```ts
+export declare function nullable<A>(or: Decoder<A>): Decoder<null | A>
+```
+
+Added in v2.2.0
+
+# number
+
+**Signature**
+
+```ts
+export declare const number: Decoder<number>
+```
+
+Added in v2.2.0
+
+# parse
+
+**Signature**
+
+```ts
+export declare function parse<A, B>(from: Decoder<A>, parser: (a: A) => Either<string, B>): Decoder<B>
+```
+
+Added in v2.2.0
+
+# partial
+
+**Signature**
+
+```ts
+export declare function partial<A>(properties: { [K in keyof A]: Decoder<A[K]> }): Decoder<Partial<A>>
+```
+
+Added in v2.2.0
+
+# record
+
+**Signature**
+
+```ts
+export declare function record<A>(codomain: Decoder<A>): Decoder<Record<string, A>>
+```
+
+Added in v2.2.0
+
+# refinement
+
+**Signature**
+
+```ts
+export declare function refinement<A, B extends A>(
+  from: Decoder<A>,
+  refinement: (a: A) => a is B,
+  expected: string
+): Decoder<B>
+```
+
+Added in v2.2.0
+
+# string
+
+**Signature**
+
+```ts
+export declare const string: Decoder<string>
+```
+
+Added in v2.2.0
+
+# success
+
+**Signature**
+
+```ts
+export declare function success<A>(a: A): Either<NonEmptyArray<Tree<string>>, A>
+```
+
+Added in v2.2.0
+
+# sum
+
+**Signature**
+
+```ts
+export declare function sum<T extends string>(
+  tag: T
+): <A>(members: { [K in keyof A]: Decoder<A[K] & Record<T, K>> }) => Decoder<A[keyof A]>
+```
+
+Added in v2.2.0
+
+# tree
+
+**Signature**
+
+```ts
+export declare function tree<A>(value: A, forest: Forest<A> = empty): Tree<A>
+```
+
+Added in v2.2.0
+
+# tuple
+
+**Signature**
+
+```ts
+export declare function tuple<A extends ReadonlyArray<unknown>>(
+  ...components: { [K in keyof A]: Decoder<A[K]> }
+): Decoder<A>
+```
+
+Added in v2.2.0
+
+# type
+
+**Signature**
+
+```ts
+export declare function type<A>(properties: { [K in keyof A]: Decoder<A[K]> }): Decoder<A>
+```
+
+Added in v2.2.0
+
+# union
+
+**Signature**
+
+```ts
+export declare function union<A extends ReadonlyArray<unknown>>(
+  ...members: { [K in keyof A]: Decoder<A[K]> }
+): Decoder<A[number]>
+```
+
+Added in v2.2.0
+
+# withExpected
+
+**Signature**
+
+```ts
+export declare function withExpected<A>(
+  decoder: Decoder<A>,
+  expected: (actual: unknown, nea: NonEmptyArray<Tree<string>>) => NonEmptyArray<Tree<string>>
+): Decoder<A>
+```
+
+Added in v2.2.0
diff --git a/docs/modules/Encoder.ts.md b/docs/modules/Encoder.ts.md
new file mode 100644
index 0000000..7450dd6
--- /dev/null
+++ b/docs/modules/Encoder.ts.md
@@ -0,0 +1,187 @@
+---
+title: Encoder.ts
+nav_order: 3
+parent: Modules
+---
+
+# Encoder overview
+
+Added in v2.2.0
+
+---
+
+<h2 class="text-delta">Table of contents</h2>
+
+- [Encoder (interface)](#encoder-interface)
+- [URI (type alias)](#uri-type-alias)
+- [URI](#uri)
+- [array](#array)
+- [contramap](#contramap)
+- [encoder](#encoder)
+- [id](#id)
+- [intersection](#intersection)
+- [lazy](#lazy)
+- [nullable](#nullable)
+- [partial](#partial)
+- [record](#record)
+- [sum](#sum)
+- [tuple](#tuple)
+- [type](#type)
+
+---
+
+# Encoder (interface)
+
+**Signature**
+
+```ts
+export interface Encoder<A> {
+  readonly encode: (a: A) => unknown
+}
+```
+
+Added in v2.2.0
+
+# URI (type alias)
+
+**Signature**
+
+```ts
+export type URI = typeof URI
+```
+
+Added in v2.2.0
+
+# URI
+
+**Signature**
+
+```ts
+export declare const URI: 'Encoder'
+```
+
+Added in v2.2.0
+
+# array
+
+**Signature**
+
+```ts
+export declare function array<A>(items: Encoder<A>): Encoder<Array<A>>
+```
+
+Added in v2.2.0
+
+# contramap
+
+**Signature**
+
+```ts
+export declare const contramap: <A, B>(f: (b: B) => A) => (fa: Encoder<A>) => Encoder<B>
+```
+
+Added in v2.2.0
+
+# encoder
+
+**Signature**
+
+```ts
+export declare const encoder: Contravariant1<'Encoder'> & Schemable<'Encoder'>
+```
+
+Added in v2.2.0
+
+# id
+
+**Signature**
+
+```ts
+export declare const id: Encoder<unknown>
+```
+
+Added in v2.2.0
+
+# intersection
+
+**Signature**
+
+```ts
+export declare function intersection<A, B>(left: Encoder<A>, right: Encoder<B>): Encoder<A & B>
+```
+
+Added in v2.2.0
+
+# lazy
+
+**Signature**
+
+```ts
+export declare function lazy<A>(f: () => Encoder<A>): Encoder<A>
+```
+
+Added in v2.2.0
+
+# nullable
+
+**Signature**
+
+```ts
+export declare function nullable<A>(or: Encoder<A>): Encoder<null | A>
+```
+
+Added in v2.2.0
+
+# partial
+
+**Signature**
+
+```ts
+export declare function partial<A>(properties: { [K in keyof A]: Encoder<A[K]> }): Encoder<Partial<A>>
+```
+
+Added in v2.2.0
+
+# record
+
+**Signature**
+
+```ts
+export declare function record<A>(codomain: Encoder<A>): Encoder<Record<string, A>>
+```
+
+Added in v2.2.0
+
+# sum
+
+**Signature**
+
+```ts
+export declare function sum<T extends string>(
+  tag: T
+): <A>(members: { [K in keyof A]: Encoder<A[K] & Record<T, K>> }) => Encoder<A[keyof A]>
+```
+
+Added in v2.2.0
+
+# tuple
+
+**Signature**
+
+```ts
+export declare function tuple<A extends ReadonlyArray<unknown>>(
+  ...components: { [K in keyof A]: Encoder<A[K]> }
+): Encoder<A>
+```
+
+Added in v2.2.0
+
+# type
+
+**Signature**
+
+```ts
+export declare function type<A>(properties: { [K in keyof A]: Encoder<A[K]> }): Encoder<A>
+```
+
+Added in v2.2.0
diff --git a/docs/modules/Guard.ts.md b/docs/modules/Guard.ts.md
new file mode 100644
index 0000000..4bd61f1
--- /dev/null
+++ b/docs/modules/Guard.ts.md
@@ -0,0 +1,266 @@
+---
+title: Guard.ts
+nav_order: 4
+parent: Modules
+---
+
+# Guard overview
+
+Added in v2.2.0
+
+---
+
+<h2 class="text-delta">Table of contents</h2>
+
+- [Guard (interface)](#guard-interface)
+- [URI (type alias)](#uri-type-alias)
+- [URI](#uri)
+- [UnknownArray](#unknownarray)
+- [UnknownRecord](#unknownrecord)
+- [array](#array)
+- [boolean](#boolean)
+- [guard](#guard)
+- [intersection](#intersection)
+- [lazy](#lazy)
+- [literal](#literal)
+- [never](#never)
+- [nullable](#nullable)
+- [number](#number)
+- [partial](#partial)
+- [record](#record)
+- [refinement](#refinement)
+- [string](#string)
+- [sum](#sum)
+- [tuple](#tuple)
+- [type](#type)
+- [union](#union)
+
+---
+
+# Guard (interface)
+
+**Signature**
+
+```ts
+export interface Guard<A> {
+  is: (u: unknown) => u is A
+}
+```
+
+Added in v2.2.0
+
+# URI (type alias)
+
+**Signature**
+
+```ts
+export type URI = typeof URI
+```
+
+Added in v2.2.0
+
+# URI
+
+**Signature**
+
+```ts
+export declare const URI: 'Guard'
+```
+
+Added in v2.2.0
+
+# UnknownArray
+
+**Signature**
+
+```ts
+export declare const UnknownArray: Guard<unknown[]>
+```
+
+Added in v2.2.0
+
+# UnknownRecord
+
+**Signature**
+
+```ts
+export declare const UnknownRecord: Guard<Record<string, unknown>>
+```
+
+Added in v2.2.0
+
+# array
+
+**Signature**
+
+```ts
+export declare function array<A>(items: Guard<A>): Guard<Array<A>>
+```
+
+Added in v2.2.0
+
+# boolean
+
+**Signature**
+
+```ts
+export declare const boolean: Guard<boolean>
+```
+
+Added in v2.2.0
+
+# guard
+
+**Signature**
+
+```ts
+export declare const guard: Schemable<'Guard'> & WithUnion<'Guard'>
+```
+
+Added in v2.2.0
+
+# intersection
+
+**Signature**
+
+```ts
+export declare function intersection<A, B>(left: Guard<A>, right: Guard<B>): Guard<A & B>
+```
+
+Added in v2.2.0
+
+# lazy
+
+**Signature**
+
+```ts
+export declare function lazy<A>(f: () => Guard<A>): Guard<A>
+```
+
+Added in v2.2.0
+
+# literal
+
+**Signature**
+
+```ts
+export declare function literal<A extends ReadonlyArray<Literal>>(...values: A): Guard<A[number]>
+```
+
+Added in v2.2.0
+
+# never
+
+**Signature**
+
+```ts
+export declare const never: Guard<never>
+```
+
+Added in v2.2.0
+
+# nullable
+
+**Signature**
+
+```ts
+export declare function nullable<A>(or: Guard<A>): Guard<null | A>
+```
+
+Added in v2.2.0
+
+# number
+
+**Signature**
+
+```ts
+export declare const number: Guard<number>
+```
+
+Added in v2.2.0
+
+# partial
+
+**Signature**
+
+```ts
+export declare function partial<A>(properties: { [K in keyof A]: Guard<A[K]> }): Guard<Partial<A>>
+```
+
+Added in v2.2.0
+
+# record
+
+**Signature**
+
+```ts
+export declare function record<A>(codomain: Guard<A>): Guard<Record<string, A>>
+```
+
+Added in v2.2.0
+
+# refinement
+
+**Signature**
+
+```ts
+export declare function refinement<A, B extends A>(from: Guard<A>, refinement: (a: A) => a is B): Guard<B>
+```
+
+Added in v2.2.0
+
+# string
+
+**Signature**
+
+```ts
+export declare const string: Guard<string>
+```
+
+Added in v2.2.0
+
+# sum
+
+**Signature**
+
+```ts
+export declare function sum<T extends string>(
+  tag: T
+): <A>(members: { [K in keyof A]: Guard<A[K] & Record<T, K>> }) => Guard<A[keyof A]>
+```
+
+Added in v2.2.0
+
+# tuple
+
+**Signature**
+
+```ts
+export declare function tuple<A extends ReadonlyArray<unknown>>(
+  ...components: { [K in keyof A]: Guard<A[K]> }
+): Guard<A>
+```
+
+Added in v2.2.0
+
+# type
+
+**Signature**
+
+```ts
+export declare function type<A>(properties: { [K in keyof A]: Guard<A[K]> }): Guard<A>
+```
+
+Added in v2.2.0
+
+# union
+
+**Signature**
+
+```ts
+export declare function union<A extends ReadonlyArray<unknown>>(
+  ...members: { [K in keyof A]: Guard<A[K]> }
+): Guard<A[number]>
+```
+
+Added in v2.2.0
diff --git a/docs/modules/PathReporter.ts.md b/docs/modules/PathReporter.ts.md
index f7ac739..4116f44 100644
--- a/docs/modules/PathReporter.ts.md
+++ b/docs/modules/PathReporter.ts.md
@@ -1,6 +1,6 @@
 ---
 title: PathReporter.ts
-nav_order: 2
+nav_order: 6
 parent: Modules
 ---
 
diff --git a/docs/modules/Reporter.ts.md b/docs/modules/Reporter.ts.md
index 152c946..a76c048 100644
--- a/docs/modules/Reporter.ts.md
+++ b/docs/modules/Reporter.ts.md
@@ -1,6 +1,6 @@
 ---
 title: Reporter.ts
-nav_order: 3
+nav_order: 7
 parent: Modules
 ---
 
diff --git a/docs/modules/Schema.ts.md b/docs/modules/Schema.ts.md
new file mode 100644
index 0000000..6500e9c
--- /dev/null
+++ b/docs/modules/Schema.ts.md
@@ -0,0 +1,51 @@
+---
+title: Schema.ts
+nav_order: 8
+parent: Modules
+---
+
+# Schema overview
+
+Added in v2.2.0
+
+---
+
+<h2 class="text-delta">Table of contents</h2>
+
+- [Schema (interface)](#schema-interface)
+- [TypeOf (type alias)](#typeof-type-alias)
+- [make](#make)
+
+---
+
+# Schema (interface)
+
+**Signature**
+
+```ts
+export interface Schema<A> {
+  <S extends URIS>(S: Schemable<S>): Kind<S, A>
+}
+```
+
+Added in v2.2.0
+
+# TypeOf (type alias)
+
+**Signature**
+
+```ts
+export type TypeOf<S> = S extends Schema<infer A> ? A : never
+```
+
+Added in v2.2.0
+
+# make
+
+**Signature**
+
+```ts
+export declare function make<A>(f: Schema<A>): Schema<A>
+```
+
+Added in v2.2.0
diff --git a/docs/modules/Schemable.ts.md b/docs/modules/Schemable.ts.md
new file mode 100644
index 0000000..2c38c3d
--- /dev/null
+++ b/docs/modules/Schemable.ts.md
@@ -0,0 +1,83 @@
+---
+title: Schemable.ts
+nav_order: 9
+parent: Modules
+---
+
+# Schemable overview
+
+Added in v2.2.0
+
+---
+
+<h2 class="text-delta">Table of contents</h2>
+
+- [Schemable (interface)](#schemable-interface)
+- [WithUnion (interface)](#withunion-interface)
+- [Literal (type alias)](#literal-type-alias)
+- [memoize](#memoize)
+
+---
+
+# Schemable (interface)
+
+**Signature**
+
+```ts
+export interface Schemable<S extends URIS> {
+  readonly URI: S
+  readonly literal: <A extends ReadonlyArray<Literal>>(...values: A) => Kind<S, A[number]>
+  readonly string: Kind<S, string>
+  readonly number: Kind<S, number>
+  readonly boolean: Kind<S, boolean>
+  readonly UnknownArray: Kind<S, Array<unknown>>
+  readonly UnknownRecord: Kind<S, Record<string, unknown>>
+  readonly nullable: <A>(or: Kind<S, A>) => Kind<S, null | A>
+  readonly type: <A>(properties: { [K in keyof A]: Kind<S, A[K]> }) => Kind<S, A>
+  readonly partial: <A>(properties: { [K in keyof A]: Kind<S, A[K]> }) => Kind<S, Partial<A>>
+  readonly record: <A>(codomain: Kind<S, A>) => Kind<S, Record<string, A>>
+  readonly array: <A>(items: Kind<S, A>) => Kind<S, Array<A>>
+  readonly tuple: <A extends ReadonlyArray<unknown>>(...components: { [K in keyof A]: Kind<S, A[K]> }) => Kind<S, A>
+  readonly intersection: <A, B>(left: Kind<S, A>, right: Kind<S, B>) => Kind<S, A & B>
+  readonly sum: <T extends string>(
+    tag: T
+  ) => <A>(members: { [K in keyof A]: Kind<S, A[K] & Record<T, K>> }) => Kind<S, A[keyof A]>
+  readonly lazy: <A>(id: string, f: () => Kind<S, A>) => Kind<S, A>
+}
+```
+
+Added in v2.2.0
+
+# WithUnion (interface)
+
+**Signature**
+
+```ts
+export interface WithUnion<S extends URIS> {
+  readonly union: <A extends ReadonlyArray<unknown>>(
+    ...members: { [K in keyof A]: Kind<S, A[K]> }
+  ) => Kind<S, A[number]>
+}
+```
+
+Added in v2.2.0
+
+# Literal (type alias)
+
+**Signature**
+
+```ts
+export type Literal = string | number | boolean | null
+```
+
+Added in v2.2.0
+
+# memoize
+
+**Signature**
+
+```ts
+export declare function memoize<A, B>(f: (a: A) => B): (a: A) => B
+```
+
+Added in v2.2.0
diff --git a/docs/modules/Tree.ts.md b/docs/modules/Tree.ts.md
new file mode 100644
index 0000000..488a86b
--- /dev/null
+++ b/docs/modules/Tree.ts.md
@@ -0,0 +1,27 @@
+---
+title: Tree.ts
+nav_order: 10
+parent: Modules
+---
+
+# Tree overview
+
+Added in v2.2.0
+
+---
+
+<h2 class="text-delta">Table of contents</h2>
+
+- [draw](#draw)
+
+---
+
+# draw
+
+**Signature**
+
+```ts
+export declare function draw(es: NonEmptyArray<Tree<string>>): string
+```
+
+Added in v2.2.0
diff --git a/docs/modules/index.ts.md b/docs/modules/index.ts.md
index d1dbde4..3bcd78e 100644
--- a/docs/modules/index.ts.md
+++ b/docs/modules/index.ts.md
@@ -1,6 +1,6 @@
 ---
 title: index.ts
-nav_order: 1
+nav_order: 5
 parent: Modules
 ---
 
diff --git a/dtslint/ts3.5/Schemable.ts b/dtslint/ts3.5/Schemable.ts
new file mode 100644
index 0000000..d7578e4
--- /dev/null
+++ b/dtslint/ts3.5/Schemable.ts
@@ -0,0 +1,113 @@
+import * as D from '../../src/Decoder'
+import { make, Schema } from '../../src/Schema'
+
+//
+// literal
+//
+make((S) => S.literal('a')) // $ExpectType Schema<"a">
+
+//
+// string
+//
+make((S) => S.string) // $ExpectType Schema<string>
+
+//
+// number
+//
+make((S) => S.number) // $ExpectType Schema<number>
+
+//
+// boolean
+//
+make((S) => S.boolean) // $ExpectType Schema<boolean>
+
+//
+// UnknownArray
+//
+make((S) => S.UnknownArray) // $ExpectType Schema<unknown[]>
+
+//
+// UnknownRecord
+//
+make((S) => S.UnknownRecord) // $ExpectType Schema<Record<string, unknown>>
+
+//
+// nullable
+//
+make((S) => S.nullable(S.string)) // $ExpectType Schema<string | null>
+
+//
+// type
+//
+make((S) => S.type({ a: S.string })) // $ExpectType Schema<{ a: string; }>
+
+//
+// partial
+//
+make((S) => S.partial({ a: S.string })) // $ExpectType Schema<Partial<{ a: string; }>>
+
+//
+// record
+//
+make((S) => S.record(S.number)) // $ExpectType Schema<Record<string, number>>
+
+//
+// array
+//
+make((S) => S.array(S.number)) // $ExpectType Schema<number[]>
+
+//
+// tuple
+//
+make((S) => S.tuple()) // $ExpectType Schema<[]>
+make((S) => S.tuple(S.string)) // $ExpectType Schema<[string]>
+make((S) => S.tuple(S.string, S.number)) // $ExpectType Schema<[string, number]>
+make((S) => S.tuple(S.string, S.number, S.boolean)) // $ExpectType Schema<[string, number, boolean]>
+
+//
+// intersection
+//
+make((S) => S.intersection(S.type({ a: S.string }), S.type({ b: S.number }))) // $ExpectType Schema<{ a: string; } & { b: number; }>
+
+//
+// sum
+//
+const S1 = make((S) => S.type({ _tag: S.literal('A'), a: S.string }))
+const S2 = make((S) => S.type({ _tag: S.literal('B'), b: S.number }))
+
+// $ExpectType Schema<{ _tag: "A"; a: string; } | { _tag: "B"; b: number; }>
+make((S) => S.sum('_tag')({ A: S1(S), B: S2(S) }))
+
+//
+// lazy
+//
+interface A {
+  a: string
+  bs: Array<B>
+}
+interface B {
+  b: number
+  as: Array<A>
+}
+const A: Schema<A> = make((S) =>
+  S.lazy('A', () =>
+    S.type({
+      a: S.string,
+      bs: S.array(B(S))
+    })
+  )
+)
+const B: Schema<B> = make((S) =>
+  S.lazy('B', () =>
+    S.type({
+      b: S.number,
+      as: S.array(A(S))
+    })
+  )
+)
+
+//
+// union
+//
+D.union(D.string) // $ExpectType Decoder<string>
+D.union(D.string, D.number) // $ExpectType Decoder<string | number>
diff --git a/package.json b/package.json
index 36c5d24..cd6aea1 100644
--- a/package.json
+++ b/package.json
@@ -23,7 +23,7 @@
     "dtslint": "dtslint dtslint",
     "declaration": "tsc -p declaration/tsconfig.json",
     "mocha": "TS_NODE_CACHE=false mocha -r ts-node/register test/*.ts",
-    "doctoc": "doctoc README.md",
+    "doctoc": "doctoc README.md Type.md Decoder.md Encoder.md Codec.md",
     "docs": "docs-ts",
     "import-path-rewrite": "import-path-rewrite"
   },
diff --git a/src/Codec.ts b/src/Codec.ts
new file mode 100644
index 0000000..61afdc3
--- /dev/null
+++ b/src/Codec.ts
@@ -0,0 +1,204 @@
+/**
+ * @since 2.2.0
+ */
+import { Invariant1 } from 'fp-ts/lib/Invariant'
+import { NonEmptyArray } from 'fp-ts/lib/NonEmptyArray'
+import { Tree } from 'fp-ts/lib/Tree'
+import * as D from './Decoder'
+import * as E from './Encoder'
+import { Schemable, Literal } from './Schemable'
+
+// -------------------------------------------------------------------------------------
+// model
+// -------------------------------------------------------------------------------------
+
+/**
+ * Laws:
+ *
+ * 1. `pipe(codec.decode(u), E.fold(() => u, codec.encode) = u` for all `u` in `unknown`
+ * 2. `codec.decode(codec.encode(a)) = E.right(a)` for all `a` in `A`
+ *
+ * @since 2.2.0
+ */
+export interface Codec<A> extends D.Decoder<A>, E.Encoder<A> {}
+
+// -------------------------------------------------------------------------------------
+// constructors
+// -------------------------------------------------------------------------------------
+
+/**
+ * @since 2.2.0
+ */
+export function make<A>(decoder: D.Decoder<A>, encoder: E.Encoder<A>): Codec<A> {
+  return {
+    decode: decoder.decode,
+    encode: encoder.encode
+  }
+}
+
+/**
+ * @since 2.2.0
+ */
+export function literal<A extends ReadonlyArray<Literal>>(...values: A): Codec<A[number]> {
+  return make(D.decoder.literal(...values), E.encoder.literal(...values))
+}
+
+// -------------------------------------------------------------------------------------
+// primitives
+// -------------------------------------------------------------------------------------
+
+/**
+ * @since 2.2.0
+ */
+export const string: Codec<string> = make(D.decoder.string, E.encoder.string)
+
+/**
+ * @since 2.2.0
+ */
+export const number: Codec<number> = make(D.decoder.number, E.encoder.number)
+
+/**
+ * @since 2.2.0
+ */
+export const boolean: Codec<boolean> = make(D.decoder.boolean, E.encoder.boolean)
+
+/**
+ * @since 2.2.0
+ */
+export const UnknownArray: Codec<Array<unknown>> = make(D.decoder.UnknownArray, E.encoder.UnknownArray)
+
+/**
+ * @since 2.2.0
+ */
+export const UnknownRecord: Codec<Record<string, unknown>> = make(D.decoder.UnknownRecord, E.encoder.UnknownRecord)
+
+// -------------------------------------------------------------------------------------
+// combinators
+// -------------------------------------------------------------------------------------
+
+/**
+ * @since 2.2.0
+ */
+export function withExpected<A>(
+  codec: Codec<A>,
+  expected: (actual: unknown, nea: NonEmptyArray<Tree<string>>) => NonEmptyArray<Tree<string>>
+): Codec<A> {
+  return make(D.withExpected(codec, expected), codec)
+}
+
+/**
+ * @since 2.2.0
+ */
+export function refinement<A, B extends A>(from: Codec<A>, refinement: (a: A) => a is B, expected: string): Codec<B> {
+  return make(D.refinement(from, refinement, expected), from)
+}
+
+/**
+ * @since 2.2.0
+ */
+export function nullable<A>(or: Codec<A>): Codec<null | A> {
+  return make(D.decoder.nullable(or), E.encoder.nullable(or))
+}
+
+/**
+ * @since 2.2.0
+ */
+export function type<A>(properties: { [K in keyof A]: Codec<A[K]> }): Codec<A> {
+  return make(D.decoder.type(properties), E.encoder.type(properties))
+}
+
+/**
+ * @since 2.2.0
+ */
+export function partial<A>(properties: { [K in keyof A]: Codec<A[K]> }): Codec<Partial<A>> {
+  return make(D.decoder.partial(properties), E.encoder.partial(properties))
+}
+
+/**
+ * @since 2.2.0
+ */
+export function record<A>(codomain: Codec<A>): Codec<Record<string, A>> {
+  return make(D.decoder.record(codomain), E.encoder.record(codomain))
+}
+
+/**
+ * @since 2.2.0
+ */
+export function array<A>(items: Codec<A>): Codec<Array<A>> {
+  return make(D.decoder.array(items), E.encoder.array(items))
+}
+
+/**
+ * @since 2.2.0
+ */
+export function tuple<A extends ReadonlyArray<unknown>>(...components: { [K in keyof A]: Codec<A[K]> }): Codec<A> {
+  return make(D.decoder.tuple<A>(...(components as any)), E.encoder.tuple<A>(...(components as any)))
+}
+
+/**
+ * @since 2.2.0
+ */
+export function intersection<A, B>(left: Codec<A>, right: Codec<B>): Codec<A & B> {
+  return make(D.decoder.intersection(left, right), E.encoder.intersection(left, right))
+}
+
+/**
+ * @since 2.2.0
+ */
+export function sum<T extends string>(
+  tag: T
+): <A>(members: { [K in keyof A]: Codec<A[K] & Record<T, K>> }) => Codec<A[keyof A]> {
+  const sumD = D.decoder.sum(tag)
+  const sumE = E.encoder.sum(tag)
+  return (members) => make(sumD(members), sumE(members))
+}
+
+/**
+ * @since 2.2.0
+ */
+export function lazy<A>(id: string, f: () => Codec<A>): Codec<A> {
+  return make(D.decoder.lazy(id, f), E.encoder.lazy(id, f))
+}
+
+// -------------------------------------------------------------------------------------
+// instances
+// -------------------------------------------------------------------------------------
+
+/**
+ * @since 2.2.0
+ */
+export const URI = 'Codec'
+
+/**
+ * @since 2.2.0
+ */
+export type URI = typeof URI
+
+declare module 'fp-ts/lib/HKT' {
+  interface URItoKind<A> {
+    readonly Codec: Codec<A>
+  }
+}
+
+/**
+ * @since 2.2.0
+ */
+export const codec: Invariant1<URI> & Schemable<URI> = {
+  URI,
+  imap: (fa, f, g) => make(D.decoder.map(fa, f), E.encoder.contramap(fa, g)),
+  literal,
+  string,
+  number,
+  boolean,
+  UnknownArray,
+  UnknownRecord,
+  nullable,
+  type,
+  partial,
+  record,
+  array,
+  tuple: tuple as Schemable<URI>['tuple'],
+  intersection,
+  sum,
+  lazy
+}
diff --git a/src/Decoder.ts b/src/Decoder.ts
new file mode 100644
index 0000000..99ef84c
--- /dev/null
+++ b/src/Decoder.ts
@@ -0,0 +1,519 @@
+/**
+ * @since 2.2.0
+ */
+import { Alternative1 } from 'fp-ts/lib/Alternative'
+import { Applicative1 } from 'fp-ts/lib/Applicative'
+import { Either, either, isLeft, isRight, left, mapLeft, right } from 'fp-ts/lib/Either'
+import { NonEmptyArray } from 'fp-ts/lib/NonEmptyArray'
+import { pipe, pipeable } from 'fp-ts/lib/pipeable'
+import { Tree, Forest } from 'fp-ts/lib/Tree'
+import * as G from './Guard'
+import { Schemable, memoize, WithUnion, Literal } from './Schemable'
+
+// -------------------------------------------------------------------------------------
+// model
+// -------------------------------------------------------------------------------------
+
+/**
+ * @since 2.2.0
+ */
+export interface Decoder<A> {
+  readonly decode: (u: unknown) => Either<NonEmptyArray<Tree<string>>, A>
+}
+
+/**
+ * @since 2.2.0
+ */
+export type TypeOf<D> = D extends Decoder<infer A> ? A : never
+
+// -------------------------------------------------------------------------------------
+// constructors
+// -------------------------------------------------------------------------------------
+
+const empty: Array<never> = []
+
+/**
+ * @since 2.2.0
+ */
+export function tree<A>(value: A, forest: Forest<A> = empty): Tree<A> {
+  return {
+    value,
+    forest
+  }
+}
+
+/**
+ * @since 2.2.0
+ */
+export function success<A>(a: A): Either<NonEmptyArray<Tree<string>>, A> {
+  return right(a)
+}
+
+/**
+ * @since 2.2.0
+ */
+export function failure<A = never>(message: string): Either<NonEmptyArray<Tree<string>>, A> {
+  return left([tree(message)])
+}
+
+/**
+ * @since 2.2.0
+ */
+export function failures<A = never>(
+  message: string,
+  errors: NonEmptyArray<Tree<string>>
+): Either<NonEmptyArray<Tree<string>>, A> {
+  return left([tree(message, errors)])
+}
+
+/**
+ * @since 2.2.0
+ */
+export function fromGuard<A>(guard: G.Guard<A>, expected: string): Decoder<A> {
+  return {
+    decode: (u) => (guard.is(u) ? success(u) : failure(`cannot decode ${JSON.stringify(u)}, should be ${expected}`))
+  }
+}
+
+/**
+ * @since 2.2.0
+ */
+export function literal<A extends ReadonlyArray<Literal>>(...values: A): Decoder<A[number]> {
+  if (values.length === 0) {
+    return never
+  }
+  const expected = values.map((value) => JSON.stringify(value)).join(' | ')
+  return fromGuard(G.guard.literal(...values), expected)
+}
+
+// -------------------------------------------------------------------------------------
+// primitives
+// -------------------------------------------------------------------------------------
+
+/**
+ * @since 2.2.0
+ */
+export const never: Decoder<never> = fromGuard(G.never, 'never')
+
+/**
+ * @since 2.2.0
+ */
+export const string: Decoder<string> = fromGuard(G.string, 'string')
+
+/**
+ * @since 2.2.0
+ */
+export const number: Decoder<number> = fromGuard(G.number, 'number')
+
+/**
+ * @since 2.2.0
+ */
+export const boolean: Decoder<boolean> = fromGuard(G.boolean, 'boolean')
+
+/**
+ * @since 2.2.0
+ */
+export const UnknownArray: Decoder<Array<unknown>> = fromGuard(G.UnknownArray, 'Array<unknown>')
+
+/**
+ * @since 2.2.0
+ */
+export const UnknownRecord: Decoder<Record<string, unknown>> = fromGuard(G.UnknownRecord, 'Record<string, unknown>')
+
+// -------------------------------------------------------------------------------------
+// combinators
+// -------------------------------------------------------------------------------------
+
+/**
+ * @since 2.2.0
+ */
+export function withExpected<A>(
+  decoder: Decoder<A>,
+  expected: (actual: unknown, nea: NonEmptyArray<Tree<string>>) => NonEmptyArray<Tree<string>>
+): Decoder<A> {
+  return {
+    decode: (u) =>
+      pipe(
+        decoder.decode(u),
+        mapLeft((nea) => expected(u, nea))
+      )
+  }
+}
+
+/**
+ * @since 2.2.0
+ */
+export function refinement<A, B extends A>(
+  from: Decoder<A>,
+  refinement: (a: A) => a is B,
+  expected: string
+): Decoder<B> {
+  return {
+    decode: (u) => {
+      const e = from.decode(u)
+      if (isLeft(e)) {
+        return e
+      }
+      const a = e.right
+      return refinement(a) ? success(a) : failure(`cannot refine ${JSON.stringify(u)}, should be ${expected}`)
+    }
+  }
+}
+
+/**
+ * @since 2.2.0
+ */
+export function parse<A, B>(from: Decoder<A>, parser: (a: A) => Either<string, B>): Decoder<B> {
+  return {
+    decode: (u) => {
+      const e = from.decode(u)
+      if (isLeft(e)) {
+        return e
+      }
+      const pe = parser(e.right)
+      if (isLeft(pe)) {
+        return failure(pe.left)
+      }
+      return pe
+    }
+  }
+}
+
+/**
+ * @since 2.2.0
+ */
+export function nullable<A>(or: Decoder<A>): Decoder<null | A> {
+  return union(literal(null), or)
+}
+
+/**
+ * @since 2.2.0
+ */
+export function type<A>(properties: { [K in keyof A]: Decoder<A[K]> }): Decoder<A> {
+  return {
+    decode: (u) => {
+      const e = UnknownRecord.decode(u)
+      if (isLeft(e)) {
+        return e
+      } else {
+        const r = e.right
+        let a: Partial<A> = {}
+        for (const k in properties) {
+          const e = properties[k].decode(r[k])
+          if (isLeft(e)) {
+            return failures(`required property ${JSON.stringify(k)}`, e.left)
+          } else {
+            a[k] = e.right
+          }
+        }
+        return success(a as A)
+      }
+    }
+  }
+}
+
+/**
+ * @since 2.2.0
+ */
+export function partial<A>(properties: { [K in keyof A]: Decoder<A[K]> }): Decoder<Partial<A>> {
+  return {
+    decode: (u) => {
+      const e = UnknownRecord.decode(u)
+      if (isLeft(e)) {
+        return e
+      } else {
+        const r = e.right
+        let a: Partial<A> = {}
+        for (const k in properties) {
+          // don't add missing properties
+          if (k in r) {
+            const rk = r[k]
+            // don't strip undefined properties
+            if (rk === undefined) {
+              a[k] = undefined
+            } else {
+              const e = properties[k].decode(rk)
+              if (isLeft(e)) {
+                return failures(`optional property ${JSON.stringify(k)}`, e.left)
+              } else {
+                a[k] = e.right
+              }
+            }
+          }
+        }
+        return success(a)
+      }
+    }
+  }
+}
+
+/**
+ * @since 2.2.0
+ */
+export function record<A>(codomain: Decoder<A>): Decoder<Record<string, A>> {
+  return {
+    decode: (u) => {
+      const e = UnknownRecord.decode(u)
+      if (isLeft(e)) {
+        return e
+      } else {
+        const r = e.right
+        let a: Record<string, A> = {}
+        for (const k in r) {
+          const e = codomain.decode(r[k])
+          if (isLeft(e)) {
+            return failures(`key ${JSON.stringify(k)}`, e.left)
+          } else {
+            a[k] = e.right
+          }
+        }
+        return success(a)
+      }
+    }
+  }
+}
+
+/**
+ * @since 2.2.0
+ */
+export function array<A>(items: Decoder<A>): Decoder<Array<A>> {
+  return {
+    decode: (u) => {
+      const e = UnknownArray.decode(u)
+      if (isLeft(e)) {
+        return e
+      } else {
+        const us = e.right
+        const len = us.length
+        const a: Array<A> = new Array(len)
+        for (let i = 0; i < len; i++) {
+          const e = items.decode(us[i])
+          if (isLeft(e)) {
+            return failures(`item ${i}`, e.left)
+          } else {
+            a[i] = e.right
+          }
+        }
+        return success(a)
+      }
+    }
+  }
+}
+
+/**
+ * @since 2.2.0
+ */
+export function tuple<A extends ReadonlyArray<unknown>>(...components: { [K in keyof A]: Decoder<A[K]> }): Decoder<A> {
+  return {
+    decode: (u) => {
+      const e = UnknownArray.decode(u)
+      if (isLeft(e)) {
+        return e
+      }
+      const us = e.right
+      const a: Array<unknown> = []
+      for (let i = 0; i < components.length; i++) {
+        const e = components[i].decode(us[i])
+        if (isLeft(e)) {
+          return failures(`component ${i}`, e.left)
+        } else {
+          a.push(e.right)
+        }
+      }
+      return success(a as any)
+    }
+  }
+}
+
+function typeOf(x: unknown): string {
+  return x === null ? 'null' : typeof x
+}
+
+/**
+ * @internal
+ */
+export function intersect<A, B>(a: A, b: B): A & B {
+  if (a !== undefined && b !== undefined) {
+    const tx = typeOf(a)
+    const ty = typeOf(b)
+    if (tx === 'object' || ty === 'object') {
+      return Object.assign({}, a, b)
+    }
+  }
+  return b as any
+}
+
+/**
+ * @since 2.2.0
+ */
+export function intersection<A, B>(left: Decoder<A>, right: Decoder<B>): Decoder<A & B> {
+  return {
+    decode: (u) => {
+      const ea = left.decode(u)
+      if (isLeft(ea)) {
+        return ea
+      }
+      const eb = right.decode(u)
+      if (isLeft(eb)) {
+        return eb
+      }
+      return success(intersect(ea.right, eb.right))
+    }
+  }
+}
+
+/**
+ * @since 2.2.0
+ */
+export function lazy<A>(id: string, f: () => Decoder<A>): Decoder<A> {
+  const get = memoize<void, Decoder<A>>(f)
+  return {
+    decode: (u) =>
+      pipe(
+        get().decode(u),
+        mapLeft((nea) => [tree(id, nea)])
+      )
+  }
+}
+
+/**
+ * @since 2.2.0
+ */
+export function sum<T extends string>(
+  tag: T
+): <A>(members: { [K in keyof A]: Decoder<A[K] & Record<T, K>> }) => Decoder<A[keyof A]> {
+  return (members) => {
+    const keys = Object.keys(members)
+    if (keys.length === 0) {
+      return never
+    }
+    const expected = keys.map((k) => JSON.stringify(k)).join(' | ')
+    return {
+      decode: (u) => {
+        const e = UnknownRecord.decode(u)
+        if (isLeft(e)) {
+          return e
+        }
+        const v = e.right[tag]
+        if (G.string.is(v) && v in members) {
+          return (members as any)[v].decode(u)
+        }
+        return failures(`required property ${JSON.stringify(tag)}`, [
+          tree(`cannot decode ${JSON.stringify(v)}, should be ${expected}`)
+        ])
+      }
+    }
+  }
+}
+
+/**
+ * @since 2.2.0
+ */
+export function union<A extends ReadonlyArray<unknown>>(
+  ...members: { [K in keyof A]: Decoder<A[K]> }
+): Decoder<A[number]> {
+  const len = members.length
+  if (len === 0) {
+    return never
+  }
+  return {
+    decode: (u) => {
+      const e = members[0].decode(u)
+      if (isRight(e)) {
+        return e
+      } else {
+        const forest: NonEmptyArray<Tree<string>> = [tree(`member 0`, e.left)]
+        for (let i = 1; i < len; i++) {
+          const e = members[i].decode(u)
+          if (isRight(e)) {
+            return e
+          } else {
+            forest.push(tree(`member ${i}`, e.left))
+          }
+        }
+        return left(forest)
+      }
+    }
+  }
+}
+
+// -------------------------------------------------------------------------------------
+// instances
+// -------------------------------------------------------------------------------------
+
+/**
+ * @since 2.2.0
+ */
+export const URI = 'Decoder'
+
+/**
+ * @since 2.2.0
+ */
+export type URI = typeof URI
+
+declare module 'fp-ts/lib/HKT' {
+  interface URItoKind<A> {
+    readonly Decoder: Decoder<A>
+  }
+}
+
+/**
+ * @since 2.2.0
+ */
+export const decoder: Applicative1<URI> & Alternative1<URI> & Schemable<URI> & WithUnion<URI> = {
+  URI,
+  map: (fa, f) => ({
+    decode: (u) => either.map(fa.decode(u), f)
+  }),
+  of: (a) => ({
+    decode: () => success(a)
+  }),
+  ap: (fab, fa) => ({
+    decode: (u) => either.ap(fab.decode(u), fa.decode(u))
+  }),
+  alt: (fx, fy) => ({
+    decode: (u) => either.alt(fx.decode(u), () => fy().decode(u))
+  }),
+  zero: () => never,
+  literal,
+  string,
+  number,
+  boolean,
+  UnknownArray,
+  UnknownRecord,
+  nullable,
+  type,
+  partial,
+  record,
+  array,
+  tuple: tuple as Schemable<URI>['tuple'],
+  intersection,
+  sum,
+  lazy,
+  union
+}
+
+const { alt, ap, apFirst, apSecond, map } = pipeable(decoder)
+
+export {
+  /**
+   * @since 2.2.0
+   */
+  alt,
+  /**
+   * @since 2.2.0
+   */
+  ap,
+  /**
+   * @since 2.2.0
+   */
+  apFirst,
+  /**
+   * @since 2.2.0
+   */
+  apSecond,
+  /**
+   * @since 2.2.0
+   */
+  map
+}
diff --git a/src/Encoder.ts b/src/Encoder.ts
new file mode 100644
index 0000000..907d292
--- /dev/null
+++ b/src/Encoder.ts
@@ -0,0 +1,197 @@
+/**
+ * @since 2.2.0
+ */
+import { Contravariant1 } from 'fp-ts/lib/Contravariant'
+import { identity } from 'fp-ts/lib/function'
+import { pipeable } from 'fp-ts/lib/pipeable'
+import { Schemable, memoize } from './Schemable'
+import { intersect } from './Decoder'
+
+// -------------------------------------------------------------------------------------
+// model
+// -------------------------------------------------------------------------------------
+
+/**
+ * @since 2.2.0
+ */
+export interface Encoder<A> {
+  readonly encode: (a: A) => unknown
+}
+
+// -------------------------------------------------------------------------------------
+// primitives
+// -------------------------------------------------------------------------------------
+
+/**
+ * @since 2.2.0
+ */
+export const id: Encoder<unknown> = {
+  encode: identity
+}
+
+// -------------------------------------------------------------------------------------
+// combinators
+// -------------------------------------------------------------------------------------
+
+/**
+ * @since 2.2.0
+ */
+export function nullable<A>(or: Encoder<A>): Encoder<null | A> {
+  return {
+    encode: (a) => (a === null ? a : or.encode(a))
+  }
+}
+
+/**
+ * @since 2.2.0
+ */
+export function type<A>(properties: { [K in keyof A]: Encoder<A[K]> }): Encoder<A> {
+  return {
+    encode: (a) => {
+      const o: Record<string, unknown> = {}
+      for (const k in properties) {
+        o[k] = properties[k].encode(a[k])
+      }
+      return o
+    }
+  }
+}
+
+/**
+ * @since 2.2.0
+ */
+export function partial<A>(properties: { [K in keyof A]: Encoder<A[K]> }): Encoder<Partial<A>> {
+  return {
+    encode: (a) => {
+      const o: Record<string, unknown> = {}
+      for (const k in properties) {
+        const v: A[Extract<keyof A, string>] | undefined = a[k]
+        // don't add missing properties
+        if (k in a) {
+          // don't strip undefined properties
+          o[k] = v === undefined ? v : properties[k].encode(v)
+        }
+      }
+      return o
+    }
+  }
+}
+
+/**
+ * @since 2.2.0
+ */
+export function record<A>(codomain: Encoder<A>): Encoder<Record<string, A>> {
+  return {
+    encode: (r) => {
+      const o: Record<string, unknown> = {}
+      for (const k in r) {
+        o[k] = codomain.encode(r[k])
+      }
+      return o
+    }
+  }
+}
+
+/**
+ * @since 2.2.0
+ */
+export function array<A>(items: Encoder<A>): Encoder<Array<A>> {
+  return {
+    encode: (as) => as.map(items.encode)
+  }
+}
+
+/**
+ * @since 2.2.0
+ */
+export function tuple<A extends ReadonlyArray<unknown>>(...components: { [K in keyof A]: Encoder<A[K]> }): Encoder<A> {
+  return {
+    encode: (as) => components.map((c, i) => c.encode(as[i]))
+  }
+}
+
+/**
+ * @since 2.2.0
+ */
+export function intersection<A, B>(left: Encoder<A>, right: Encoder<B>): Encoder<A & B> {
+  return {
+    encode: (ab) => intersect(left.encode(ab), right.encode(ab))
+  }
+}
+
+/**
+ * @since 2.2.0
+ */
+export function sum<T extends string>(
+  tag: T
+): <A>(members: { [K in keyof A]: Encoder<A[K] & Record<T, K>> }) => Encoder<A[keyof A]> {
+  return (members: Record<string, Encoder<any>>) => {
+    return {
+      encode: (a: Record<string, any>) => members[a[tag]].encode(a)
+    }
+  }
+}
+
+/**
+ * @since 2.2.0
+ */
+export function lazy<A>(f: () => Encoder<A>): Encoder<A> {
+  const get = memoize<void, Encoder<A>>(f)
+  return {
+    encode: (a) => get().encode(a)
+  }
+}
+
+// -------------------------------------------------------------------------------------
+// instances
+// -------------------------------------------------------------------------------------
+
+/**
+ * @since 2.2.0
+ */
+export const URI = 'Encoder'
+
+/**
+ * @since 2.2.0
+ */
+export type URI = typeof URI
+
+declare module 'fp-ts/lib/HKT' {
+  interface URItoKind<A> {
+    readonly Encoder: Encoder<A>
+  }
+}
+
+/**
+ * @since 2.2.0
+ */
+export const encoder: Contravariant1<URI> & Schemable<URI> = {
+  URI,
+  contramap: (fa, f) => ({
+    encode: (b) => fa.encode(f(b))
+  }),
+  literal: () => id,
+  string: id,
+  number: id,
+  boolean: id,
+  UnknownArray: id,
+  UnknownRecord: id,
+  nullable,
+  type,
+  partial,
+  record,
+  array,
+  tuple,
+  intersection,
+  sum,
+  lazy: (_, f) => lazy(f)
+}
+
+const { contramap } = pipeable(encoder)
+
+export {
+  /**
+   * @since 2.2.0
+   */
+  contramap
+}
diff --git a/src/Guard.ts b/src/Guard.ts
new file mode 100644
index 0000000..e21c827
--- /dev/null
+++ b/src/Guard.ts
@@ -0,0 +1,244 @@
+/**
+ * @since 2.2.0
+ */
+import { Schemable, memoize, WithUnion, Literal } from './Schemable'
+
+// -------------------------------------------------------------------------------------
+// model
+// -------------------------------------------------------------------------------------
+
+/**
+ * @since 2.2.0
+ */
+export interface Guard<A> {
+  is: (u: unknown) => u is A
+}
+
+// -------------------------------------------------------------------------------------
+// constructors
+// -------------------------------------------------------------------------------------
+
+/**
+ * @since 2.2.0
+ */
+export function literal<A extends ReadonlyArray<Literal>>(...values: A): Guard<A[number]> {
+  return {
+    is: (u: unknown): u is A[number] => values.findIndex((a) => a === u) !== -1
+  }
+}
+
+// -------------------------------------------------------------------------------------
+// primitives
+// -------------------------------------------------------------------------------------
+
+/**
+ * @since 2.2.0
+ */
+export const never: Guard<never> = {
+  is: (_u): _u is never => false
+}
+
+/**
+ * @since 2.2.0
+ */
+export const string: Guard<string> = {
+  is: (u: unknown): u is string => typeof u === 'string'
+}
+
+/**
+ * @since 2.2.0
+ */
+export const number: Guard<number> = {
+  is: (u: unknown): u is number => typeof u === 'number'
+}
+
+/**
+ * @since 2.2.0
+ */
+export const boolean: Guard<boolean> = {
+  is: (u: unknown): u is boolean => typeof u === 'boolean'
+}
+
+/**
+ * @since 2.2.0
+ */
+export const UnknownArray: Guard<Array<unknown>> = {
+  is: Array.isArray
+}
+
+/**
+ * @since 2.2.0
+ */
+export const UnknownRecord: Guard<Record<string, unknown>> = {
+  is: (u: unknown): u is Record<string, unknown> => Object.prototype.toString.call(u) === '[object Object]'
+}
+
+// -------------------------------------------------------------------------------------
+// combinators
+// -------------------------------------------------------------------------------------
+
+/**
+ * @since 2.2.0
+ */
+export function refinement<A, B extends A>(from: Guard<A>, refinement: (a: A) => a is B): Guard<B> {
+  return {
+    is: (u: unknown): u is B => from.is(u) && refinement(u)
+  }
+}
+
+/**
+ * @since 2.2.0
+ */
+export function nullable<A>(or: Guard<A>): Guard<null | A> {
+  return {
+    is: (u): u is null | A => u === null || or.is(u)
+  }
+}
+
+/**
+ * @since 2.2.0
+ */
+export function type<A>(properties: { [K in keyof A]: Guard<A[K]> }): Guard<A> {
+  return refinement(UnknownRecord, (r): r is {
+    [K in keyof A]: A[K]
+  } => {
+    for (const k in properties) {
+      if (!(k in r) || !properties[k].is(r[k])) {
+        return false
+      }
+    }
+    return true
+  })
+}
+
+/**
+ * @since 2.2.0
+ */
+export function partial<A>(properties: { [K in keyof A]: Guard<A[K]> }): Guard<Partial<A>> {
+  return refinement(UnknownRecord, (r): r is Partial<A> => {
+    for (const k in properties) {
+      const v = r[k]
+      if (v !== undefined && !properties[k].is(v)) {
+        return false
+      }
+    }
+    return true
+  })
+}
+
+/**
+ * @since 2.2.0
+ */
+export function record<A>(codomain: Guard<A>): Guard<Record<string, A>> {
+  return refinement(UnknownRecord, (r): r is Record<string, A> => {
+    for (const k in r) {
+      if (!codomain.is(r[k])) {
+        return false
+      }
+    }
+    return true
+  })
+}
+
+/**
+ * @since 2.2.0
+ */
+export function array<A>(items: Guard<A>): Guard<Array<A>> {
+  return refinement(UnknownArray, (us): us is Array<A> => us.every(items.is))
+}
+
+/**
+ * @since 2.2.0
+ */
+export function tuple<A extends ReadonlyArray<unknown>>(...components: { [K in keyof A]: Guard<A[K]> }): Guard<A> {
+  return {
+    is: (u): u is A => Array.isArray(u) && u.length === components.length && components.every((c, i) => c.is(u[i]))
+  }
+}
+
+/**
+ * @since 2.2.0
+ */
+export function intersection<A, B>(left: Guard<A>, right: Guard<B>): Guard<A & B> {
+  return {
+    is: (u: unknown): u is A & B => left.is(u) && right.is(u)
+  }
+}
+
+/**
+ * @since 2.2.0
+ */
+export function union<A extends ReadonlyArray<unknown>>(...members: { [K in keyof A]: Guard<A[K]> }): Guard<A[number]> {
+  return {
+    is: (u: unknown): u is A | A[number] => members.some((m) => m.is(u))
+  }
+}
+
+/**
+ * @since 2.2.0
+ */
+export function sum<T extends string>(
+  tag: T
+): <A>(members: { [K in keyof A]: Guard<A[K] & Record<T, K>> }) => Guard<A[keyof A]> {
+  return <A>(members: { [K in keyof A]: Guard<A[K] & Record<T, K>> }) =>
+    refinement(UnknownRecord, (r): r is { [K in keyof A]: A[K] & Record<T, K> }[keyof A] => {
+      const v = r[tag]
+      if (string.is(v) && v in members) {
+        return (members as any)[v].is(r)
+      }
+      return false
+    })
+}
+
+/**
+ * @since 2.2.0
+ */
+export function lazy<A>(f: () => Guard<A>): Guard<A> {
+  const get = memoize<void, Guard<A>>(f)
+  return {
+    is: (u: unknown): u is A => get().is(u)
+  }
+}
+
+// -------------------------------------------------------------------------------------
+// instances
+// -------------------------------------------------------------------------------------
+
+/**
+ * @since 2.2.0
+ */
+export const URI = 'Guard'
+
+/**
+ * @since 2.2.0
+ */
+export type URI = typeof URI
+
+declare module 'fp-ts/lib/HKT' {
+  interface URItoKind<A> {
+    readonly Guard: Guard<A>
+  }
+}
+
+/**
+ * @since 2.2.0
+ */
+export const guard: Schemable<URI> & WithUnion<URI> = {
+  URI,
+  literal,
+  string,
+  number,
+  boolean,
+  UnknownArray,
+  UnknownRecord,
+  nullable,
+  type,
+  partial,
+  record,
+  array,
+  tuple: tuple as Schemable<URI>['tuple'],
+  intersection,
+  sum,
+  lazy: (_, f) => lazy(f),
+  union
+}
diff --git a/src/Schema.ts b/src/Schema.ts
new file mode 100644
index 0000000..30fd480
--- /dev/null
+++ b/src/Schema.ts
@@ -0,0 +1,24 @@
+/**
+ * @since 2.2.0
+ */
+import { Kind, URIS } from 'fp-ts/lib/HKT'
+import { Schemable, memoize } from './Schemable'
+
+/**
+ * @since 2.2.0
+ */
+export interface Schema<A> {
+  <S extends URIS>(S: Schemable<S>): Kind<S, A>
+}
+
+/**
+ * @since 2.2.0
+ */
+export type TypeOf<S> = S extends Schema<infer A> ? A : never
+
+/**
+ * @since 2.2.0
+ */
+export function make<A>(f: Schema<A>): Schema<A> {
+  return memoize(f)
+}
diff --git a/src/Schemable.ts b/src/Schemable.ts
new file mode 100644
index 0000000..a4d615c
--- /dev/null
+++ b/src/Schemable.ts
@@ -0,0 +1,57 @@
+/**
+ * @since 2.2.0
+ */
+import { Kind, URIS } from 'fp-ts/lib/HKT'
+
+/**
+ * @since 2.2.0
+ */
+export type Literal = string | number | boolean | null
+
+/**
+ * @since 2.2.0
+ */
+export interface Schemable<S extends URIS> {
+  readonly URI: S
+  readonly literal: <A extends ReadonlyArray<Literal>>(...values: A) => Kind<S, A[number]>
+  readonly string: Kind<S, string>
+  readonly number: Kind<S, number>
+  readonly boolean: Kind<S, boolean>
+  readonly UnknownArray: Kind<S, Array<unknown>>
+  readonly UnknownRecord: Kind<S, Record<string, unknown>>
+  readonly nullable: <A>(or: Kind<S, A>) => Kind<S, null | A>
+  readonly type: <A>(properties: { [K in keyof A]: Kind<S, A[K]> }) => Kind<S, A>
+  readonly partial: <A>(properties: { [K in keyof A]: Kind<S, A[K]> }) => Kind<S, Partial<A>>
+  readonly record: <A>(codomain: Kind<S, A>) => Kind<S, Record<string, A>>
+  readonly array: <A>(items: Kind<S, A>) => Kind<S, Array<A>>
+  readonly tuple: <A extends ReadonlyArray<unknown>>(...components: { [K in keyof A]: Kind<S, A[K]> }) => Kind<S, A>
+  readonly intersection: <A, B>(left: Kind<S, A>, right: Kind<S, B>) => Kind<S, A & B>
+  readonly sum: <T extends string>(
+    tag: T
+  ) => <A>(members: { [K in keyof A]: Kind<S, A[K] & Record<T, K>> }) => Kind<S, A[keyof A]>
+  readonly lazy: <A>(id: string, f: () => Kind<S, A>) => Kind<S, A>
+}
+
+/**
+ * @since 2.2.0
+ */
+export interface WithUnion<S extends URIS> {
+  readonly union: <A extends ReadonlyArray<unknown>>(
+    ...members: { [K in keyof A]: Kind<S, A[K]> }
+  ) => Kind<S, A[number]>
+}
+
+/**
+ * @since 2.2.0
+ */
+export function memoize<A, B>(f: (a: A) => B): (a: A) => B {
+  let cache = new Map()
+  return (a) => {
+    if (!cache.has(a)) {
+      const b = f(a)
+      cache.set(a, b)
+      return b
+    }
+    return cache.get(a)
+  }
+}
diff --git a/src/Tree.ts b/src/Tree.ts
new file mode 100644
index 0000000..79a38c1
--- /dev/null
+++ b/src/Tree.ts
@@ -0,0 +1,12 @@
+/**
+ * @since 2.2.0
+ */
+import { drawTree, Tree } from 'fp-ts/lib/Tree'
+import { NonEmptyArray } from 'fp-ts/lib/NonEmptyArray'
+
+/**
+ * @since 2.2.0
+ */
+export function draw(es: NonEmptyArray<Tree<string>>): string {
+  return es.map(drawTree).join('\n')
+}


Test patch:
diff --git a/test/Codec.ts b/test/Codec.ts
new file mode 100644
index 0000000..0e5d89d
--- /dev/null
+++ b/test/Codec.ts
@@ -0,0 +1,532 @@
+import * as assert from 'assert'
+import { left, right } from 'fp-ts/lib/Either'
+import * as C from '../src/Codec'
+import * as D from '../src/Decoder'
+import * as G from '../src/Guard'
+import * as E from '../src/Encoder'
+
+const NumberFromString: C.Codec<number> = C.make(
+  D.parse(D.string, (s) => {
+    const n = parseFloat(s)
+    return isNaN(n) ? left(`cannot decode ${JSON.stringify(s)}, should be parsable into a number`) : right(n)
+  }),
+  { encode: String }
+)
+
+interface PositiveBrand {
+  readonly Positive: unique symbol
+}
+type Positive = number & PositiveBrand
+const Positive: C.Codec<Positive> = C.refinement(C.number, (n): n is Positive => n > 0, 'Positive')
+
+interface IntBrand {
+  readonly Int: unique symbol
+}
+type Int = number & IntBrand
+const Int: C.Codec<Int> = C.refinement(C.number, (n): n is Int => Number.isInteger(n), 'Int')
+
+const undefinedGuard: G.Guard<undefined> = {
+  is: (u): u is undefined => u === undefined
+}
+const undef: C.Codec<undefined> = C.make(D.fromGuard(undefinedGuard, 'undefined'), E.id)
+
+describe('Codec', () => {
+  describe('codec', () => {
+    it('imap', () => {
+      const codec = C.codec.imap(
+        C.string,
+        (s) => ({ value: s }),
+        ({ value }) => value
+      )
+      assert.deepStrictEqual(codec.decode('a'), right({ value: 'a' }))
+      assert.deepStrictEqual(codec.encode({ value: 'a' }), 'a')
+    })
+  })
+
+  describe('withExpected', () => {
+    describe('decode', () => {
+      it('should, return the provided expected', () => {
+        const codec = C.withExpected(C.number, () => [D.tree(`not a number`)])
+        assert.deepStrictEqual(codec.decode('a'), left([D.tree('not a number')]))
+      })
+    })
+  })
+
+  describe('string', () => {
+    describe('decode', () => {
+      it('should decode a valid input', () => {
+        const codec = C.string
+        assert.deepStrictEqual(codec.decode('a'), right('a'))
+      })
+
+      it('should reject an invalid input', () => {
+        const codec = C.string
+        assert.deepStrictEqual(codec.decode(null), left([D.tree('cannot decode null, should be string')]))
+      })
+    })
+  })
+
+  describe('number', () => {
+    describe('decode', () => {
+      it('should decode a valid input', () => {
+        const codec = C.number
+        assert.deepStrictEqual(codec.decode(1), right(1))
+      })
+
+      it('should reject an invalid input', () => {
+        const codec = C.number
+        assert.deepStrictEqual(codec.decode(null), left([D.tree('cannot decode null, should be number')]))
+      })
+    })
+  })
+
+  describe('boolean', () => {
+    describe('decode', () => {
+      it('should decode a valid input', () => {
+        const codec = C.boolean
+        assert.deepStrictEqual(codec.decode(true), right(true))
+        assert.deepStrictEqual(codec.decode(false), right(false))
+      })
+
+      it('should reject an invalid input', () => {
+        const codec = C.boolean
+        assert.deepStrictEqual(codec.decode(null), left([D.tree('cannot decode null, should be boolean')]))
+      })
+    })
+  })
+
+  describe('literal', () => {
+    describe('decode', () => {
+      it('should decode a valid input', () => {
+        const codec = C.literal('a', null, 'b', 1, true)
+        assert.deepStrictEqual(codec.decode('a'), right('a'))
+        assert.deepStrictEqual(codec.decode(null), right(null))
+      })
+
+      it('should reject an invalid input', () => {
+        const codec = C.literal('a', null)
+        assert.deepStrictEqual(codec.decode('b'), left([D.tree('cannot decode "b", should be "a" | null')]))
+      })
+
+      it('should handle zero members', () => {
+        assert.deepStrictEqual(C.literal().decode({}), left([D.tree('cannot decode {}, should be never')]))
+      })
+    })
+
+    describe('encode', () => {
+      it('should encode a value', () => {
+        const codec = C.literal('a')
+        assert.deepStrictEqual(codec.encode('a'), 'a')
+      })
+    })
+  })
+
+  describe('refinement', () => {
+    describe('decode', () => {
+      it('should decode a valid input', () => {
+        const codec = C.refinement(C.string, (s): s is string => s.length > 0, 'NonEmptyString')
+        assert.deepStrictEqual(codec.decode('a'), right('a'))
+      })
+
+      it('should reject an invalid input', () => {
+        const codec = C.refinement(C.string, (s): s is string => s.length > 0, 'NonEmptyString')
+        assert.deepStrictEqual(codec.decode(undefined), left([D.tree('cannot decode undefined, should be string')]))
+        assert.deepStrictEqual(codec.decode(''), left([D.tree('cannot refine "", should be NonEmptyString')]))
+      })
+    })
+
+    describe('encode', () => {
+      it('should encode a value', () => {
+        const codec = C.refinement(C.string, (s): s is string => s.length > 0, 'NonEmptyString')
+        assert.strictEqual(codec.encode('a'), 'a')
+      })
+    })
+  })
+
+  describe('nullable', () => {
+    describe('decode', () => {
+      it('should decode a valid input', () => {
+        const codec = C.nullable(NumberFromString)
+        assert.deepStrictEqual(codec.decode(null), right(null))
+        assert.deepStrictEqual(codec.decode('1'), right(1))
+      })
+
+      it('should reject an invalid input', () => {
+        const codec = C.nullable(NumberFromString)
+        assert.deepStrictEqual(
+          codec.decode(undefined),
+          left([
+            D.tree('member 0', [D.tree('cannot decode undefined, should be null')]),
+            D.tree('member 1', [D.tree('cannot decode undefined, should be string')])
+          ])
+        )
+        assert.deepStrictEqual(
+          codec.decode('a'),
+          left([
+            D.tree('member 0', [D.tree('cannot decode "a", should be null')]),
+            D.tree('member 1', [D.tree('cannot decode "a", should be parsable into a number')])
+          ])
+        )
+      })
+    })
+
+    describe('encode', () => {
+      it('should encode a value', () => {
+        const codec = C.nullable(NumberFromString)
+        assert.strictEqual(codec.encode(null), null)
+        assert.strictEqual(codec.encode(1), '1')
+      })
+    })
+  })
+
+  describe('type', () => {
+    describe('decode', () => {
+      it('should decode a valid input', () => {
+        const codec = C.type({
+          a: C.string
+        })
+        assert.deepStrictEqual(codec.decode({ a: 'a' }), right({ a: 'a' }))
+      })
+
+      it('should strip additional fields', () => {
+        const codec = C.type({
+          a: C.string
+        })
+        assert.deepStrictEqual(codec.decode({ a: 'a', b: 1 }), right({ a: 'a' }))
+      })
+
+      it('should not strip fields corresponding to undefined values', () => {
+        const codec = C.type({
+          a: undef
+        })
+        assert.deepStrictEqual(codec.decode({}), right({ a: undefined }))
+      })
+
+      it('should reject an invalid input', () => {
+        const codec = C.type({
+          a: C.string
+        })
+        assert.deepStrictEqual(
+          codec.decode(undefined),
+          left([D.tree('cannot decode undefined, should be Record<string, unknown>')])
+        )
+        assert.deepStrictEqual(
+          codec.decode({ a: 1 }),
+          left([D.tree('required property "a"', [D.tree('cannot decode 1, should be string')])])
+        )
+      })
+    })
+
+    describe('encode', () => {
+      it('should encode a value', () => {
+        const codec = C.type({ a: NumberFromString })
+        assert.deepStrictEqual(codec.encode({ a: 1 }), { a: '1' })
+      })
+
+      it('should strip additional fields', () => {
+        const codec = C.type({ a: C.number })
+        const a = { a: 1, b: true }
+        assert.deepStrictEqual(codec.encode(a), { a: 1 })
+      })
+    })
+  })
+
+  describe('partial', () => {
+    describe('decode', () => {
+      it('should decode a valid input', () => {
+        const codec = C.partial({ a: C.string })
+        assert.deepStrictEqual(codec.decode({ a: 'a' }), right({ a: 'a' }))
+        assert.deepStrictEqual(codec.decode({}), right({}))
+      })
+
+      it('should strip additional fields', () => {
+        const codec = C.partial({ a: C.string })
+        assert.deepStrictEqual(codec.decode({ a: 'a', b: 1 }), right({ a: 'a' }))
+      })
+
+      it('should not add missing fields', () => {
+        const codec = C.partial({ a: C.string })
+        assert.deepStrictEqual(codec.decode({}), right({}))
+      })
+
+      it('should not strip fields corresponding to undefined values', () => {
+        const codec = C.partial({ a: C.string })
+        assert.deepStrictEqual(codec.decode({ a: undefined }), right({ a: undefined }))
+      })
+
+      it('should reject an invalid input', () => {
+        const codec = C.partial({ a: C.string })
+        assert.deepStrictEqual(
+          codec.decode(undefined),
+          left([D.tree('cannot decode undefined, should be Record<string, unknown>')])
+        )
+        assert.deepStrictEqual(
+          codec.decode({ a: 1 }),
+          left([D.tree('optional property "a"', [D.tree('cannot decode 1, should be string')])])
+        )
+      })
+    })
+
+    describe('encode', () => {
+      it('should encode a value', () => {
+        const codec = C.partial({ a: NumberFromString })
+        assert.deepStrictEqual(codec.encode({}), {})
+        assert.deepStrictEqual(codec.encode({ a: 1 }), { a: '1' })
+      })
+
+      it('should strip additional fields', () => {
+        const codec = C.partial({ a: C.string })
+        const a = { a: 'a', b: true }
+        assert.deepStrictEqual(codec.encode(a), { a: 'a' })
+      })
+
+      it('should not add missing fields', () => {
+        const codec = C.partial({ a: C.string })
+        assert.deepStrictEqual(codec.encode({}), {})
+      })
+
+      it('should not strip fields corresponding to undefined values', () => {
+        const codec = C.partial({ a: C.string })
+        assert.deepStrictEqual(codec.encode({ a: undefined }), { a: undefined })
+      })
+    })
+  })
+
+  describe('record', () => {
+    describe('decode', () => {
+      it('should decode a valid value', () => {
+        const codec = C.record(C.number)
+        assert.deepStrictEqual(codec.decode({}), right({}))
+        assert.deepStrictEqual(codec.decode({ a: 1 }), right({ a: 1 }))
+      })
+
+      it('should reject an invalid value', () => {
+        const codec = C.record(C.number)
+        assert.deepStrictEqual(
+          codec.decode(undefined),
+          left([D.tree('cannot decode undefined, should be Record<string, unknown>')])
+        )
+        assert.deepStrictEqual(
+          codec.decode({ a: 'a' }),
+          left([D.tree('key "a"', [D.tree('cannot decode "a", should be number')])])
+        )
+      })
+    })
+
+    describe('encode', () => {
+      it('should encode a value', () => {
+        const codec = C.record(NumberFromString)
+        assert.deepStrictEqual(codec.encode({ a: 1, b: 2 }), { a: '1', b: '2' })
+      })
+    })
+  })
+
+  describe('array', () => {
+    describe('decode', () => {
+      it('should decode a valid input', () => {
+        const codec = C.array(C.string)
+        assert.deepStrictEqual(codec.decode([]), right([]))
+        assert.deepStrictEqual(codec.decode(['a']), right(['a']))
+      })
+
+      it('should reject an invalid input', () => {
+        const codec = C.array(C.string)
+        assert.deepStrictEqual(
+          codec.decode(undefined),
+          left([D.tree('cannot decode undefined, should be Array<unknown>')])
+        )
+        assert.deepStrictEqual(
+          codec.decode([1]),
+          left([D.tree('item 0', [D.tree('cannot decode 1, should be string')])])
+        )
+      })
+    })
+
+    describe('encode', () => {
+      it('should encode a value', () => {
+        const codec = C.array(NumberFromString)
+        assert.deepStrictEqual(codec.encode([1, 2]), ['1', '2'])
+      })
+    })
+  })
+
+  describe('tuple', () => {
+    describe('decode', () => {
+      it('should decode a valid input', () => {
+        const codec = C.tuple(C.string, C.number)
+        assert.deepStrictEqual(codec.decode(['a', 1]), right(['a', 1]))
+      })
+
+      it('should handle zero components', () => {
+        assert.deepStrictEqual(C.tuple().decode([]), right([]))
+      })
+
+      it('should reject an invalid input', () => {
+        const codec = C.tuple(C.string, C.number)
+        assert.deepStrictEqual(
+          codec.decode(undefined),
+          left([D.tree('cannot decode undefined, should be Array<unknown>')])
+        )
+        assert.deepStrictEqual(
+          codec.decode(['a']),
+          left([D.tree('component 1', [D.tree('cannot decode undefined, should be number')])])
+        )
+        assert.deepStrictEqual(
+          codec.decode([1, 2]),
+          left([D.tree('component 0', [D.tree('cannot decode 1, should be string')])])
+        )
+      })
+
+      it('should strip additional components', () => {
+        const codec = C.tuple(C.string, C.number)
+        assert.deepStrictEqual(codec.decode(['a', 1, true]), right(['a', 1]))
+      })
+    })
+
+    describe('encode', () => {
+      it('should encode a value', () => {
+        const codec = C.tuple(NumberFromString, C.string)
+        assert.deepStrictEqual(codec.encode([1, 'a']), ['1', 'a'])
+      })
+    })
+  })
+
+  describe('intersection', () => {
+    describe('decode', () => {
+      it('should decode a valid input', () => {
+        const codec = C.intersection(C.type({ a: C.string }), C.type({ b: C.number }))
+        assert.deepStrictEqual(codec.decode({ a: 'a', b: 1 }), right({ a: 'a', b: 1 }))
+      })
+
+      it('should handle primitives', () => {
+        const codec = C.intersection(Int, Positive)
+        assert.deepStrictEqual(codec.decode(1), right(1))
+      })
+
+      it('should reject an invalid input', () => {
+        const codec = C.intersection(C.type({ a: C.string }), C.type({ b: C.number }))
+        assert.deepStrictEqual(
+          codec.decode({ a: 'a' }),
+          left([D.tree('required property "b"', [D.tree('cannot decode undefined, should be number')])])
+        )
+        assert.deepStrictEqual(
+          codec.decode({ b: 1 }),
+          left([D.tree('required property "a"', [D.tree('cannot decode undefined, should be string')])])
+        )
+      })
+    })
+
+    describe('encode', () => {
+      it('should encode a value', () => {
+        const codec = C.intersection(C.type({ a: C.string }), C.type({ b: NumberFromString }))
+        assert.deepStrictEqual(codec.encode({ a: 'a', b: 1 }), { a: 'a', b: '1' })
+      })
+
+      it('should handle primitives', () => {
+        const codec = C.intersection(Int, Positive)
+        assert.deepStrictEqual(codec.encode(1 as any), 1)
+      })
+    })
+  })
+
+  describe('sum', () => {
+    const sum = C.sum('_tag')
+
+    describe('decode', () => {
+      it('should decode a valid input', () => {
+        const A = C.type({ _tag: C.literal('A'), a: C.string })
+        const B = C.type({ _tag: C.literal('B'), b: C.number })
+        const codec = sum({ A, B })
+        assert.deepStrictEqual(codec.decode({ _tag: 'A', a: 'a' }), right({ _tag: 'A', a: 'a' }))
+        assert.deepStrictEqual(codec.decode({ _tag: 'B', b: 1 }), right({ _tag: 'B', b: 1 }))
+      })
+
+      it('should reject an invalid input', () => {
+        const A = C.type({ _tag: C.literal('A'), a: C.string })
+        const B = C.type({ _tag: C.literal('B'), b: C.number })
+        const codec = sum({ A, B })
+        assert.deepStrictEqual(
+          codec.decode(null),
+          left([D.tree('cannot decode null, should be Record<string, unknown>')])
+        )
+        assert.deepStrictEqual(
+          codec.decode({}),
+          left([D.tree('required property "_tag"', [D.tree('cannot decode undefined, should be "A" | "B"')])])
+        )
+        assert.deepStrictEqual(
+          codec.decode({ _tag: 'A', a: 1 }),
+          left([D.tree('required property "a"', [D.tree('cannot decode 1, should be string')])])
+        )
+      })
+
+      it('should handle zero members', () => {
+        assert.deepStrictEqual(C.sum('_tag')({}).decode({}), left([D.tree('cannot decode {}, should be never')]))
+      })
+
+      it('should support empty records', () => {
+        const decoder = sum({})
+        assert.deepStrictEqual(decoder.decode({}), left([D.tree('cannot decode {}, should be never')]))
+      })
+    })
+
+    describe('encode', () => {
+      it('should encode a value', () => {
+        const A = C.type({ _tag: C.literal('A'), a: C.string })
+        const B = C.type({ _tag: C.literal('B'), b: NumberFromString })
+        const codec = sum({ A, B })
+        assert.deepStrictEqual(codec.encode({ _tag: 'A', a: 'a' }), { _tag: 'A', a: 'a' })
+        assert.deepStrictEqual(codec.encode({ _tag: 'B', b: 1 }), { _tag: 'B', b: '1' })
+      })
+    })
+  })
+
+  describe('lazy', () => {
+    interface A {
+      a: number
+      b?: A
+    }
+
+    const codec: C.Codec<A> = C.lazy('A', () =>
+      C.intersection(C.type({ a: NumberFromString }), C.partial({ b: codec }))
+    )
+
+    describe('decode', () => {
+      it('should decode a valid input', () => {
+        assert.deepStrictEqual(codec.decode({ a: '1' }), right({ a: 1 }))
+        assert.deepStrictEqual(codec.decode({ a: '1', b: { a: '2' } }), right({ a: 1, b: { a: 2 } }))
+      })
+
+      it('should reject an invalid input', () => {
+        assert.deepStrictEqual(
+          codec.decode({ a: 1 }),
+          left([D.tree('A', [D.tree('required property "a"', [D.tree('cannot decode 1, should be string')])])])
+        )
+        assert.deepStrictEqual(
+          codec.decode({ a: 'a' }),
+          left([
+            D.tree('A', [
+              D.tree('required property "a"', [D.tree('cannot decode "a", should be parsable into a number')])
+            ])
+          ])
+        )
+        assert.deepStrictEqual(
+          codec.decode({ a: '1', b: {} }),
+          left([
+            D.tree('A', [
+              D.tree('optional property "b"', [
+                D.tree('A', [D.tree('required property "a"', [D.tree('cannot decode undefined, should be string')])])
+              ])
+            ])
+          ])
+        )
+      })
+    })
+
+    describe('encode', () => {
+      it('should encode a value', () => {
+        assert.deepStrictEqual(codec.encode({ a: 1 }), { a: '1' })
+        assert.deepStrictEqual(codec.encode({ a: 1, b: { a: 2 } }), { a: '1', b: { a: '2' } })
+      })
+    })
+  })
+})
diff --git a/test/Decoder.ts b/test/Decoder.ts
new file mode 100644
index 0000000..9471e79
--- /dev/null
+++ b/test/Decoder.ts
@@ -0,0 +1,104 @@
+import * as assert from 'assert'
+import * as E from 'fp-ts/lib/Either'
+import { pipe } from 'fp-ts/lib/pipeable'
+import * as D from '../src/Decoder'
+
+describe('Decoder', () => {
+  describe('decoder', () => {
+    it('map', () => {
+      const decoder = pipe(
+        D.string,
+        D.map((s) => s.length)
+      )
+      assert.deepStrictEqual(decoder.decode('aaa'), E.right(3))
+    })
+
+    it('of', () => {
+      const decoder = D.decoder.of(1)
+      assert.deepStrictEqual(decoder.decode(1), E.right(1))
+      assert.deepStrictEqual(decoder.decode('a'), E.right(1))
+    })
+
+    it('ap', () => {
+      const fab = D.decoder.of((s: string): number => s.length)
+      const fa = D.string
+      assert.deepStrictEqual(pipe(fab, D.ap(fa)).decode('aaa'), E.right(3))
+    })
+
+    it('alt', () => {
+      const decoder = pipe(
+        D.string,
+        D.alt(() => pipe(D.number, D.map(String)))
+      )
+      assert.deepStrictEqual(decoder.decode('a'), E.right('a'))
+      assert.deepStrictEqual(decoder.decode(1), E.right('1'))
+    })
+
+    it('zero', () => {
+      const decoder = D.decoder.zero()
+      assert.deepStrictEqual(decoder.decode(null), E.left([D.tree('cannot decode null, should be never')]))
+    })
+  })
+
+  describe('union', () => {
+    it('should decode a valid input', () => {
+      assert.deepStrictEqual(D.union(D.string).decode('a'), E.right('a'))
+      const decoder = D.union(D.string, D.number)
+      assert.deepStrictEqual(decoder.decode('a'), E.right('a'))
+      assert.deepStrictEqual(decoder.decode(1), E.right(1))
+    })
+
+    it('should reject an invalid input', () => {
+      const decoder = D.union(D.string, D.number)
+      assert.deepStrictEqual(
+        decoder.decode(true),
+        E.left([
+          D.tree('member 0', [D.tree('cannot decode true, should be string')]),
+          D.tree('member 1', [D.tree('cannot decode true, should be number')])
+        ])
+      )
+    })
+
+    it('should handle zero members', () => {
+      assert.deepStrictEqual(D.union().decode({}), E.left([D.tree('cannot decode {}, should be never')]))
+    })
+  })
+
+  describe('intersect', () => {
+    it('should concat strings', () => {
+      assert.deepStrictEqual(D.intersect('a', 'b'), 'b')
+    })
+
+    it('should concat numbers', () => {
+      assert.deepStrictEqual(D.intersect(1, 2), 2)
+    })
+
+    it('should concat booleans', () => {
+      assert.deepStrictEqual(D.intersect(true, false), false)
+    })
+
+    it('should concat nulls', () => {
+      assert.deepStrictEqual(D.intersect(null, null), null)
+    })
+
+    it('should concat undefineds', () => {
+      assert.deepStrictEqual(D.intersect(undefined, undefined), undefined)
+    })
+
+    it('should concat objects', () => {
+      assert.deepStrictEqual(D.intersect({ a: 1 }, { b: 2 }), { a: 1, b: 2 })
+    })
+
+    it('should concat a string with an object', () => {
+      assert.deepStrictEqual(D.intersect('a', { a: 1 }), { 0: 'a', a: 1 })
+    })
+
+    it('should concat a number with an object', () => {
+      assert.deepStrictEqual(D.intersect(1, { a: 1 }), { a: 1 })
+    })
+
+    it('should concat a boolean with an object', () => {
+      assert.deepStrictEqual(D.intersect(true, { a: 1 }), { a: 1 })
+    })
+  })
+})
diff --git a/test/Encoder.ts b/test/Encoder.ts
new file mode 100644
index 0000000..4d1f066
--- /dev/null
+++ b/test/Encoder.ts
@@ -0,0 +1,11 @@
+import * as assert from 'assert'
+import * as E from '../src/Encoder'
+
+describe('Encoder', () => {
+  describe('encoder', () => {
+    it('contramap', () => {
+      const encoder = E.encoder.contramap(E.encoder.number, (s: string) => s.length)
+      assert.deepStrictEqual(encoder.encode('aaa'), 3)
+    })
+  })
+})
diff --git a/test/Guard.ts b/test/Guard.ts
new file mode 100644
index 0000000..1c65cf7
--- /dev/null
+++ b/test/Guard.ts
@@ -0,0 +1,208 @@
+import * as assert from 'assert'
+import * as G from '../src/Guard'
+
+describe('Guard', () => {
+  describe('refinement', () => {
+    it('should accepts valid inputs', () => {
+      const guard = G.refinement(G.string, (s): s is string => s.length > 0)
+      assert.strictEqual(guard.is('a'), true)
+    })
+
+    it('should rejects invalid inputs', () => {
+      const guard = G.refinement(G.string, (s): s is string => s.length > 0)
+      assert.strictEqual(guard.is(undefined), false)
+      assert.strictEqual(guard.is(''), false)
+    })
+  })
+
+  describe('nullable', () => {
+    it('should accepts valid inputs', () => {
+      const guard = G.nullable(G.string)
+      assert.strictEqual(guard.is(null), true)
+      assert.strictEqual(guard.is('a'), true)
+    })
+
+    it('should rejects invalid inputs', () => {
+      const guard = G.nullable(G.string)
+      assert.strictEqual(guard.is(1), false)
+    })
+  })
+
+  describe('type', () => {
+    it('should accepts valid inputs', () => {
+      const guard = G.type({ a: G.string, b: G.number })
+      assert.strictEqual(guard.is({ a: 'a', b: 1 }), true)
+    })
+
+    it('should accepts additional fields', () => {
+      const guard = G.type({ a: G.string, b: G.number })
+      assert.strictEqual(guard.is({ a: 'a', b: 1, c: true }), true)
+    })
+
+    it('should rejects invalid inputs', () => {
+      const guard = G.type({ a: G.string, b: G.number })
+      assert.strictEqual(guard.is(undefined), false)
+      assert.strictEqual(guard.is({ a: 'a' }), false)
+    })
+
+    it('should check missing fields', () => {
+      const undef: G.Guard<undefined> = {
+        is: (u): u is undefined => u === undefined
+      }
+      const guard = G.type({ a: undef })
+      assert.strictEqual(guard.is({}), false)
+    })
+  })
+
+  describe('partial', () => {
+    it('should accepts valid inputs', () => {
+      const guard = G.partial({ a: G.string, b: G.number })
+      assert.strictEqual(guard.is({ a: 'a', b: 1 }), true)
+      assert.strictEqual(guard.is({ a: 'a' }), true)
+      assert.strictEqual(guard.is({ b: 1 }), true)
+      assert.strictEqual(guard.is({}), true)
+    })
+
+    it('should accepts additional fields', () => {
+      const guard = G.partial({ a: G.string, b: G.number })
+      assert.strictEqual(guard.is({ a: 'a', b: 1, c: true }), true)
+    })
+
+    it('should rejects invalid inputs', () => {
+      const guard = G.partial({ a: G.string, b: G.number })
+      assert.strictEqual(guard.is(undefined), false)
+      assert.strictEqual(guard.is({ a: 'a', b: 'b' }), false)
+    })
+  })
+
+  describe('record', () => {
+    it('should accepts valid inputs', () => {
+      const guard = G.record(G.string)
+      assert.strictEqual(guard.is({}), true)
+      assert.strictEqual(guard.is({ a: 'a', b: 'b' }), true)
+    })
+
+    it('should rejects invalid inputs', () => {
+      const guard = G.record(G.string)
+      assert.strictEqual(guard.is(undefined), false)
+      assert.strictEqual(guard.is({ a: 'a', b: 1 }), false)
+    })
+  })
+
+  describe('array', () => {
+    it('should accepts valid inputs', () => {
+      const guard = G.array(G.number)
+      assert.strictEqual(guard.is([]), true)
+      assert.strictEqual(guard.is([1, 2, 3]), true)
+    })
+
+    it('should rejects invalid inputs', () => {
+      const guard = G.array(G.number)
+      assert.strictEqual(guard.is(undefined), false)
+      assert.strictEqual(guard.is(['a']), false)
+    })
+  })
+
+  describe('tuple', () => {
+    it('should accepts valid inputs', () => {
+      const guard = G.tuple(G.string, G.number)
+      assert.strictEqual(guard.is(['a', 1]), true)
+    })
+
+    it('should rejects invalid inputs', () => {
+      const guard = G.tuple(G.string, G.number)
+      assert.strictEqual(guard.is([1, 2]), false)
+    })
+
+    it('should rejects additional fields', () => {
+      const guard = G.tuple(G.string, G.number)
+      assert.strictEqual(guard.is(['a', 1, true]), false)
+    })
+
+    it('should rejects missing fields', () => {
+      const guard = G.tuple(G.string, G.number)
+      assert.strictEqual(guard.is(['a']), false)
+    })
+  })
+
+  describe('intersection', () => {
+    it('should accepts valid inputs', () => {
+      const guard = G.intersection(G.type({ a: G.string }), G.type({ b: G.number }))
+      assert.strictEqual(guard.is({ a: 'a', b: 1 }), true)
+    })
+
+    it('should rejects invalid inputs', () => {
+      const guard = G.intersection(G.type({ a: G.string }), G.type({ b: G.number }))
+      assert.strictEqual(guard.is({ a: 'a' }), false)
+    })
+  })
+
+  describe('union', () => {
+    it('should accepts valid inputs', () => {
+      const guard = G.union(G.string, G.number)
+      assert.strictEqual(guard.is('a'), true)
+      assert.strictEqual(guard.is(1), true)
+    })
+
+    it('should rejects invalid inputs', () => {
+      const guard = G.union(G.string, G.number)
+      assert.strictEqual(guard.is(undefined), false)
+    })
+
+    it('should handle zero members', () => {
+      assert.deepStrictEqual(G.union().is(null), false)
+      assert.deepStrictEqual(G.union().is({}), false)
+    })
+  })
+
+  describe('lazy', () => {
+    interface A {
+      a: number
+      b: Array<A>
+    }
+
+    const guard: G.Guard<A> = G.guard.lazy('A', () =>
+      G.type({
+        a: G.number,
+        b: G.array(guard)
+      })
+    )
+
+    it('should accepts valid inputs', () => {
+      assert.strictEqual(guard.is({ a: 1, b: [] }), true)
+      assert.strictEqual(guard.is({ a: 1, b: [{ a: 2, b: [] }] }), true)
+    })
+
+    it('should rejects invalid inputs', () => {
+      const guard = G.union(G.string, G.number)
+      assert.strictEqual(guard.is(undefined), false)
+    })
+  })
+
+  describe('sum', () => {
+    const sum = G.sum('_tag')
+
+    it('should accepts valid inputs', () => {
+      const guard = sum({
+        A: G.type({ _tag: G.literal('A'), a: G.string }),
+        B: G.type({ _tag: G.literal('B'), b: G.number })
+      })
+      assert.deepStrictEqual(guard.is({ _tag: 'A', a: 'a' }), true)
+      assert.deepStrictEqual(guard.is({ _tag: 'B', b: 1 }), true)
+    })
+
+    it('should rejects invalid inputs', () => {
+      const guard = sum({
+        A: G.type({ _tag: G.literal('A'), a: G.string }),
+        B: G.type({ _tag: G.literal('B'), b: G.number })
+      })
+      assert.strictEqual(guard.is(undefined), false)
+      assert.strictEqual(guard.is({}), false)
+    })
+
+    it('should handle zero members', () => {
+      assert.deepStrictEqual(sum({}).is(null), false)
+      assert.deepStrictEqual(sum({}).is({}), false)
+    })
+  })
+})
diff --git a/test/Schema.ts b/test/Schema.ts
new file mode 100644
index 0000000..6b8f2a6
--- /dev/null
+++ b/test/Schema.ts
@@ -0,0 +1,33 @@
+import * as assert from 'assert'
+import { right } from 'fp-ts/lib/Either'
+import * as D from '../src/Decoder'
+import * as S from '../src/Schema'
+import * as E from '../src/Encoder'
+import * as G from '../src/Guard'
+import * as C from '../src/Codec'
+
+const Person = S.make((S) =>
+  S.type({
+    name: S.string,
+    age: S.number
+  })
+)
+
+describe('Schemable', () => {
+  it('should handle decoders', () => {
+    assert.deepStrictEqual(Person(D.decoder).decode({ name: 'name', age: 46 }), right({ name: 'name', age: 46 }))
+  })
+
+  it('should handle encoders', () => {
+    assert.deepStrictEqual(Person(E.encoder).encode({ name: 'name', age: 46 }), { name: 'name', age: 46 })
+  })
+
+  it('should handle guards', () => {
+    assert.deepStrictEqual(Person(G.guard).is({ name: 'name', age: 46 }), true)
+  })
+
+  it('should handle codecs', () => {
+    assert.deepStrictEqual(Person(C.codec).decode({ name: 'name', age: 46 }), right({ name: 'name', age: 46 }))
+    assert.deepStrictEqual(Person(E.encoder).encode({ name: 'name', age: 46 }), { name: 'name', age: 46 })
+  })
+})
diff --git a/test/Tree.ts b/test/Tree.ts
new file mode 100644
index 0000000..c948360
--- /dev/null
+++ b/test/Tree.ts
@@ -0,0 +1,19 @@
+import * as assert from 'assert'
+import * as E from 'fp-ts/lib/Either'
+import * as D from '../src/Decoder'
+import { draw } from '../src/Tree'
+import { pipe } from 'fp-ts/lib/pipeable'
+
+describe('Tree', () => {
+  it('should draw a tree', () => {
+    const codec = D.type({
+      a: D.string
+    })
+    assert.deepStrictEqual(pipe(codec.decode({ a: 'a' }), E.mapLeft(draw)), E.right({ a: 'a' }))
+    assert.deepStrictEqual(
+      pipe(codec.decode({ a: 1 }), E.mapLeft(draw)),
+      E.left(`required property "a"
+└─ cannot decode 1, should be string`)
+    )
+  })
+})