diff --git a/CHANGELOG.md b/CHANGELOG.md index 99538bb19..dc2d69c17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,12 @@ **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. +# 1.0.0 + +* **Breaking Change** + * upgrade to `fp-ts@1.0.0` + * see https://github.com/gcanti/io-ts/pull/112 (@gcanti) + # 0.9.8 * **New Feature** diff --git a/README.md b/README.md index 4a2284784..afe4c9dca 100644 --- a/README.md +++ b/README.md @@ -4,32 +4,33 @@ # The idea -A value of type `Type` (called "runtime type") is the runtime representation of the static type `A`. +A value of type `Type` (called "runtime type") is the runtime representation of the static type `A`. Also a runtime type can -* decode inputs (through `decode`) -* encode outputs (through `serialize`) +* decode inputs of type `I` (through `decode`) +* encode outputs of type `O` (through `encode`) * be used as a custom type guard (through `is`) ```ts export type mixed = object | number | string | boolean | symbol | undefined | null -class Type { +class Type { readonly _A: A - readonly _S: S + readonly _O: O + readonly _I: I constructor( /** a unique name for this runtime type */ readonly name: string, /** a custom type guard */ readonly is: (v: mixed) => v is A, /** succeeds if a value of type S can be decoded to a value of type A */ - readonly validate: (input: S, context: Context) => Either, + readonly validate: (input: I, context: Context) => Either, /** converts a value of type A to a value of type S */ - readonly serialize: (output: A) => S + readonly encode: (a: A) => O ) {} /** a version of `validate` with a default context */ - decode(s: S): Either + decode(i: I): Either } ``` @@ -43,12 +44,13 @@ A runtime type representing `string` can be defined as ```js import * as t from 'io-ts' -export class StringType extends Type { +export class StringType extends Type { // equivalent to Type as per type parameter defaults + readonly _tag: 'StringType' = 'StringType' constructor() { super( 'string', - (v): v is string => typeof v === 'string', - (s, c) => (this.is(s) ? success(s) : failure(s, c)), + (m): m is string => typeof m === 'string', + (m, c) => (this.is(m) ? success(m) : failure(m, c)), a => a ) } @@ -58,7 +60,7 @@ export class StringType extends Type { A runtime type can be used to validate an object in memory (for example an API payload) ```js -const Person = t.interface({ +const Person = t.type({ name: t.string, age: t.number }) @@ -133,7 +135,43 @@ type IPerson = { } ``` -## Recursive types +# Implemented types / combinators + +```js +import * as t from 'io-ts' +``` + +| Type | TypeScript | Flow | Runtime type / combinator | +| --------------------- | --------------------------------------- | --------------------------------------- | ------------------------------------------------------------ | +| null | `null` | `null` | `t.null` or `t.nullType` | +| undefined | `undefined` | `void` | `t.undefined` | +| string | `string` | `string` | `t.string` | +| number | `number` | `number` | `t.number` | +| boolean | `boolean` | `boolean` | `t.boolean` | +| any | `any` | `any` | `t.any` | +| never | `never` | `empty` | `t.never` | +| object | `object` | ✘ | `t.object` | +| integer | ✘ | ✘ | `t.Integer` | +| array of any | `Array` | `Array` | `t.Array` | +| array of type | `Array` | `Array` | `t.array(A)` | +| dictionary of any | `{ [key: string]: mixed }` | `{ [key: string]: mixed }` | `t.Dictionary` | +| dictionary of type | `{ [K in A]: B }` | `{ [key: A]: B }` | `t.dictionary(A, B)` | +| function | `Function` | `Function` | `t.Function` | +| literal | `'s'` | `'s'` | `t.literal('s')` | +| partial | `Partial<{ name: string }>` | `$Shape<{ name: string }>` | `t.partial({ name: t.string })` | +| readonly | `Readonly` | `ReadOnly` | `t.readonly(T)` | +| readonly array | `ReadonlyArray` | `ReadOnlyArray` | `t.readonlyArray(t.number)` | +| interface | `interface A { name: string }` | `interface A { name: string }` | `t.type({ name: t.string })` or `t.type({ name: t.string })` | +| interface inheritance | `interface B extends A {}` | `interface B extends A {}` | `t.intersection([ A, t.type({}) ])` | +| tuple | `[ A, B ]` | `[ A, B ]` | `t.tuple([ A, B ])` | +| union | `A \| B` | `A \| B` | `t.union([ A, B ])` or `t.taggedUnion(tag, [ A, B ])` | +| intersection | `A & B` | `A & B` | `t.intersection([ A, B ])` | +| keyof | `keyof M` | `$Keys` | `t.keyof(M)` | +| recursive types | see [Recursive types](#recursive-types) | see [Recursive types](#recursive-types) | `t.recursion(name, definition)` | +| refinement | ✘ | ✘ | `t.refinement(A, predicate)` | +| strict/exact types | ✘ | `$Exact<{{ name: t.string }}>` | `t.strict({ name: t.string })` | + +# Recursive types Recursive types can't be inferred by TypeScript so you must provide the static type as a hint @@ -149,48 +187,12 @@ const Category = ICategory > ('Category', self => - t.interface({ + t.type({ name: t.string, categories: t.array(self) })) ``` -# Implemented types / combinators - -```js -import * as t from 'io-ts' -``` - -| Type | TypeScript | Flow | Runtime type / combinator | -| --------------------- | --------------------------------------- | --------------------------------------- | ----------------------------------------------------------------- | -| null | `null` | `null` | `t.null` or `t.nullType` | -| undefined | `undefined` | `void` | `t.undefined` | -| string | `string` | `string` | `t.string` | -| number | `number` | `number` | `t.number` | -| boolean | `boolean` | `boolean` | `t.boolean` | -| any | `any` | `any` | `t.any` | -| never | `never` | `empty` | `t.never` | -| object | `object` | ✘ | `t.object` | -| integer | ✘ | ✘ | `t.Integer` | -| array of any | `Array` | `Array` | `t.Array` | -| array of type | `Array` | `Array` | `t.array(A)` | -| dictionary of any | `{ [key: string]: mixed }` | `{ [key: string]: mixed }` | `t.Dictionary` | -| dictionary of type | `{ [K in A]: B }` | `{ [key: A]: B }` | `t.dictionary(A, B)` | -| function | `Function` | `Function` | `t.Function` | -| literal | `'s'` | `'s'` | `t.literal('s')` | -| partial | `Partial<{ name: string }>` | `$Shape<{ name: string }>` | `t.partial({ name: t.string })` | -| readonly | `Readonly` | `ReadOnly` | `t.readonly(T)` | -| readonly array | `ReadonlyArray` | `ReadOnlyArray` | `t.readonlyArray(t.number)` | -| interface | `interface A { name: string }` | `interface A { name: string }` | `t.interface({ name: t.string })` or `t.type({ name: t.string })` | -| interface inheritance | `interface B extends A {}` | `interface B extends A {}` | `t.intersection([ A, t.interface({}) ])` | -| tuple | `[ A, B ]` | `[ A, B ]` | `t.tuple([ A, B ])` | -| union | `A \| B` | `A \| B` | `t.union([ A, B ])` or `t.taggedUnion(tag, [ A, B ])` | -| intersection | `A & B` | `A & B` | `t.intersection([ A, B ])` | -| keyof | `keyof M` | `$Keys` | `t.keyof(M)` | -| recursive types | see [Recursive types](#recursive-types) | see [Recursive types](#recursive-types) | `t.recursion(name, definition)` | -| refinement | ✘ | ✘ | `t.refinement(A, predicate)` | -| strict/exact types | ✘ | `$Exact<{{ name: t.string }}>` | `t.strict({ name: t.string })` | - # Tagged unions If you are encoding tagged unions, instead of the general purpose `union` combinator, you may want to use the @@ -226,7 +228,7 @@ const Adult = t.refinement(Person, person => person.age >= 18, 'Adult') You can make an interface strict (which means that only the given properties are allowed) using the `strict` combinator ```ts -const Person = t.interface({ +const Person = t.type({ name: t.string, age: t.number }) @@ -242,7 +244,7 @@ StrictPerson.decode({ name: 'Giulio', age: 43, surname: 'Canti' }) // fails Note. You can mix required and optional props using an intersection ```ts -const A = t.interface({ +const A = t.type({ foo: t.string }) @@ -264,13 +266,16 @@ type CT = { You can define a custom combinator to avoid the boilerplate ```ts -export function interfaceWithOptionals( - required: R, - optional: O, +export function interfaceWithOptionals( + required: RequiredProps, + optional: OptionalProps, name?: string ): t.IntersectionType< - [t.InterfaceType>, t.PartialType>], - t.InterfaceOf & t.PartialOf + [ + t.InterfaceType>, + t.PartialType> + ], + t.TypeOfProps & t.TypeOfPartialProps > { return t.intersection([t.interface(required), t.partial(optional)], name) } @@ -286,11 +291,11 @@ You can define your own types. Let's see an example import * as t from 'io-ts' // represents a Date from an ISO string -const DateFromString = new t.Type( +const DateFromString = new t.Type( 'DateFromString', - (v): v is Date => v instanceof Date, - (v, c) => - t.string.validate(v, c).chain(s => { + (m): m is Date => m instanceof Date, + (m, c) => + t.string.validate(m, c).chain(s => { const d = new Date(s) return isNaN(d.getTime()) ? t.failure(s, c) : t.success(d) }), @@ -317,7 +322,10 @@ You can define your own combinators. Let's see some examples An equivalent to `T | null` ```ts -export function maybe(type: RT, name?: string): t.UnionType<[RT, t.NullType], t.TypeOf | null> { +export function maybe( + type: RT, + name?: string +): t.UnionType<[RT, t.NullType], t.TypeOf | null, t.OutputOf | null, t.InputOf | null> { return t.union<[RT, t.NullType]>([type, t.null], name) } ``` @@ -327,29 +335,29 @@ export function maybe(type: RT, name?: string): t.UnionType<[R Extracting the runtime type of a field contained in each member of a union ```ts -const pluck = >, any>>( +const pluck = >>>( union: U, field: F -): t.Type[F]> => { +): t.Type[F]> => { return t.union(union.types.map(type => type.props[field])) } export const Action = t.union([ - t.interface({ + t.type({ type: t.literal('Action1'), - payload: t.interface({ + payload: t.type({ foo: t.string }) }), - t.interface({ + t.type({ type: t.literal('Action2'), - payload: t.interface({ + payload: t.type({ bar: t.string }) }) ]) -// ActionType: t.Type +// ActionType: t.Type<"Action1" | "Action2", "Action1" | "Action2", t.mixed> const ActionType = pluck(Action, 'type') ``` @@ -383,8 +391,8 @@ Due to an upstream [bug](https://github.com/Microsoft/TypeScript/issues/14041), nested interfaces ```ts -const NestedInterface = t.interface({ - foo: t.interface({ +const NestedInterface = t.type({ + foo: t.type({ bar: t.string }) }) diff --git a/flow-tests.js b/flow-tests.js deleted file mode 100644 index 0e60e9989..000000000 --- a/flow-tests.js +++ /dev/null @@ -1,316 +0,0 @@ -// @flow - -import * as t from '.' -import { PathReporter } from './lib/PathReporter' - -// -// Either -// -// $FlowFixMe -;(t.validate('a', t.string).fold(() => 'ko', () => 'ok'): number) -;(t.validate('a', t.string).fold(() => 'ko', () => 'ok'): string) - -// -// refinements -// - -type Integer = t.TypeOf - -const int1: Integer = 1 -// $FlowFixMe -const int2: Integer = 'foo' - -// -// literals -// -const L1 = t.literal(('foo': 'foo')) -type L1T = t.TypeOf -const l1: L1T = 'foo' -// $FlowFixMe -const l2: L1T = 'bar' - -function testLiteralAsTagInUnion() { - // Note that `t.literal(('foo': 'foo'))` does not work for this use case - const Foo = t.type({ - type: (t.literal('foo'): t.Type<*, 'foo'>), - foo: t.string - }) - const Bar = t.type({ - type: (t.literal('bar'): t.Type<*, 'bar'>), - bar: t.number - }) - const FooOrBar = t.union([Foo, Bar]) - type FooOrBarT = t.TypeOf - - // only passes type-checking if tag-based refinement works - function getFoo(x: FooOrBarT): ?string { - if (x.type === 'foo') { - return x.foo - } - } -} - -function testLiteralMismatch() { - const LiteralMismatch = t.type({ - // $FlowFixMe - type: (t.literal('sometag'): t.Type<*, 'differenttag'>) - }) -} - -// -// keyof -// - -const K1 = t.keyof({ a: true, b: true }) -type K1T = t.TypeOf -const k1: K1T = 'a' -const k2: K1T = 'b' -// $FlowFixMe -const k3: K1T = 'c' - -// -// arrays -// -const A1 = t.array(t.number) -type A1T = t.TypeOf -const a1: A1T = [1, 2, 3] -// $FlowFixMe -const a2: A1T = [1, 2, 'foo'] - -// -// interfaces -// - -const Person = t.type({ - name: t.string, - age: t.number -}) - -type PersonT = t.TypeOf - -const person1: PersonT = { - name: 'foo', - age: 43 -} -const person2: PersonT = { - name: 'foo', - // $FlowFixMe - age: 'bar' -} - -// -// partials -// -const P1 = t.partial({ - foo: t.number -}) -type P1T = t.TypeOf -const p1: P1T = {} -const p2: P1T = { foo: 1 } -const p3: P1T = { foo: undefined } -// $FlowFixMe -const p4: P1T = { foo: 'foo' } - -// -// dictionaries -// -const D1 = t.dictionary(t.string, t.number) -type D1T = t.TypeOf -const d1: D1T = {} -const d2: D1T = { a: 1 } -const d3: D1T = { a: 1, b: 2 } -// $FlowFixMe -const d4: D1T = { a: 'foo' } -const D2 = t.dictionary(t.keyof({ a: true }), t.number) -type D2T = t.TypeOf -const d5: D2T = {} -const d6: D2T = { a: 1 } -// $FlowFixMe -const d7: D2T = { a: 1, b: 2 } - -// -// unions -// -const U1 = t.union([t.string, t.number]) -type U1T = t.TypeOf -const u1: U1T = 1 -const u2: U1T = 'foo' -// $FlowFixMe -const u3: U1T = true - -// -// intersections -// -const I1 = t.intersection([t.type({ a: t.string }), t.type({ b: t.number })]) -type I1T = t.TypeOf -const i1: I1T = { a: 'foo', b: 1 } -// $FlowFixMe -const i2: I1T = { a: 'foo' } - -// -// tuples -// -const T1 = t.tuple([t.string, t.number]) -type T1T = t.TypeOf -const t1: T1T = ['foo', 1] -// $FlowFixMe -const t2: T1T = ['foo', true] -// $FlowFixMe -const t3: T1T = ['foo'] -// $FlowFixMe -const t4: T1T = [] - -// -// readonly objects -// -const RO1 = t.readonly(t.type({ a: t.number })) -type RO1T = t.TypeOf -const ro1: RO1T = { a: 1 } -// $FlowFixMe -ro1.a = 2 - -// -// readonly arrays -// -const ROA1 = t.readonlyArray(t.number) -type ROA1T = t.TypeOf -const roa1: ROA1T = [1, 2, 3] -// $FlowFixMe -roa1[0] = 2 - -// -// strict interfaces -// -const S1 = t.strict({ a: t.number }) -type S1T = t.TypeOf -const s1: S1T = { a: 1 } -// $FlowFixMe -const s2: S1T = { a: 1, b: 2 } - -// -// validate -// -const validation = t.validate(1, t.number) -const result: string = validation.fold(() => 'error', () => 'ok') -const report = PathReporter.report(validation) -;(report: Array) - -function optional>(type: RT, name?: string): t.UnionType<[RT, t.UndefinedType], A | void> { - return t.union([type, t.undefined], name) -} - -type GenerableProps = { +[key: string]: Generable } -type GenerableInterface = t.InterfaceType -type GenerableStrict = t.StrictType -type GenerablePartials = t.PartialType -type GenerableDictionary = t.DictionaryType -type GenerableRefinement = t.RefinementType -type GenerableArray = t.ArrayType -type GenerableUnion = t.UnionType, any> -type GenerableIntersection = t.IntersectionType, any> -type GenerableTuple = t.TupleType, any> -type GenerableReadonly = t.ReadonlyType -type GenerableReadonlyArray = t.ReadonlyArrayType -type GenerableRecursive = t.RecursiveType -type Generable = - | t.StringType - | t.NumberType - | t.BooleanType - | GenerableInterface - | GenerableRefinement - | GenerableArray - | GenerableStrict - | GenerablePartials - | GenerableDictionary - | GenerableUnion - | GenerableIntersection - | GenerableTuple - | GenerableReadonly - | GenerableReadonlyArray - | t.LiteralType - | t.KeyofType - | GenerableRecursive - | t.UndefinedType - -function f(generable: Generable): string { - if (generable._tag === 'InterfaceType') { - ;(generable: t.InterfaceType) - } - switch (generable._tag) { - case 'InterfaceType': - const props = generable.props - return Object.keys(props) - .map(k => f(props[k])) - .join('/') - case 'StringType': - return 'StringType' - case 'NumberType': - return 'StringType' - case 'BooleanType': - return 'BooleanType' - case 'RefinementType': - return f(generable.type) - case 'ArrayType': - return 'ArrayType' - case 'StrictType': - return 'StrictType' - case 'PartialType': - return 'PartialType' - case 'DictionaryType': - return 'DictionaryType' - case 'UnionType': - return 'UnionType' - case 'IntersectionType': - return 'IntersectionType' - case 'TupleType': - return generable.types.map(type => f(type)).join('/') - case 'ReadonlyType': - return 'ReadonlyType' - case 'ReadonlyArrayType': - return 'ReadonlyArrayType' - case 'LiteralType': - return 'LiteralType' - case 'KeyofType': - return 'KeyofType' - case 'RecursiveType': - return f(generable.type) - case 'UndefinedType': - return 'UndefinedType' - } - throw new Error('Impossible') -} - -const schema = t.type({ - a: t.string, - b: t.union([ - t.partial({ - c: t.string, - d: t.literal('eee') - }), - t.boolean - ]), - e: t.intersection([ - t.type({ - f: t.array(t.string) - }), - t.type({ - g: t.union([t.literal('toto'), t.literal('tata')]) - }) - ]) -}) - -f(schema) // OK! - -type RecT = { - a: number, - b: RecT | void -} - -const Rec = t.recursion('T', self => - t.type({ - a: t.number, - b: t.union([self, t.undefined]) - }) -) - -f(Rec) // OK! diff --git a/package-lock.json b/package-lock.json index 818582937..353d88b21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "io-ts", - "version": "0.9.7", + "version": "1.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -434,9 +434,9 @@ } }, "fp-ts": { - "version": "0.6.8", - "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-0.6.8.tgz", - "integrity": "sha512-T//qYN1EcKrFiyp9MjkElsNzTf40E7tLen7bYKIVWTFhpV7TeH2xPEETm+JidhWMCgbUbmzU6FvAdTqZmIKGGg==" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-1.0.0.tgz", + "integrity": "sha512-eXq3sOoJNS4aQJFSi8LWoz9TSLAdm9+TZqnj1aA55AC4fSltlGeOorJ3zQiQWcWFea3DdTXSEoUuAp97ObCI2Q==" }, "fs.realpath": { "version": "1.0.0", diff --git a/package.json b/package.json index 7a7819199..7d3d272c1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "io-ts", - "version": "0.9.8", + "version": "1.0.0", "description": "TypeScript compatible runtime type system for IO validation", "files": ["lib"], "main": "lib/index.js", @@ -9,18 +9,14 @@ "lint": "tslint src/**/*.ts test/**/*.ts", "typings-checker": "typings-checker --allow-expect-error --project typings-checker/tsconfig.json typings-checker/index.ts", - "mocha": "mocha -r ts-node/register test/*.ts", + "mocha": "TS_NODE_CACHE=false mocha -r ts-node/register test/*.ts", "prettier": "prettier --no-semi --single-quote --print-width 120 --parser typescript --list-different \"{src,test}/**/*.ts\"", "fix-prettier": "prettier --no-semi --single-quote --print-width 120 --parser typescript --write \"{src,test,examples,exercises}/**/*.ts\"", - "flow-copy-definition-files": "cp src/*.js.flow lib", - "flow-test": "flow status", - "flow-fix-prettier": - "prettier --no-semi --single-quote --print-width 120 --parser flow --write \"{src,test,examples,exercises}/**/*.js.flow\"", "test": "npm run prettier && npm run lint && npm run typings-checker && npm run mocha", "clean": "rm -rf lib/*", - "build": "npm run clean && tsc && npm run flow-copy-definition-files", + "build": "npm run clean && tsc", "perf": "node perf/index" }, "repository": { @@ -34,7 +30,7 @@ }, "homepage": "https://github.com/gcanti/io-ts", "dependencies": { - "fp-ts": "^0.6.4" + "fp-ts": "^1.0.0" }, "devDependencies": { "@types/benchmark": "1.0.31", diff --git a/src/PathReporter.js.flow b/src/PathReporter.js.flow deleted file mode 100644 index d709b2701..000000000 --- a/src/PathReporter.js.flow +++ /dev/null @@ -1,9 +0,0 @@ -// @flow -import type { Reporter } from './Reporter' -import type { ValidationError } from './index' - -declare export function failure(es: Array): Array - -declare export function success(): Array - -declare export var PathReporter: Reporter> diff --git a/src/Reporter.js.flow b/src/Reporter.js.flow deleted file mode 100644 index e052b7e71..000000000 --- a/src/Reporter.js.flow +++ /dev/null @@ -1,6 +0,0 @@ -// @flow -import type { Validation } from './index' - -export interface Reporter { - report: (validation: Validation) => A; -} diff --git a/src/ThrowReporter.js.flow b/src/ThrowReporter.js.flow deleted file mode 100644 index 96ad30d88..000000000 --- a/src/ThrowReporter.js.flow +++ /dev/null @@ -1,4 +0,0 @@ -// @flow -import type { Reporter } from './Reporter' - -declare export var ThrowReporter: Reporter diff --git a/src/index.js.flow b/src/index.js.flow deleted file mode 100644 index e3b27fcbc..000000000 --- a/src/index.js.flow +++ /dev/null @@ -1,576 +0,0 @@ -// @flow -import type { Either } from 'fp-ts/lib/Either' -import type { Predicate } from 'fp-ts/lib/function' - -export interface ContextEntry { - +key: string; - +type: Decoder; -} -export type Context = Array -export interface ValidationError { - +value: mixed; - +context: Context; -} -export type Errors = Array -export type Validation = Either -export type Is = (v: mixed) => boolean -export type Validate = (s: S, context: Context) => Validation -export type Serialize = (a: A) => S -export type Any = Type -type ExtractType> = A -export type TypeOf> = ExtractType<*, *, RT> -type ExtractInput> = S -export type InputOf> = ExtractInput<*, *, RT> - -export interface Decoder { - +name: string; - +validate: Validate; -} - -export interface Encoder { - +serialize: Serialize; -} - -/** - * Laws: - * 1. validate(x).fold(() => x, serialize) = x - * 2. validate(serialize(x)) = Right(x) - */ -declare export class Type { - +name: string; - +is: Is; - +validate: Validate; - +serialize: Serialize; - constructor(name: string, is: Is, validate: Validate, serialize: Serialize): Type; - pipe(ab: Type, name?: string): Type; - asDecoder(): Decoder; - asEncoder(): Encoder; -} - -// -// basic types -// - -declare export class NullType extends Type { - +_tag: 'NullType'; -} - -// TODO alias -declare export var nullType: NullType - -declare export class UndefinedType extends Type { - +_tag: 'UndefinedType'; -} - -declare export var undefined: UndefinedType - -declare export class AnyType extends Type { - +_tag: 'AnyType'; -} - -declare export var any: AnyType - -declare export class NeverType extends Type { - +_tag: 'NeverType'; -} - -declare export var never: NeverType - -declare export class StringType extends Type { - +_tag: 'StringType'; -} - -declare export var string: StringType - -declare export class NumberType extends Type { - +_tag: 'NumberType'; -} - -declare export var number: NumberType - -declare export class BooleanType extends Type { - +_tag: 'BooleanType'; -} - -declare export var boolean: BooleanType - -declare export class AnyArrayType extends Type> { - +_tag: 'AnyArrayType'; -} - -declare export var Array: AnyArrayType - -declare export class AnyDictionaryType extends Type { - +_tag: 'AnyDictionaryType'; -} - -declare export var Dictionary: AnyDictionaryType - -declare export class FunctionType extends Type { - +_tag: 'FunctionType'; -} - -declare export var Function: FunctionType - -// -// refinements -// - -declare export class RefinementType<+RT: Type, S, A> extends Type { - +_tag: 'RefinementType'; - +type: RT; - +predicate: Predicate; - constructor( - name: string, - is: Is, - validate: Validate, - serialize: Serialize, - type: RT, - predicate: Predicate - ): RefinementType; -} - -declare export var refinement: >( - type: RT, - predicate: Predicate>, - name?: string -) => RefinementType, TypeOf> - -declare export var Integer: RefinementType - -// -// literals -// - -declare export class LiteralType extends Type { - +_tag: 'LiteralType'; - +value: V; - constructor( - name: string, - is: Is, - validate: Validate, - serialize: Serialize, - value: V - ): LiteralType; -} - -declare export var literal: (value: V, name?: string) => LiteralType - -// -// keyof -// - -declare export class KeyofType extends Type> { - +_tag: 'KeyofType'; - +keys: D; - constructor( - name: string, - is: Is<$Keys>, - validate: Validate>, - serialize: Serialize>, - keys: D - ): KeyofType; -} - -declare export var keyof: (keys: D, name?: string) => KeyofType - -// -// recursive types -// - -declare export class RecursiveType<+RT: Any, A> extends Type { - +_tag: 'RecursiveType'; - +type: RT; - constructor( - name: string, - is: Is, - validate: Validate, - serialize: Serialize - ): RecursiveType; -} - -declare export var recursion: (name: string, definition: (self: RT) => RT) => RecursiveType - -// -// arrays -// - -declare export class ArrayType<+RT: Any, A> extends Type { - +_tag: 'ArrayType'; - +type: RT; - constructor( - name: string, - is: Is, - validate: Validate, - serialize: Serialize, - type: RT - ): ArrayType; -} - -declare export var array: (type: RT, name?: string) => ArrayType>> - -// -// interfaces -// - -export type Props = { +[key: string]: Any } - -export type InterfaceOf = $ObjMap(v: Type) => T> - -declare export class InterfaceType extends Type { - +_tag: 'InterfaceType'; - +props: P; - constructor( - name: string, - is: Is, - validate: Validate, - serialize: Serialize, - props: P - ): InterfaceType; -} - -declare export var type: (props: P, name?: string) => InterfaceType> - -// -// partials -// - -export type PartialOf = $Shape> - -declare export class PartialType extends Type { - +_tag: 'PartialType'; - +props: P; - constructor( - name: string, - is: Is, - validate: Validate, - serialize: Serialize, - props: P - ): PartialType; -} - -declare export var partial: (props: P, name?: string) => PartialType> - -// -// dictionaries -// - -declare export class DictionaryType<+D: Any, +C: Any, A> extends Type { - +_tag: 'DictionaryType'; - +domain: D; - +codomain: C; - constructor( - name: string, - is: Is, - validate: Validate, - serialize: Serialize, - domain: D, - codomain: C - ): DictionaryType; -} - -declare export var dictionary: ( - domain: D, - codomain: C, - name?: string -) => DictionaryType]: TypeOf }> - -// -// unions -// - -declare export class UnionType<+RTS, A> extends Type { - +_tag: 'UnionType'; - +types: RTS; - constructor( - name: string, - is: Is, - validate: Validate, - serialize: Serialize, - types: RTS - ): UnionType; -} - -declare export function union< - A, - B, - C, - D, - E, - F, - G, - H, - I, - J, - RTS: [ - Type, - Type, - Type, - Type, - Type, - Type, - Type, - Type, - Type, - Type - ] ->( - types: RTS, - name?: string -): UnionType -declare export function union< - A, - B, - C, - D, - E, - F, - G, - H, - I, - RTS: [ - Type, - Type, - Type, - Type, - Type, - Type, - Type, - Type, - Type - ] ->( - types: RTS, - name?: string -): UnionType -declare export function union< - A, - B, - C, - D, - E, - F, - G, - H, - RTS: [ - Type, - Type, - Type, - Type, - Type, - Type, - Type, - Type - ] ->( - types: RTS, - name?: string -): UnionType -declare export function union< - A, - B, - C, - D, - E, - F, - G, - RTS: [Type, Type, Type, Type, Type, Type, Type] ->( - types: RTS, - name?: string -): UnionType -declare export function union< - A, - B, - C, - D, - E, - F, - RTS: [Type, Type, Type, Type, Type, Type] ->( - types: RTS, - name?: string -): UnionType -declare export function union< - A, - B, - C, - D, - E, - RTS: [Type, Type, Type, Type, Type] ->( - types: RTS, - name?: string -): UnionType -declare export function union, Type, Type, Type]>( - types: RTS, - name?: string -): UnionType -declare export function union, Type, Type]>( - types: RTS, - name?: string -): UnionType -declare export function union, Type]>( - types: RTS, - name?: string -): UnionType -declare export function union]>(types: RTS, name?: string): UnionType - -// -// intersections -// - -declare export class IntersectionType<+RTS, A> extends Type { - +_tag: 'IntersectionType'; - +types: RTS; - constructor( - name: string, - is: Is, - validate: Validate, - serialize: Serialize, - types: RTS - ): IntersectionType; -} - -declare export function intersection< - A, - B, - C, - D, - E, - RTS: [Type, Type, Type, Type, Type] ->( - types: RTS, - name?: string -): UnionType -declare export function intersection, Type, Type, Type]>( - types: RTS, - name?: string -): UnionType -declare export function intersection, Type, Type]>( - types: RTS, - name?: string -): UnionType -declare export function intersection, Type]>( - types: RTS, - name?: string -): UnionType -declare export function intersection]>(types: RTS, name?: string): UnionType - -// -// tuples -// - -declare export class TupleType<+RTS, A> extends Type { - +_tag: 'TupleType'; - +types: RTS; - constructor( - name: string, - is: Is, - validate: Validate, - serialize: Serialize, - types: RTS - ): TupleType; -} - -declare export function tuple< - A, - B, - C, - D, - E, - RTS: [Type, Type, Type, Type, Type] ->( - types: RTS, - name?: string -): UnionType -declare export function tuple, Type, Type, Type]>( - types: RTS, - name?: string -): UnionType -declare export function tuple, Type, Type]>( - types: RTS, - name?: string -): UnionType -declare export function tuple, Type]>( - types: RTS, - name?: string -): UnionType -declare export function tuple]>(types: RTS, name?: string): UnionType - -// -// readonly objects -// - -declare export class ReadonlyType<+RT: Any, A> extends Type { - +_tag: 'ReadonlyType'; - +type: RT; - constructor( - name: string, - is: Is, - validate: Validate, - serialize: Serialize, - type: RT - ): ReadonlyType; -} - -declare export var readonly: (type: RT, name?: string) => ReadonlyType>> - -// -// readonly arrays -// - -declare export class ReadonlyArrayType<+RT: Any, A> extends Type { - +_tag: 'ReadonlyArrayType'; - +type: RT; - constructor( - name: string, - is: Is, - validate: Validate, - serialize: Serialize, - type: RT - ): ReadonlyArrayType; -} - -declare export var readonlyArray: ( - type: RT, - name?: string -) => ReadonlyArrayType>> - -// -// strict interfaces -// - -declare export class StrictType extends Type { - +_tag: 'StrictType'; - +props: P; - constructor( - name: string, - is: Is, - validate: Validate, - serialize: Serialize, - props: P - ): StrictType

; -} - -declare export var strict: (props: P, name?: string) => StrictType>> - -declare export var identity: (a: A) => A - -declare export var getFunctionName: (f: Function) => string - -declare export var getContextEntry: (key: string, type: Decoder) => ContextEntry - -declare export var getValidationError: (value: mixed, context: Context) => ValidationError - -declare export var getDefaultContext: (type: Decoder) => Context - -declare export var appendContext: (c: Context, key: string, type: Decoder) => Context - -declare export var failures: (errors: Errors) => Validation - -declare export var failure: (value: mixed, context: Context) => Validation - -declare export var success: (value: T) => Validation - -declare export var validate: (value: S, type: Decoder) => Validation diff --git a/src/index.ts b/src/index.ts index d6d105e4d..a489278ab 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,21 +21,24 @@ export interface ValidationError { } export type Errors = Array export type Validation = Either -export type Is = (v: mixed) => v is A -export type Validate = (s: S, context: Context) => Validation -export type Serialize = (a: A) => S -export type Any = Type -export type TypeOf> = RT['_A'] -export type InputOf> = RT['_S'] - -export interface Decoder { +export type Is = (m: mixed) => m is A +export type Validate = (i: I, context: Context) => Validation +export type Decode = (i: I) => Validation +export type Encode = (a: A) => O +export type Any = Type +export type Mixed = Type +export type TypeOf = RT['_A'] +export type InputOf = RT['_I'] +export type OutputOf = RT['_O'] + +export interface Decoder { readonly name: string - readonly validate: Validate - readonly decode: (s: S) => Validation + readonly validate: Validate + readonly decode: Decode } -export interface Encoder { - readonly serialize: Serialize +export interface Encoder { + readonly encode: Encode } /** @@ -45,40 +48,40 @@ export interface Encoder { * * where `T` is a runtime type */ -export class Type implements Decoder, Encoder { +export class Type implements Decoder, Encoder { // prettier-ignore readonly '_A': A // prettier-ignore - readonly '_S': S + readonly '_O': O + // prettier-ignore + readonly '_I': I constructor( /** a unique name for this runtime type */ readonly name: string, /** a custom type guard */ readonly is: Is, - /** succeeds if a value of type S can be decoded to a value of type A */ - readonly validate: Validate, - /** converts a value of type A to a value of type S */ - readonly serialize: Serialize + /** succeeds if a value of type I can be decoded to a value of type A */ + readonly validate: Validate, + /** converts a value of type A to a value of type O */ + readonly encode: Encode ) {} - pipe(ab: Type, name?: string): Type { + pipe(ab: Type, name?: string): Type { return new Type( name || `pipe(${this.name}, ${ab.name})`, ab.is, - (s, c) => this.validate(s, c).chain(a => ab.validate(a, c)), - this.serialize === identity && ab.serialize === identity - ? (identity as any) - : b => this.serialize(ab.serialize(b)) + (i, c) => this.validate(i, c).chain(a => ab.validate(a, c)), + this.encode === identity && ab.encode === identity ? (identity as any) : b => this.encode(ab.encode(b)) ) } - asDecoder(): Decoder { + asDecoder(): Decoder { return this } - asEncoder(): Encoder { + asEncoder(): Encoder { return this } /** a version of `validate` with a default context */ - decode(s: S): Validation { - return this.validate(s, getDefaultContext(this)) + decode(i: I): Validation { + return this.validate(i, getDefaultContext(this)) } } @@ -110,33 +113,29 @@ export const failure = (value: mixed, context: Context): Validation => export const success = (value: T): Validation => new Right(value) -/** @deprecated use `type.decode(value)` instead */ -export const validate = (value: S, type: Decoder): Validation => - type.validate(value, getDefaultContext(type)) - const pushAll = (xs: Array, ys: Array): void => Array.prototype.push.apply(xs, ys) // // basic types // -export class NullType extends Type { +export class NullType extends Type { readonly _tag: 'NullType' = 'NullType' constructor() { - super('null', (v): v is null => v === null, (s, c) => (this.is(s) ? success(s) : failure(s, c)), identity) + super('null', (m): m is null => m === null, (m, c) => (this.is(m) ? success(m) : failure(m, c)), identity) } } /** @alias `null` */ export const nullType: NullType = new NullType() -export class UndefinedType extends Type { +export class UndefinedType extends Type { readonly _tag: 'UndefinedType' = 'UndefinedType' constructor() { super( 'undefined', - (v): v is undefined => v === void 0, - (s, c) => (this.is(s) ? success(s) : failure(s, c)), + (m): m is undefined => m === void 0, + (m, c) => (this.is(m) ? success(m) : failure(m, c)), identity ) } @@ -144,7 +143,7 @@ export class UndefinedType extends Type { const undefinedType: UndefinedType = new UndefinedType() -export class AnyType extends Type { +export class AnyType extends Type { readonly _tag: 'AnyType' = 'AnyType' constructor() { super('any', (_): _ is any => true, success, identity) @@ -153,13 +152,13 @@ export class AnyType extends Type { export const any: AnyType = new AnyType() -export class NeverType extends Type { +export class NeverType extends Type { readonly _tag: 'NeverType' = 'NeverType' constructor() { super( 'never', (_): _ is never => false, - (s, c) => failure(s, c), + (m, c) => failure(m, c), () => { throw new Error('cannot serialize never') } @@ -169,13 +168,13 @@ export class NeverType extends Type { export const never: NeverType = new NeverType() -export class StringType extends Type { +export class StringType extends Type { readonly _tag: 'StringType' = 'StringType' constructor() { super( 'string', - (v): v is string => typeof v === 'string', - (s, c) => (this.is(s) ? success(s) : failure(s, c)), + (m): m is string => typeof m === 'string', + (m, c) => (this.is(m) ? success(m) : failure(m, c)), identity ) } @@ -183,13 +182,13 @@ export class StringType extends Type { export const string: StringType = new StringType() -export class NumberType extends Type { +export class NumberType extends Type { readonly _tag: 'NumberType' = 'NumberType' constructor() { super( 'number', - (v): v is number => typeof v === 'number', - (s, c) => (this.is(s) ? success(s) : failure(s, c)), + (m): m is number => typeof m === 'number', + (m, c) => (this.is(m) ? success(m) : failure(m, c)), identity ) } @@ -197,13 +196,13 @@ export class NumberType extends Type { export const number: NumberType = new NumberType() -export class BooleanType extends Type { +export class BooleanType extends Type { readonly _tag: 'BooleanType' = 'BooleanType' constructor() { super( 'boolean', - (v): v is boolean => typeof v === 'boolean', - (s, c) => (this.is(s) ? success(s) : failure(s, c)), + (m): m is boolean => typeof m === 'boolean', + (m, c) => (this.is(m) ? success(m) : failure(m, c)), identity ) } @@ -211,22 +210,22 @@ export class BooleanType extends Type { export const boolean: BooleanType = new BooleanType() -export class AnyArrayType extends Type> { +export class AnyArrayType extends Type> { readonly _tag: 'AnyArrayType' = 'AnyArrayType' constructor() { - super('Array', Array.isArray, (s, c) => (this.is(s) ? success(s) : failure(s, c)), identity) + super('Array', Array.isArray, (m, c) => (this.is(m) ? success(m) : failure(m, c)), identity) } } const arrayType: AnyArrayType = new AnyArrayType() -export class AnyDictionaryType extends Type { +export class AnyDictionaryType extends Type<{ [key: string]: mixed }> { readonly _tag: 'AnyDictionaryType' = 'AnyDictionaryType' constructor() { super( 'Dictionary', - (v): v is { [key: string]: mixed } => v !== null && typeof v === 'object', - (s, c) => (this.is(s) ? success(s) : failure(s, c)), + (m): m is { [key: string]: mixed } => m !== null && typeof m === 'object', + (m, c) => (this.is(m) ? success(m) : failure(m, c)), identity ) } @@ -234,7 +233,7 @@ export class AnyDictionaryType extends Type { export const Dictionary: AnyDictionaryType = new AnyDictionaryType() -export class ObjectType extends Type { +export class ObjectType extends Type { readonly _tag: 'ObjectType' = 'ObjectType' constructor() { super('object', Dictionary.is, Dictionary.validate, identity) @@ -243,13 +242,13 @@ export class ObjectType extends Type { export const object: ObjectType = new ObjectType() -export class FunctionType extends Type { +export class FunctionType extends Type { readonly _tag: 'FunctionType' = 'FunctionType' constructor() { super( 'Function', - (v): v is Function => typeof v === 'function', - (s, c) => (this.is(s) ? success(s) : failure(s, c)), + (m): m is Function => typeof m === 'function', + (m, c) => (this.is(m) ? success(m) : failure(m, c)), identity ) } @@ -261,13 +260,13 @@ export const Function: FunctionType = new FunctionType() // refinements // -export class RefinementType, S, A> extends Type { +export class RefinementType extends Type { readonly _tag: 'RefinementType' = 'RefinementType' constructor( name: string, - is: RefinementType['is'], - validate: RefinementType['validate'], - serialize: RefinementType['serialize'], + is: RefinementType['is'], + validate: RefinementType['validate'], + serialize: RefinementType['encode'], readonly type: RT, readonly predicate: Predicate ) { @@ -275,16 +274,16 @@ export class RefinementType, S, A> extends Type } } -export const refinement = >( +export const refinement = ( type: RT, predicate: Predicate>, name: string = `(${type.name} | ${getFunctionName(predicate)})` -): RefinementType, TypeOf> => +): RefinementType, OutputOf, InputOf> => new RefinementType( name, - (v): v is TypeOf => type.is(v) && predicate(v), - (s, c) => type.validate(s, c).chain(a => (predicate(a) ? success(a) : failure(a, c))), - type.serialize, + (m): m is TypeOf => type.is(m) && predicate(m), + (i, c) => type.validate(i, c).chain(a => (predicate(a) ? success(a) : failure(a, c))), + type.encode, type, predicate ) @@ -295,13 +294,13 @@ export const Integer = refinement(number, n => n % 1 === 0, 'Integer') // literals // -export class LiteralType extends Type { +export class LiteralType extends Type { readonly _tag: 'LiteralType' = 'LiteralType' constructor( name: string, is: LiteralType['is'], validate: LiteralType['validate'], - serialize: LiteralType['serialize'], + serialize: LiteralType['encode'], readonly value: V ) { super(name, is, validate, serialize) @@ -312,46 +311,46 @@ export const literal = ( value: V, name: string = JSON.stringify(value) ): LiteralType => { - const is = (v: mixed): v is V => v === value - return new LiteralType(name, is, (s, c) => (is(s) ? success(value) : failure(s, c)), identity, value) + const is = (m: mixed): m is V => m === value + return new LiteralType(name, is, (m, c) => (is(m) ? success(value) : failure(m, c)), identity, value) } // // keyof // -export class KeyofType extends Type { +export class KeyofType extends Type { readonly _tag: 'KeyofType' = 'KeyofType' constructor( name: string, is: KeyofType['is'], validate: KeyofType['validate'], - serialize: KeyofType['serialize'], + serialize: KeyofType['encode'], readonly keys: D ) { super(name, is, validate, serialize) } } -export const keyof = ( +export const keyof = ( keys: D, name: string = `(keyof ${JSON.stringify(Object.keys(keys))})` ): KeyofType => { - const is = (v: mixed): v is keyof D => string.is(v) && keys.hasOwnProperty(v) - return new KeyofType(name, is, (s, c) => (is(s) ? success(s) : failure(s, c)), identity, keys) + const is = (m: mixed): m is keyof D => string.is(m) && keys.hasOwnProperty(m) + return new KeyofType(name, is, (m, c) => (is(m) ? success(m) : failure(m, c)), identity, keys) } // // recursive types // -export class RecursiveType extends Type { +export class RecursiveType extends Type { readonly _tag: 'RecursiveType' = 'RecursiveType' constructor( name: string, - is: RecursiveType['is'], - validate: RecursiveType['validate'], - serialize: RecursiveType['serialize'], + is: RecursiveType['is'], + validate: RecursiveType['validate'], + serialize: RecursiveType['encode'], private runDefinition: () => RT ) { super(name, is, validate, serialize) @@ -361,10 +360,10 @@ export class RecursiveType extends Type { } } -export const recursion = = Type>( +export const recursion = = Type>( name: string, definition: (self: RT) => RT -): RecursiveType => { +): RecursiveType => { let cache: RT const runDefinition = (): RT => { if (!cache) { @@ -372,11 +371,11 @@ export const recursion = = Type>( } return cache } - const Self: any = new RecursiveType( + const Self: any = new RecursiveType( name, (m): m is A => runDefinition().is(m), (m, c) => runDefinition().validate(m, c), - a => runDefinition().serialize(a), + a => runDefinition().encode(a), runDefinition ) return Self @@ -386,28 +385,28 @@ export const recursion = = Type>( // arrays // -export class ArrayType extends Type { +export class ArrayType extends Type { readonly _tag: 'ArrayType' = 'ArrayType' constructor( name: string, - is: ArrayType['is'], - validate: ArrayType['validate'], - serialize: ArrayType['serialize'], + is: ArrayType['is'], + validate: ArrayType['validate'], + serialize: ArrayType['encode'], readonly type: RT ) { super(name, is, validate, serialize) } } -export const array = ( +export const array = ( type: RT, name: string = `Array<${type.name}>` -): ArrayType>> => +): ArrayType>, Array>, mixed> => new ArrayType( name, - (v): v is Array> => arrayType.is(v) && v.every(type.is), - (s, c) => - arrayType.validate(s, c).chain(xs => { + (m): m is Array> => arrayType.is(m) && m.every(type.is), + (m, c) => + arrayType.validate(m, c).chain(xs => { const len = xs.length let a: Array> = xs const errors: Errors = [] @@ -428,7 +427,7 @@ export const array = ( } return errors.length ? failures(errors) : success(a) }), - type.serialize === identity ? identity : a => a.map(type.serialize), + type.encode === identity ? identity : a => a.map(type.encode), type ) @@ -436,23 +435,23 @@ export const array = ( // interfaces // -export type Props = { [key: string]: Any } - -export type InterfaceOf

= { [K in keyof P]: TypeOf } - -export class InterfaceType

extends Type { +export class InterfaceType

extends Type { readonly _tag: 'InterfaceType' = 'InterfaceType' constructor( name: string, - is: InterfaceType['is'], - validate: InterfaceType['validate'], - serialize: InterfaceType['serialize'], + is: InterfaceType['is'], + validate: InterfaceType['validate'], + serialize: InterfaceType['encode'], readonly props: P ) { super(name, is, validate, serialize) } } +export interface AnyProps { + [key: string]: Any +} + const getNameFromProps = (props: Props): string => `{ ${Object.keys(props) .map(k => `${k}: ${props[k].name}`) @@ -460,33 +459,41 @@ const getNameFromProps = (props: Props): string => const useIdentity = (props: Props): boolean => { for (let k in props) { - if (props[k].serialize !== identity) { + if (props[k].encode !== identity) { return false } } return true } +export type TypeOfProps

= { [K in keyof P]: TypeOf } + +export type OutputOfProps

= { [K in keyof P]: OutputOf } + +export interface Props { + [key: string]: Mixed +} + /** @alias `interface` */ export const type =

( props: P, name: string = getNameFromProps(props) -): InterfaceType> => +): InterfaceType }, { [K in keyof P]: OutputOf }, mixed> => new InterfaceType( name, - (v): v is InterfaceOf

=> { - if (!Dictionary.is(v)) { + (m): m is TypeOfProps

=> { + if (!Dictionary.is(m)) { return false } for (let k in props) { - if (!props[k].is(v[k])) { + if (!props[k].is(m[k])) { return false } } return true }, - (s, c) => - Dictionary.validate(s, c).chain(o => { + (m, c) => + Dictionary.validate(m, c).chain(o => { let a = o const errors: Errors = [] for (let k in props) { @@ -512,9 +519,9 @@ export const type =

( : a => { const s: { [x: string]: any } = { ...(a as any) } for (let k in props) { - s[k] = props[k].serialize(a[k]) + s[k] = props[k].encode(a[k]) } - return s + return s as any }, props ) @@ -523,25 +530,27 @@ export const type =

( // partials // -export type PartialOf

= { [K in keyof P]?: TypeOf } - -export class PartialType

extends Type { +export class PartialType

extends Type { readonly _tag: 'PartialType' = 'PartialType' constructor( name: string, - is: PartialType['is'], - validate: PartialType['validate'], - serialize: PartialType['serialize'], + is: PartialType['is'], + validate: PartialType['validate'], + serialize: PartialType['encode'], readonly props: P ) { super(name, is, validate, serialize) } } +export type TypeOfPartialProps

= { [K in keyof P]?: TypeOf } + +export type InputOfPartialProps

= { [K in keyof P]?: OutputOf } + export const partial =

( props: P, name: string = `PartialType<${getNameFromProps(props)}>` -): PartialType> => { +): PartialType }, { [K in keyof P]?: OutputOf }, mixed> => { const partials: Props = {} for (let k in props) { partials[k] = union([props[k], undefinedType]) @@ -558,10 +567,10 @@ export const partial =

( for (let k in props) { const ak = a[k] if (ak !== undefined) { - s[k] = props[k].serialize(ak) + s[k] = props[k].encode(ak) } } - return s + return s as any }, props ) @@ -571,13 +580,13 @@ export const partial =

( // dictionaries // -export class DictionaryType extends Type { +export class DictionaryType extends Type { readonly _tag: 'DictionaryType' = 'DictionaryType' constructor( name: string, - is: DictionaryType['is'], - validate: DictionaryType['validate'], - serialize: DictionaryType['serialize'], + is: DictionaryType['is'], + validate: DictionaryType['validate'], + serialize: DictionaryType['encode'], readonly domain: D, readonly codomain: C ) { @@ -585,17 +594,21 @@ export class DictionaryType extends Type( +export type TypeOfDictionary = { [K in TypeOf]: TypeOf } + +export type OutputOfDictionary = { [K in OutputOf]: OutputOf } + +export const dictionary = ( domain: D, codomain: C, name: string = `{ [K in ${domain.name}]: ${codomain.name} }` -): DictionaryType]: TypeOf }> => +): DictionaryType]: TypeOf }, { [K in OutputOf]: OutputOf }, mixed> => new DictionaryType( name, - (v): v is { [K in TypeOf]: TypeOf } => - Dictionary.is(v) && Object.keys(v).every(k => domain.is(k) && codomain.is(v[k])), - (s, c) => - Dictionary.validate(s, c).chain(o => { + (m): m is TypeOfDictionary => + Dictionary.is(m) && Object.keys(m).every(k => domain.is(k) && codomain.is(m[k])), + (m, c) => + Dictionary.validate(m, c).chain(o => { const a: { [key: string]: any } = {} const errors: Errors = [] let changed = false @@ -620,14 +633,14 @@ export const dictionary = ( } return errors.length ? failures(errors) : success((changed ? a : o) as any) }), - domain.serialize === identity && codomain.serialize === identity + domain.encode === identity && codomain.encode === identity ? identity : a => { const s: { [key: string]: any } = {} for (let k in a) { - s[String(domain.serialize(k))] = codomain.serialize(a[k]) + s[String(domain.encode(k))] = codomain.encode(a[k]) } - return s + return s as any }, domain, codomain @@ -637,32 +650,32 @@ export const dictionary = ( // unions // -export class UnionType, A> extends Type { +export class UnionType, A = any, O = A, I = mixed> extends Type { readonly _tag: 'UnionType' = 'UnionType' constructor( name: string, - is: UnionType['is'], - validate: UnionType['validate'], - serialize: UnionType['serialize'], + is: UnionType['is'], + validate: UnionType['validate'], + serialize: UnionType['encode'], readonly types: RTS ) { super(name, is, validate, serialize) } } -export const union = >( +export const union = >( types: RTS, name: string = `(${types.map(type => type.name).join(' | ')})` -): UnionType> => { +): UnionType, OutputOf, mixed> => { const len = types.length return new UnionType( name, - (v): v is TypeOf => types.some(type => type.is(v)), - (s, c) => { + (m): m is TypeOf => types.some(type => type.is(m)), + (m, c) => { const errors: Errors = [] for (let i = 0; i < len; i++) { const type = types[i] - const validation = type.validate(s, appendContext(c, String(i), type)) + const validation = type.validate(m, appendContext(c, String(i), type)) if (validation.isRight()) { return validation } else { @@ -671,17 +684,17 @@ export const union = >( } return failures(errors) }, - types.every(type => type.serialize === identity) + types.every(type => type.encode === identity) ? identity : a => { let i = 0 for (; i < len - 1; i++) { const type = types[i] if (type.is(a)) { - return type.serialize(a) + return type.encode(a) } } - return types[i].serialize(a) + return types[i].encode(a) }, types ) @@ -691,46 +704,59 @@ export const union = >( // intersections // -export class IntersectionType, A> extends Type { +export class IntersectionType, A = any, O = A, I = mixed> extends Type { readonly _tag: 'IntersectionType' = 'IntersectionType' constructor( name: string, - is: IntersectionType['is'], - validate: IntersectionType['validate'], - serialize: IntersectionType['serialize'], + is: IntersectionType['is'], + validate: IntersectionType['validate'], + serialize: IntersectionType['encode'], readonly types: RTS ) { super(name, is, validate, serialize) } } -export function intersection( +export function intersection( types: [A, B, C, D, E], name?: string -): IntersectionType<[A, B, C, D, E], TypeOf & TypeOf & TypeOf & TypeOf & TypeOf> -export function intersection( +): IntersectionType< + [A, B, C, D, E], + TypeOf & TypeOf & TypeOf & TypeOf & TypeOf, + OutputOf & OutputOf & OutputOf & OutputOf & OutputOf, + mixed +> +export function intersection( types: [A, B, C, D], name?: string -): IntersectionType<[A, B, C, D], TypeOf & TypeOf & TypeOf & TypeOf> -export function intersection( +): IntersectionType< + [A, B, C, D], + TypeOf & TypeOf & TypeOf & TypeOf, + OutputOf & OutputOf & OutputOf & OutputOf, + mixed +> +export function intersection( types: [A, B, C], name?: string -): IntersectionType<[A, B, C], TypeOf & TypeOf & TypeOf> -export function intersection( +): IntersectionType<[A, B, C], TypeOf & TypeOf & TypeOf, OutputOf & OutputOf & OutputOf, mixed> +export function intersection( types: [A, B], name?: string -): IntersectionType<[A, B], TypeOf & TypeOf> -export function intersection(types: [A], name?: string): IntersectionType<[A], TypeOf> -export function intersection>( +): IntersectionType<[A, B], TypeOf & TypeOf, OutputOf & OutputOf, mixed> +export function intersection( + types: [A], + name?: string +): IntersectionType<[A], TypeOf, OutputOf, mixed> +export function intersection>( types: RTS, name: string = `(${types.map(type => type.name).join(' & ')})` -): IntersectionType { +): IntersectionType { const len = types.length return new IntersectionType( name, - (v): v is any => types.every(type => type.is(v)), - (s, c) => { - let a = s + (m): m is any => types.every(type => type.is(m)), + (m, c) => { + let a = m const errors: Errors = [] for (let i = 0; i < len; i++) { const type = types[i] @@ -739,13 +765,13 @@ export function intersection>( } return errors.length ? failures(errors) : success(a) }, - types.every(type => type.serialize === identity) + types.every(type => type.encode === identity) ? identity : a => { let s = a for (let i = 0; i < len; i++) { const type = types[i] - s = type.serialize(s) + s = type.encode(s) } return s }, @@ -757,46 +783,56 @@ export function intersection>( // tuples // -export class TupleType, A> extends Type { +export class TupleType, A = any, O = A, I = mixed> extends Type { readonly _tag: 'TupleType' = 'TupleType' constructor( name: string, - is: TupleType['is'], - validate: TupleType['validate'], - serialize: TupleType['serialize'], + is: TupleType['is'], + validate: TupleType['validate'], + serialize: TupleType['encode'], readonly types: RTS ) { super(name, is, validate, serialize) } } -export function tuple( +export function tuple( types: [A, B, C, D, E], name?: string -): TupleType<[A, B, C, D, E], [TypeOf, TypeOf, TypeOf, TypeOf, TypeOf]> -export function tuple( +): TupleType< + [A, B, C, D, E], + [TypeOf, TypeOf, TypeOf, TypeOf, TypeOf], + [OutputOf, OutputOf, OutputOf, OutputOf, OutputOf], + mixed +> +export function tuple( types: [A, B, C, D], name?: string -): TupleType<[A, B, C, D], [TypeOf, TypeOf, TypeOf, TypeOf]> -export function tuple( +): TupleType< + [A, B, C, D], + [TypeOf, TypeOf, TypeOf, TypeOf], + [OutputOf, OutputOf, OutputOf, OutputOf], + mixed +> +export function tuple( types: [A, B, C], name?: string -): TupleType<[A, B, C], [TypeOf, TypeOf, TypeOf]> -export function tuple( +): TupleType<[A, B, C], [TypeOf, TypeOf, TypeOf], [OutputOf, OutputOf, OutputOf], mixed> +export function tuple( types: [A, B], name?: string -): TupleType<[A, B], [TypeOf, TypeOf]> -export function tuple(types: [A], name?: string): TupleType<[A], [TypeOf]> -export function tuple>( +): TupleType<[A, B], [TypeOf, TypeOf], [OutputOf, OutputOf], mixed> +export function tuple(types: [A], name?: string): TupleType<[A], [TypeOf], [OutputOf], mixed> +export function tuple>( types: RTS, name: string = `[${types.map(type => type.name).join(', ')}]` -): TupleType { +): TupleType { const len = types.length return new TupleType( name, - (v): v is any => arrayType.is(v) && v.length === len && types.every((type, i) => type.is(v[i])), - (s, c) => - arrayType.validate(s, c).chain(as => { + (m): m is any => arrayType.is(m) && m.length === len && types.every((type, i) => type.is(m[i])), + (m, c) => + arrayType.validate(m, c).chain(as => { let t: Array = as const errors: Errors = [] for (let i = 0; i < len; i++) { @@ -820,7 +856,7 @@ export function tuple>( } return errors.length ? failures(errors) : success(t) }), - types.every(type => type.serialize === identity) ? identity : a => types.map((type, i) => type.serialize(a[i])), + types.every(type => type.encode === identity) ? identity : a => types.map((type, i) => type.encode(a[i])), types ) } @@ -829,34 +865,34 @@ export function tuple>( // readonly objects // -export class ReadonlyType extends Type { +export class ReadonlyType extends Type { readonly _tag: 'ReadonlyType' = 'ReadonlyType' constructor( name: string, - is: ReadonlyType['is'], - validate: ReadonlyType['validate'], - serialize: ReadonlyType['serialize'], + is: ReadonlyType['is'], + validate: ReadonlyType['validate'], + serialize: ReadonlyType['encode'], readonly type: RT ) { super(name, is, validate, serialize) } } -export const readonly = ( +export const readonly = ( type: RT, name: string = `Readonly<${type.name}>` -): ReadonlyType>> => +): ReadonlyType>, Readonly>, mixed> => new ReadonlyType( name, type.is, - (s, c) => - type.validate(s, c).map(x => { + (m, c) => + type.validate(m, c).map(x => { if (process.env.NODE_ENV !== 'production') { return Object.freeze(x) } return x }), - type.serialize === identity ? identity : type.serialize, + type.encode === identity ? identity : type.encode, type ) @@ -864,36 +900,36 @@ export const readonly = ( // readonly arrays // -export class ReadonlyArrayType extends Type { +export class ReadonlyArrayType extends Type { readonly _tag: 'ReadonlyArrayType' = 'ReadonlyArrayType' constructor( name: string, - is: ReadonlyArrayType['is'], - validate: ReadonlyArrayType['validate'], - serialize: ReadonlyArrayType['serialize'], + is: ReadonlyArrayType['is'], + validate: ReadonlyArrayType['validate'], + serialize: ReadonlyArrayType['encode'], readonly type: RT ) { super(name, is, validate, serialize) } } -export const readonlyArray = ( +export const readonlyArray = ( type: RT, name: string = `ReadonlyArray<${type.name}>` -): ReadonlyArrayType>> => { +): ReadonlyArrayType>, ReadonlyArray>, mixed> => { const arrayType = array(type) return new ReadonlyArrayType( name, arrayType.is, - (s, c) => - arrayType.validate(s, c).map(x => { + (m, c) => + arrayType.validate(m, c).map(x => { if (process.env.NODE_ENV !== 'production') { return Object.freeze(x) } else { return x } }), - arrayType.serialize as any, + arrayType.encode as any, type ) } @@ -902,13 +938,13 @@ export const readonlyArray = ( // strict interfaces // -export class StrictType

extends Type { +export class StrictType

extends Type { readonly _tag: 'StrictType' = 'StrictType' constructor( name: string, - is: StrictType['is'], - validate: StrictType['validate'], - serialize: StrictType['serialize'], + is: StrictType['is'], + validate: StrictType['validate'], + serialize: StrictType['encode'], readonly props: P ) { super(name, is, validate, serialize) @@ -919,13 +955,13 @@ export class StrictType

extends Type { export const strict =

( props: P, name: string = `StrictType<${getNameFromProps(props)}>` -): StrictType> => { +): StrictType }, { [K in keyof P]: OutputOf }, mixed> => { const loose = type(props) return new StrictType( name, - (v): v is InterfaceOf

=> loose.is(v) && Object.getOwnPropertyNames(v).every(k => props.hasOwnProperty(k)), - (s, c) => - loose.validate(s, c).chain(o => { + (m): m is TypeOfProps

=> loose.is(m) && Object.getOwnPropertyNames(m).every(k => props.hasOwnProperty(k)), + (m, c) => + loose.validate(m, c).chain(o => { const keys = Object.getOwnPropertyNames(o) const len = keys.length const errors: Errors = [] @@ -937,7 +973,7 @@ export const strict =

( } return errors.length ? failures(errors) : success(o) }), - loose.serialize, + loose.encode, props ) } @@ -947,35 +983,35 @@ export const strict =

( // export type TaggedProps = { [K in Tag]: LiteralType } -export interface TaggedRefinement extends RefinementType, mixed, A> {} -export interface TaggedUnion extends UnionType>, A> {} +export interface TaggedRefinement extends RefinementType, A, O> {} +export interface TaggedUnion extends UnionType>, A, O> {} export type TaggedIntersectionArgument = | [Tagged] - | [Tagged, Any] - | [Any, Tagged] - | [Tagged, Any, Any] - | [Any, Tagged, Any] - | [Any, Any, Tagged] - | [Tagged, Any, Any, Any] - | [Any, Tagged, Any, Any] - | [Any, Any, Tagged, Any] - | [Any, Any, Any, Tagged] - | [Tagged, Any, Any, Any, Any] - | [Any, Tagged, Any, Any, Any] - | [Any, Any, Tagged, Any, Any] - | [Any, Any, Any, Tagged, Any] - | [Any, Any, Any, Any, Tagged] -export interface TaggedIntersection - extends IntersectionType, A> {} -export type Tagged = - | InterfaceType, A> - | StrictType, A> - | TaggedRefinement - | TaggedUnion - | TaggedIntersection - -const isTagged = (tag: Tag): ((type: Any) => type is Tagged) => { - const f = (type: Any): type is Tagged => { + | [Tagged, Mixed] + | [Mixed, Tagged] + | [Tagged, Mixed, Mixed] + | [Mixed, Tagged, Mixed] + | [Mixed, Mixed, Tagged] + | [Tagged, Mixed, Mixed, Mixed] + | [Mixed, Tagged, Mixed, Mixed] + | [Mixed, Mixed, Tagged, Mixed] + | [Mixed, Mixed, Mixed, Tagged] + | [Tagged, Mixed, Mixed, Mixed, Mixed] + | [Mixed, Tagged, Mixed, Mixed, Mixed] + | [Mixed, Mixed, Tagged, Mixed, Mixed] + | [Mixed, Mixed, Mixed, Tagged, Mixed] + | [Mixed, Mixed, Mixed, Mixed, Tagged] +export interface TaggedIntersection + extends IntersectionType, A, O> {} +export type Tagged = + | InterfaceType, A, O> + | StrictType, A, O> + | TaggedRefinement + | TaggedUnion + | TaggedIntersection + +const isTagged = (tag: Tag): ((type: Mixed) => type is Tagged) => { + const f = (type: Mixed): type is Tagged => { if (type instanceof InterfaceType || type instanceof StrictType) { return type.props.hasOwnProperty(tag) } else if (type instanceof IntersectionType) { @@ -1025,7 +1061,7 @@ export const taggedUnion = >>( tag: Tag, types: RTS, name: string = `(${types.map(type => type.name).join(' | ')})` -): UnionType> => { +): UnionType, OutputOf, mixed> => { const tagValue2Index: { [key: string]: number } = {} const tagValues: { [key: string]: null } = {} const len = types.length @@ -1036,7 +1072,7 @@ export const taggedUnion = >>( tagValues[value] = null } const TagValue = keyof(tagValues) - return new UnionType( + return new UnionType, OutputOf, mixed>( name, (v): v is TypeOf => { if (!Dictionary.is(v)) { @@ -1053,7 +1089,7 @@ export const taggedUnion = >>( return type.validate(d, appendContext(c, String(i), type)) }) ), - types.every(type => type.serialize === identity) ? identity : a => types[tagValue2Index[a[tag]]].serialize(a), + types.every(type => type.encode === identity) ? identity : a => types[tagValue2Index[a[tag] as any]].encode(a), types ) } diff --git a/test/array.ts b/test/array.ts index 4774b420d..774f4f9b2 100644 --- a/test/array.ts +++ b/test/array.ts @@ -28,12 +28,12 @@ describe('array', () => { it('should serialize a deserialized', () => { const T = t.array(DateFromNumber) - assert.deepEqual(T.serialize([new Date(0), new Date(1)]), [0, 1]) + assert.deepEqual(T.encode([new Date(0), new Date(1)]), [0, 1]) }) it('should return the same reference when serializing', () => { const T = t.array(t.number) - assert.strictEqual(T.serialize, t.identity) + assert.strictEqual(T.encode, t.identity) }) it('should type guard', () => { diff --git a/test/dictionary.ts b/test/dictionary.ts index 0809cd54c..e86f751c9 100644 --- a/test/dictionary.ts +++ b/test/dictionary.ts @@ -53,16 +53,16 @@ describe('dictionary', () => { it('should serialize a deserialized', () => { const T1 = t.dictionary(t.string, DateFromNumber) - assert.deepEqual(T1.serialize({ a: new Date(0), b: new Date(1) }), { a: 0, b: 1 }) + assert.deepEqual(T1.encode({ a: new Date(0), b: new Date(1) }), { a: 0, b: 1 }) const T2 = t.dictionary(string2, t.number) - assert.deepEqual(T2.serialize({ 'a-a': 1, 'a-b': 2 }), { aa: 1, ab: 2 }) + assert.deepEqual(T2.encode({ 'a-a': 1, 'a-b': 2 }), { aa: 1, ab: 2 }) }) it('should return the same reference when serializing', () => { const T1 = t.dictionary(t.string, t.number) - assert.strictEqual(T1.serialize, t.identity) + assert.strictEqual(T1.encode, t.identity) const T2 = t.dictionary(string2, t.number) - assert.strictEqual(T2.serialize === t.identity, false) + assert.strictEqual(T2.encode === t.identity, false) }) it('should type guard', () => { diff --git a/test/helpers.ts b/test/helpers.ts index 40e6aebf9..c5c85d059 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -19,7 +19,7 @@ export function assertDeepEqual(validation: t.Validation, value: any): voi assert.deepEqual(validation.fold(t.identity, t.identity), value) } -export const string2 = new t.Type( +export const string2 = new t.Type( 'string2', (v): v is string => t.string.is(v) && v[1] === '-', (s, c) => @@ -33,7 +33,7 @@ export const string2 = new t.Type( a => a[0] + a[2] ) -export const DateFromNumber = new t.Type( +export const DateFromNumber = new t.Type( 'DateFromNumber', (v): v is Date => v instanceof Date, (s, c) => @@ -44,7 +44,7 @@ export const DateFromNumber = new t.Type( a => a.getTime() ) -export const NumberFromString = new t.Type( +export const NumberFromString = new t.Type( 'NumberFromString', t.number.is, (s, c) => { @@ -56,11 +56,11 @@ export const NumberFromString = new t.Type( export const IntegerFromString = t.refinement(NumberFromString, t.Integer.is, 'IntegerFromString') -export function withDefault(type: T, defaultValue: t.TypeOf): t.Type, t.TypeOf> { +export function withDefault(type: T, defaultValue: t.TypeOf): t.Type, t.TypeOf> { return new t.Type( `withDefault(${type.name}, ${JSON.stringify(defaultValue)})`, type.is, (v, c) => type.validate(v != null ? v : defaultValue, c), - type.serialize + type.encode ) } diff --git a/test/interface.ts b/test/interface.ts index 98dc0f6db..445f40f94 100644 --- a/test/interface.ts +++ b/test/interface.ts @@ -43,12 +43,12 @@ describe('interface', () => { it('should serialize a deserialized', () => { const T = t.type({ a: DateFromNumber }) - assert.deepEqual(T.serialize({ a: new Date(0) }), { a: 0 }) + assert.deepEqual(T.encode({ a: new Date(0) }), { a: 0 }) }) it('should return the same reference when serializing', () => { const T = t.type({ a: t.number }) - assert.strictEqual(T.serialize, t.identity) + assert.strictEqual(T.encode, t.identity) }) it('should type guard', () => { diff --git a/test/intersection.ts b/test/intersection.ts index f3a6e27b3..ff85d9e0d 100644 --- a/test/intersection.ts +++ b/test/intersection.ts @@ -38,12 +38,12 @@ describe('intersection', () => { it('should serialize a deserialized', () => { const T = t.intersection([t.interface({ a: DateFromNumber }), t.interface({ b: t.number })]) - assert.deepEqual(T.serialize({ a: new Date(0), b: 1 }), { a: 0, b: 1 }) + assert.deepEqual(T.encode({ a: new Date(0), b: 1 }), { a: 0, b: 1 }) }) it('should return the same reference when serializing', () => { const T = t.intersection([t.interface({ a: t.number }), t.interface({ b: t.number })]) - assert.strictEqual(T.serialize, t.identity) + assert.strictEqual(T.encode, t.identity) }) it('should type guard', () => { diff --git a/test/keyof.ts b/test/keyof.ts index 5aed67952..b1b4ffe9f 100644 --- a/test/keyof.ts +++ b/test/keyof.ts @@ -16,7 +16,7 @@ describe('keyof', () => { it('should return the same reference when serializing', () => { const T = t.keyof({ a: 1, b: 2 }) - assert.strictEqual(T.serialize, t.identity) + assert.strictEqual(T.encode, t.identity) }) it('should type guard', () => { diff --git a/test/literal.ts b/test/literal.ts index 9c1f7cb3e..74b175a50 100644 --- a/test/literal.ts +++ b/test/literal.ts @@ -15,7 +15,7 @@ describe('literal', () => { it('should return the same reference when serializing', () => { const T = t.literal('a') - assert.strictEqual(T.serialize, t.identity) + assert.strictEqual(T.encode, t.identity) }) it('should type guard', () => { diff --git a/test/partial.ts b/test/partial.ts index 38242b61e..0cb1a2b0b 100644 --- a/test/partial.ts +++ b/test/partial.ts @@ -48,13 +48,13 @@ describe('partial', () => { it('should serialize a deserialized', () => { const T = t.partial({ a: DateFromNumber }) - assert.deepEqual(T.serialize({ a: new Date(0) }), { a: 0 }) - assert.deepEqual(T.serialize({}), {}) + assert.deepEqual(T.encode({ a: new Date(0) }), { a: 0 }) + assert.deepEqual(T.encode({}), {}) }) it('should return the same reference when serializing', () => { const T = t.partial({ a: t.number }) - assert.strictEqual(T.serialize, t.identity) + assert.strictEqual(T.encode, t.identity) }) it('should type guard', () => { diff --git a/test/readonly.ts b/test/readonly.ts index a2399f763..f627903f8 100644 --- a/test/readonly.ts +++ b/test/readonly.ts @@ -20,12 +20,12 @@ describe('readonly', () => { it('should serialize a deserialized', () => { const T = t.readonly(t.interface({ a: DateFromNumber })) - assert.deepEqual(T.serialize({ a: new Date(0) }), { a: 0 }) + assert.deepEqual(T.encode({ a: new Date(0) }), { a: 0 }) }) it('should return the same reference when serializing', () => { const T = t.readonly(t.type({ a: t.number })) - assert.strictEqual(T.serialize, t.identity) + assert.strictEqual(T.encode, t.identity) }) it('should type guard', () => { diff --git a/test/readonlyArray.ts b/test/readonlyArray.ts index d165a23f9..5803616b7 100644 --- a/test/readonlyArray.ts +++ b/test/readonlyArray.ts @@ -20,12 +20,12 @@ describe('readonlyArray', () => { it('should serialize a deserialized', () => { const T = t.readonlyArray(DateFromNumber) - assert.deepEqual(T.serialize([new Date(0), new Date(1)]), [0, 1]) + assert.deepEqual(T.encode([new Date(0), new Date(1)]), [0, 1]) }) it('should return the same reference when serializing', () => { const T = t.readonlyArray(t.number) - assert.strictEqual(T.serialize, t.identity) + assert.strictEqual(T.encode, t.identity) }) it('should type guard', () => { diff --git a/test/recursion.ts b/test/recursion.ts index 4ea4d28f9..ac1362ac8 100644 --- a/test/recursion.ts +++ b/test/recursion.ts @@ -20,6 +20,16 @@ describe('recursion', () => { }) it('should return the same reference if validation succeeded', () => { + type T = { + a: number + b: T | null | undefined + } + const T = t.recursion('T', self => + t.interface({ + a: t.number, + b: t.union([self, t.undefined, t.null]) + }) + ) const value = { a: 1, b: { a: 2, b: null } } assertStrictEqual(T.decode(value), value) }) @@ -35,26 +45,34 @@ describe('recursion', () => { }) it('should serialize a deserialized', () => { - type T = { + type A = { a: Date - b: T | null + b: A | null } - const T = t.recursion('T', self => + type O = { + a: number + b: O | null + } + const T = t.recursion('T', self => t.interface({ a: DateFromNumber, b: t.union([self, t.null]) }) ) - assert.deepEqual(T.serialize({ a: new Date(0), b: null }), { a: 0, b: null }) - assert.deepEqual(T.serialize({ a: new Date(0), b: { a: new Date(1), b: null } }), { a: 0, b: { a: 1, b: null } }) + assert.deepEqual(T.encode({ a: new Date(0), b: null }), { a: 0, b: null }) + assert.deepEqual(T.encode({ a: new Date(0), b: { a: new Date(1), b: null } }), { a: 0, b: { a: 1, b: null } }) }) it('should type guard', () => { - type T = { + type A = { a: Date - b: T | null + b: A | null } - const T = t.recursion('T', self => + type O = { + a: number + b: O | null + } + const T = t.recursion('T', self => t.interface({ a: DateFromNumber, b: t.union([self, t.null]) @@ -87,7 +105,7 @@ describe('recursion', () => { type B = { a: A | null } - const A: t.RecursiveType = t.recursion('A', self => + const A: t.RecursiveType = t.recursion('A', self => t.interface({ b: t.union([self, B, t.null]) }) diff --git a/test/refinement.ts b/test/refinement.ts index a34c7dc8b..a8a612d79 100644 --- a/test/refinement.ts +++ b/test/refinement.ts @@ -29,12 +29,12 @@ describe('refinement', () => { it('should serialize a deserialized', () => { const T = t.refinement(t.array(DateFromNumber), () => true) - assert.deepEqual(T.serialize([new Date(0)]), [0]) + assert.deepEqual(T.encode([new Date(0)]), [0]) }) it('should return the same reference when serializing', () => { const T = t.refinement(t.array(t.number), () => true) - assert.strictEqual(T.serialize, t.identity) + assert.strictEqual(T.encode, t.identity) }) it('should type guard', () => { diff --git a/test/strict.ts b/test/strict.ts index 20391e1b1..ef7c0494a 100644 --- a/test/strict.ts +++ b/test/strict.ts @@ -36,12 +36,12 @@ describe('strict', () => { it('should serialize a deserialized', () => { const T = t.strict({ a: DateFromNumber }) - assert.deepEqual(T.serialize({ a: new Date(0) }), { a: 0 }) + assert.deepEqual(T.encode({ a: new Date(0) }), { a: 0 }) }) it('should return the same reference when serializing', () => { const T = t.strict({ a: t.number }) - assert.strictEqual(T.serialize, t.identity) + assert.strictEqual(T.encode, t.identity) }) it('should type guard', () => { diff --git a/test/taggedUnion.ts b/test/taggedUnion.ts index 25f419967..1fb23ccdd 100644 --- a/test/taggedUnion.ts +++ b/test/taggedUnion.ts @@ -61,14 +61,14 @@ describe('taggedUnion', () => { }) it('should serialize a deserialized', () => { - assert.deepEqual(T.serialize({ type: 'a', foo: 'foo' }), { type: 'a', foo: 'foo' }) - assert.deepEqual(T.serialize({ type: 'b', bar: 1 }), { type: 'b', bar: 1 }) - assert.deepEqual(T.serialize({ type: 'c', baz: new Date(0) }), { type: 'c', baz: 0 }) + assert.deepEqual(T.encode({ type: 'a', foo: 'foo' }), { type: 'a', foo: 'foo' }) + assert.deepEqual(T.encode({ type: 'b', bar: 1 }), { type: 'b', bar: 1 }) + assert.deepEqual(T.encode({ type: 'c', baz: new Date(0) }), { type: 'c', baz: 0 }) }) it('should return the same reference when serializing', () => { const T = t.taggedUnion('type', [TUA, TUB]) - assert.strictEqual(T.serialize, t.identity) + assert.strictEqual(T.encode, t.identity) }) it('should type guard', () => { diff --git a/test/tuple.ts b/test/tuple.ts index 9aecfbc53..d0e25cb1d 100644 --- a/test/tuple.ts +++ b/test/tuple.ts @@ -32,12 +32,12 @@ describe('tuple', () => { it('should serialize a deserialized', () => { const T = t.tuple([DateFromNumber, t.string]) - assert.deepEqual(T.serialize([new Date(0), 'foo']), [0, 'foo']) + assert.deepEqual(T.encode([new Date(0), 'foo']), [0, 'foo']) }) it('should return the same reference when serializing', () => { const T = t.tuple([t.number, t.string]) - assert.strictEqual(T.serialize, t.identity) + assert.strictEqual(T.encode, t.identity) }) it('should type guard', () => { diff --git a/test/union.ts b/test/union.ts index d10e14a97..6d0f463e2 100644 --- a/test/union.ts +++ b/test/union.ts @@ -25,15 +25,15 @@ describe('union', () => { it('should serialize a deserialized', () => { const T1 = t.union([t.interface({ a: DateFromNumber }), t.number]) - assert.deepEqual(T1.serialize({ a: new Date(0) }), { a: 0 }) - assert.deepEqual(T1.serialize(1), 1) + assert.deepEqual(T1.encode({ a: new Date(0) }), { a: 0 }) + assert.deepEqual(T1.encode(1), 1) const T2 = t.union([t.number, DateFromNumber]) - assert.deepEqual(T2.serialize(new Date(0)), 0) + assert.deepEqual(T2.encode(new Date(0)), 0) }) it('should return the same reference when serializing', () => { const T = t.union([t.type({ a: t.number }), t.string]) - assert.strictEqual(T.serialize, t.identity) + assert.strictEqual(T.encode, t.identity) }) it('should type guard', () => { diff --git a/typings-checker/index.ts b/typings-checker/index.ts index 14ba65e6e..427e6a734 100644 --- a/typings-checker/index.ts +++ b/typings-checker/index.ts @@ -15,7 +15,7 @@ const Rec1 = t.recursion('T', Self => items: t.array(Self) }) ) -// $ExpectError Type 'InterfaceOf<{ type: LiteralType<"a">; items: ArrayType, string[]>; }>' is not assignable to type 'string' +// $ExpectError Argument of type '(Self: Type) => InterfaceType<{ type: LiteralType<"a">; items: ArrayType) => Type' const Rec2 = t.recursion('T', Self => t.interface({ type: t.literal('a'), @@ -92,9 +92,10 @@ const x8: TypeOf = { age: 43 } const x9: TypeOf = { name: 'name', age: 43 } const I2 = t.interface({ name: t.string, father: t.interface({ surname: t.string }) }) +type I2T = TypeOf // $ExpectError Property 'surname' is missing in type '{}' -const x10: TypeOf = { name: 'name', father: {} } -const x11: TypeOf = { name: 'name', father: { surname: 'surname' } } +const x10: I2T = { name: 'name', father: {} } +const x11: I2T = { name: 'name', father: { surname: 'surname' } } // // dictionary @@ -103,7 +104,7 @@ const x11: TypeOf = { name: 'name', father: { surname: 'surname' } } const D1 = t.dictionary(t.keyof({ a: true }), t.number) // $ExpectError Type 'string' is not assignable to type 'number' const x12: TypeOf = { a: 's' } -// $ExpectError Object literal may only specify known properties, and 'c' does not exist in type '{ a: number; }' +// $ExpectError Type '{ c: number; }' is not assignable to type '{ a: number; }'. const x12_2: TypeOf = { c: 1 } const x13: TypeOf = { a: 1 } @@ -140,10 +141,11 @@ const x20: TypeOf = ['s', 1] // const P1 = t.partial({ name: t.string }) +type P1T = TypeOf // $ExpectError Type 'number' is not assignable to type 'string | undefined' -const x21: TypeOf = { name: 1 } -const x22: TypeOf = {} -const x23: TypeOf = { name: 's' } +const x21: P1T = { name: 1 } +const x22: P1T = {} +const x23: P1T = { name: 's' } // // readonly @@ -161,7 +163,7 @@ const x25: TypeOf = { name: 1 } // const ROA1 = t.readonlyArray(t.number) -// $ExpectError Type 'string' is not assignable to type 'number' +// $ExpectError Type 'string[]' is not assignable to type 'ReadonlyArray' const x26: TypeOf = ['s'] const x27: TypeOf = [1] // $ExpectError Index signature in type 'ReadonlyArray' only permits reading @@ -191,18 +193,18 @@ const x34: TO1 = { name: 'Giulio' } const x35: TO1 = 'foo' type GenerableProps = { [key: string]: Generable } -type GenerableInterface = t.InterfaceType -type GenerableStrict = t.StrictType -type GenerablePartials = t.PartialType -interface GenerableDictionary extends t.DictionaryType {} -interface GenerableRefinement extends t.RefinementType {} -interface GenerableArray extends t.ArrayType {} -interface GenerableUnion extends t.UnionType, any> {} -interface GenerableIntersection extends t.IntersectionType, any> {} -interface GenerableTuple extends t.TupleType, any> {} -interface GenerableReadonly extends t.ReadonlyType {} -interface GenerableReadonlyArray extends t.ReadonlyArrayType {} -interface GenerableRecursive extends t.RecursiveType {} +type GenerableInterface = t.InterfaceType +type GenerableStrict = t.StrictType +type GenerablePartials = t.PartialType +interface GenerableDictionary extends t.DictionaryType {} +interface GenerableRefinement extends t.RefinementType {} +interface GenerableArray extends t.ArrayType {} +interface GenerableUnion extends t.UnionType> {} +interface GenerableIntersection extends t.IntersectionType> {} +interface GenerableTuple extends t.TupleType> {} +interface GenerableReadonly extends t.ReadonlyType {} +interface GenerableReadonlyArray extends t.ReadonlyArrayType {} +interface GenerableRecursive extends t.RecursiveType {} type Generable = | t.StringType | t.NumberType @@ -286,16 +288,15 @@ const schema = t.interface({ }) f(schema) // OK! - type Rec = { a: number b: Rec | undefined } -const Rec = t.recursion('T', Self => +const Rec = t.recursion('T', self => t.interface({ a: t.number, - b: t.union([Self, t.undefined]) + b: t.union([self, t.undefined]) }) ) @@ -304,20 +305,58 @@ f(Rec) // OK! // // tagged union // - const TU1 = t.taggedUnion('type', [t.type({ type: t.literal('a') }), t.type({ type: t.literal('b') })]) -// $ExpectError Type 'true' is not assignable to type 'InterfaceOf<{ type: LiteralType<"a">; }> | InterfaceOf<{ type: LiteralType<"b">; }>' +// $ExpectError Type 'true' is not assignable to type '{ type: "a"; } | { type: "b"; }' const x36: TypeOf = true const x37: TypeOf = { type: 'a' } const x38: TypeOf = { type: 'b' } -export function interfaceWithOptionals( - required: R, - optional: O, +// +// custom combinators +// + +export function interfaceWithOptionals( + required: RequiredProps, + optional: OptionalProps, name?: string ): t.IntersectionType< - [t.InterfaceType>, t.PartialType>], - t.InterfaceOf & t.PartialOf + [ + t.InterfaceType>, + t.PartialType> + ], + t.TypeOfProps & t.TypeOfPartialProps > { return t.intersection([t.interface(required), t.partial(optional)], name) } + +export function maybe( + type: RT, + name?: string +): t.UnionType<[RT, t.NullType], t.TypeOf | null, t.OutputOf | null, t.InputOf | null> { + return t.union<[RT, t.NullType]>([type, t.null], name) +} + +const pluck = >>>( + union: U, + field: F +): t.Type[F]> => { + return t.union(union.types.map(type => type.props[field])) +} + +export const Action = t.union([ + t.type({ + type: t.literal('Action1'), + payload: t.type({ + foo: t.string + }) + }), + t.type({ + type: t.literal('Action2'), + payload: t.type({ + bar: t.string + }) + }) +]) + +// ActionType: t.Type<"Action1" | "Action2", "Action1" | "Action2", t.mixed> +const ActionType = pluck(Action, 'type')