├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── Bug_report.md │ ├── Documentation.md │ └── Feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── main.yml ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── CHANGELOG.md ├── Codec.md ├── Decoder.md ├── Encoder.md ├── Eq.md ├── LICENSE ├── README.md ├── Schema.md ├── docs ├── _config.yml ├── index.md └── modules │ ├── Codec.ts.md │ ├── DecodeError.ts.md │ ├── Decoder.ts.md │ ├── Encoder.ts.md │ ├── Eq.ts.md │ ├── FreeSemigroup.ts.md │ ├── Guard.ts.md │ ├── Kleisli.ts.md │ ├── PathReporter.ts.md │ ├── Reporter.ts.md │ ├── Schema.ts.md │ ├── Schemable.ts.md │ ├── TaskDecoder.ts.md │ ├── Type.ts.md │ ├── index.md │ └── index.ts.md ├── dtslint ├── Codec.ts ├── Decoder.ts ├── Encoder.ts ├── Eq.ts ├── Guard.ts ├── Schema.ts ├── TaskDecoder.ts ├── Type.ts ├── index.d.ts ├── index.ts └── tsconfig.json ├── images ├── inference.png └── introspection.png ├── index.md ├── jest.config.js ├── package-lock.json ├── package.json ├── perf ├── Decoder.ts ├── Decoder2.ts ├── index.ts └── typescript-runtime-type-benchmarks.ts ├── scripts ├── FileSystem.ts ├── build.ts ├── pre-publish.ts ├── release.ts └── run.ts ├── src ├── Codec.ts ├── DecodeError.ts ├── Decoder.ts ├── Encoder.ts ├── Eq.ts ├── FreeSemigroup.ts ├── Guard.ts ├── Kleisli.ts ├── PathReporter.ts ├── Reporter.ts ├── Schema.ts ├── Schemable.ts ├── TaskDecoder.ts ├── ThrowReporter.ts ├── Type.ts └── index.ts ├── test ├── 2.1.x │ ├── PathReporter.ts │ ├── ThrowReporter.ts │ ├── TypeClass.ts │ ├── array.ts │ ├── brand.ts │ ├── default-types.ts │ ├── exact.ts │ ├── helpers.ts │ ├── intersection.ts │ ├── keyof.ts │ ├── literal.ts │ ├── partial.ts │ ├── readonly.ts │ ├── readonlyArray.ts │ ├── record.ts │ ├── recursion.ts │ ├── refinement.ts │ ├── strict.ts │ ├── strictInterfaceWithOptionals.ts │ ├── taggedUnion.ts │ ├── tuple.ts │ ├── type.ts │ └── union.ts ├── Arbitrary.ts ├── Codec.ts ├── Decoder.ts ├── Encoder.ts ├── Eq.ts ├── FreeSemigroup.ts ├── Guard.ts ├── JsonSchema.test.ts ├── JsonSchema.ts ├── Schema.ts ├── Schemable.ts ├── TaskDecoder.ts ├── Type.ts ├── helpers.ts └── tsconfig.json ├── tsconfig.build-es6.json ├── tsconfig.build.json ├── tsconfig.json └── vite.config.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "project": "./tsconfig.json" 4 | }, 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/eslint-recommended", 8 | "plugin:@typescript-eslint/recommended" 9 | ], 10 | "plugins": ["deprecation", "import", "simple-import-sort"], 11 | "rules": { 12 | "@typescript-eslint/array-type": [ 13 | "warn", 14 | { 15 | "default": "generic", 16 | "readonly": "generic" 17 | } 18 | ], 19 | "@typescript-eslint/prefer-readonly": "warn", 20 | "@typescript-eslint/member-delimiter-style": 0, 21 | "@typescript-eslint/no-non-null-assertion": "off", 22 | "@typescript-eslint/ban-types": "off", 23 | "@typescript-eslint/no-explicit-any": "off", 24 | "@typescript-eslint/no-empty-interface": "off", 25 | "no-unused-vars": "off", 26 | "@typescript-eslint/no-unused-vars": [ 27 | "error", 28 | { 29 | "argsIgnorePattern": "^_" 30 | } 31 | ], 32 | "@typescript-eslint/prefer-as-const": "off", 33 | "@typescript-eslint/ban-ts-comment": "off", 34 | "prefer-rest-params": "off", 35 | "prefer-spread": "off", 36 | "deprecation/deprecation": "off", 37 | "import/first": "error", 38 | "import/no-cycle": "error", 39 | "import/newline-after-import": "error", 40 | "import/no-duplicates": "error", 41 | "import/no-unresolved": "off", 42 | "import/order": "off", 43 | "simple-import-sort/imports": "error" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Bug report" 3 | about: Create a report to help make io-ts better 4 | --- 5 | 6 | ## 🐛 Bug report 7 | 8 | ### Current Behavior 9 | 10 | 11 | 12 | ### Expected behavior 13 | 14 | 15 | 16 | ### Reproducible example 17 | 18 | ### Suggested solution(s) 19 | 20 | 21 | 22 | ### Additional context 23 | 24 | 25 | 26 | ### Your environment 27 | 28 | Which versions of io-ts are affected by this issue? Did this work in previous versions of io-ts? 29 | 30 | 31 | 32 | | Software | Version(s) | 33 | | ---------- | ---------- | 34 | | io-ts | | 35 | | fp-ts | | 36 | | TypeScript | | 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Documentation" 3 | about: Improvements or suggestions of io-ts documentation 4 | --- 5 | 6 | ## 📖 Documentation 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F680Feature request" 3 | about: Suggest an idea for io-ts 4 | --- 5 | 6 | ## 🚀 Feature request 7 | 8 | ### Current Behavior 9 | 10 | 11 | 12 | ### Desired Behavior 13 | 14 | 15 | 16 | ### Suggested Solution 17 | 18 | 19 | 20 | 21 | 22 | ### Who does this impact? Who is this for? 23 | 24 | 25 | 26 | ### Describe alternatives you've considered 27 | 28 | 29 | 30 | ### Additional context 31 | 32 | 33 | 34 | ### Your environment 35 | 36 | 37 | 38 | | Software | Version(s) | 39 | | ---------- | ---------- | 40 | | io-ts | | 41 | | fp-ts | | 42 | | TypeScript | | 43 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Before submitting a pull request,** please make sure the following is done: 2 | 3 | - Fork [the repository](https://github.com/gcanti/io-ts) and create your branch from `master`. 4 | - Run `npm install` in the repository root. 5 | - If you've fixed a bug or added code that should be tested, add tests! 6 | - Ensure the test suite passes (`npm test`). 7 | 8 | **Note**. If you find a typo in the **documentation**, make sure to modify the corresponding source (docs are generated). 9 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [16.17.1] 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v1 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - run: npm install 27 | - run: npm run build --if-present 28 | - run: npm test 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | node_modules 3 | /dist 4 | dev 5 | coverage 6 | declaration/out/src 7 | .DS_Store 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "printWidth": 120, 5 | "trailingComma": "none" 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "./node_modules/typescript/lib", 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "eslint.validate": ["typescript"], 5 | "editor.codeActionsOnSave": { 6 | "source.fixAll.eslint": true 7 | }, 8 | "files.insertFinalNewline": true 9 | } 10 | -------------------------------------------------------------------------------- /Codec.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | - [Codec interface](#codec-interface) 5 | 6 | 7 | 8 | # Codec interface 9 | 10 | ```ts 11 | export interface Codec extends D.Decoder, E.Encoder {} 12 | ``` 13 | 14 | A codec is just a decoder and an encoder packed together. 15 | 16 | The following laws must hold 17 | 18 | 1. `pipe(codec.decode(u), E.fold(() => u, codec.encode)) = u` for all `u` in `unknown` 19 | 2. `codec.decode(codec.encode(a)) = E.right(a)` for all `a` in `A` 20 | 21 | You can build a new codec using the `make` helper 22 | 23 | **Example** 24 | 25 | ```ts 26 | import * as C from 'io-ts/Codec' 27 | import * as D from 'io-ts/Decoder' 28 | import * as E from 'io-ts/Encoder' 29 | import { pipe } from 'fp-ts/function' 30 | 31 | const decoder: D.Decoder = pipe( 32 | D.string, 33 | D.parse((s) => { 34 | const n = parseFloat(s) 35 | return isNaN(n) 36 | ? D.failure(s, `cannot decode ${JSON.stringify(s)}, should be parsable into a number`) 37 | : D.success(n) 38 | }) 39 | ) 40 | 41 | const encoder: E.Encoder = { 42 | encode: String 43 | } 44 | 45 | export const NumberFromString: C.Codec = C.make(decoder, encoder) 46 | ``` 47 | -------------------------------------------------------------------------------- /Encoder.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | - [Encoder interface](#encoder-interface) 5 | - [Extracting static types from encoders](#extracting-static-types-from-encoders) 6 | 7 | 8 | 9 | # Encoder interface 10 | 11 | ```ts 12 | export interface Encoder { 13 | readonly encode: (a: A) => O 14 | } 15 | ``` 16 | 17 | **Example** 18 | 19 | An encoder representing a nullable value 20 | 21 | ```ts 22 | import * as E from 'io-ts/Encoder' 23 | 24 | export function nullable(or: E.Encoder): E.Encoder { 25 | return { 26 | encode: (a) => (a === null ? null : or.encode(a)) 27 | } 28 | } 29 | ``` 30 | 31 | # Extracting static types from encoders 32 | 33 | Static types can be extracted from encoders using the `OutputOf` operator 34 | 35 | ```ts 36 | const NumberToString: E.Encoder = { 37 | encode: String 38 | } 39 | 40 | type MyOutputType = E.OutputOf 41 | /* 42 | type MyOutputType = string 43 | */ 44 | ``` 45 | -------------------------------------------------------------------------------- /Eq.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | - [Eq interface](#eq-interface) 5 | - [Built-in primitive eqs](#built-in-primitive-eqs) 6 | - [Combinators](#combinators) 7 | 8 | 9 | 10 | # Eq interface 11 | 12 | ```ts 13 | export interface Eq { 14 | readonly equals: (x: A, y: A) => boolean 15 | } 16 | ``` 17 | 18 | The `Eq` type class represents types which support decidable equality. 19 | 20 | Instances must satisfy the following laws: 21 | 22 | 1. Reflexivity: `E.equals(a, a) === true` 23 | 2. Symmetry: `E.equals(a, b) === E.equals(b, a)` 24 | 3. Transitivity: if `E.equals(a, b) === true` and `E.equals(b, c) === true`, then `E.equals(a, c) === true` 25 | 26 | **Example** 27 | 28 | ```ts 29 | import { Eq } from 'fp-ts/Eq' 30 | 31 | export const string: Eq = { 32 | equals: (x, y) => x === y 33 | } 34 | ``` 35 | 36 | # Built-in primitive eqs 37 | 38 | - `string: Eq` 39 | - `number: Eq` 40 | - `boolean: Eq` 41 | - `UnknownArray: Eq>` 42 | - `UnknownRecord: Eq>` 43 | 44 | # Combinators 45 | 46 | - `literal` 47 | - `nullable` 48 | - `type` 49 | - `partial` 50 | - `record` 51 | - `array` 52 | - `tuple` 53 | - `intersection` 54 | - `sum` 55 | - `lazy` 56 | 57 | **Example** 58 | 59 | ```ts 60 | import * as E from 'io-ts/Eq' 61 | 62 | const Person = E.type({ 63 | name: E.string, 64 | age: E.number 65 | }) 66 | 67 | console.log(Person.equals({ name: 'a', age: 0 }, { name: 'a', age: 0 })) // => true 68 | console.log(Person.equals({ name: 'a', age: 0 }, { name: '', age: 0 })) // => false 69 | console.log(Person.equals({ name: 'a', age: 0 }, { name: 'a', age: 1 })) // => false 70 | ``` 71 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Giulio Canti 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![build status](https://img.shields.io/travis/gcanti/io-ts/master.svg?style=flat-square)](https://travis-ci.org/gcanti/io-ts) 2 | ![npm downloads](https://img.shields.io/npm/dm/io-ts.svg) 3 | 4 | 5 | 6 | 7 | - [Installation](#installation) 8 | - [Usage](#usage) 9 | - [Stable features](#stable-features) 10 | - [Experimental features (version `2.2+`)](#experimental-features-version-22) 11 | 12 | 13 | 14 | # Installation 15 | 16 | To install the stable version 17 | 18 | ```sh 19 | npm i io-ts fp-ts 20 | ``` 21 | 22 | **Note**. [`fp-ts`](https://github.com/gcanti/fp-ts) is a peer dependency for `io-ts` 23 | 24 | # Usage 25 | 26 | 27 | ## Stable features 28 | 29 | - [Documentation of the main stable features (`index.ts` module)](index.md) 30 | 31 | ## Experimental modules (version `2.2+`) 32 | 33 | Experimental modules (\*) are published in order to get early feedback from the community, see these tracking [issues](https://github.com/gcanti/io-ts/issues?q=label%3Av2.2+) for further discussions and enhancements. 34 | 35 | The experimental modules are **independent and backward-incompatible** with stable ones. 36 | 37 | - [`Decoder.ts` module](Decoder.md) 38 | - [`Encoder.ts` module](Encoder.md) 39 | - [`Codec.ts` module](Codec.md) 40 | - [`Eq.ts` module](Eq.md) 41 | - [`Schema.ts` module (advanced feature)](Schema.md) 42 | 43 | (\*) A feature tagged as _Experimental_ is in a high state of flux, you're at risk of it changing without notice. 44 | -------------------------------------------------------------------------------- /Schema.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | - [Schema interface](#schema-interface) 5 | - [How to extend the built-in `Schema`](#how-to-extend-the-built-in-schema) 6 | 7 | 8 | 9 | # Schema interface 10 | 11 | ```ts 12 | export interface Schema { 13 | (S: Schemable): Kind 14 | } 15 | ``` 16 | 17 | `Schema` allows to define (through the `make` constructor) a generic schema once and then derive multiple concrete instances. 18 | 19 | **Example** 20 | 21 | ```ts 22 | import * as D from 'io-ts/Decoder' 23 | import * as Eq from 'io-ts/Eq' 24 | import * as G from 'io-ts/Guard' 25 | import * as S from 'io-ts/Schema' 26 | import * as TD from 'io-ts/TaskDecoder' 27 | 28 | export const Person = S.make((S) => 29 | S.struct({ 30 | name: S.string, 31 | age: S.number 32 | }) 33 | ) 34 | 35 | export const decoderPerson = S.interpreter(D.Schemable)(Person) 36 | export const guardPerson = S.interpreter(G.Schemable)(Person) 37 | export const eqPerson = S.interpreter(Eq.Schemable)(Person) 38 | export const taskDecoderPerson = S.interpreter(TD.Schemable)(Person) 39 | ``` 40 | 41 | # How to extend the built-in `Schema` 42 | 43 | Let's say we want to add an `Int` capability to the default `Schemable` type class. 44 | 45 | First of all we must represent an integer at the type level, I'll make use of a branded type for this 46 | 47 | ```ts 48 | export interface IntBrand { 49 | readonly Int: unique symbol 50 | } 51 | 52 | export type Int = number & IntBrand 53 | ``` 54 | 55 | Now we must define a custom `MySchemable` type class containing a new member `Int`... 56 | 57 | ```ts 58 | import { Kind, Kind2, URIS, URIS2, HKT } from 'fp-ts/HKT' 59 | import * as S from 'io-ts/Schemable' 60 | 61 | // base type class definition 62 | export interface MySchemable extends S.Schemable { 63 | readonly Int: HKT 64 | } 65 | 66 | // type class definition for * -> * constructors (e.g. `Eq`, `Guard`) 67 | export interface MySchemable1 extends S.Schemable1 { 68 | readonly Int: Kind 69 | } 70 | 71 | // type class definition for * -> * -> * constructors (e.g. `Decoder`, `Encoder`) 72 | export interface MySchemable2C extends S.Schemable2C { 73 | readonly Int: Kind2 74 | } 75 | ``` 76 | 77 | ...and a `make` constructor for our extended schemas 78 | 79 | ```ts 80 | export interface MySchema { 81 | (S: MySchemable): HKT 82 | } 83 | 84 | export function make(f: MySchema): MySchema { 85 | return S.memoize(f) 86 | } 87 | ``` 88 | 89 | Finally we must define an instance of `MySchemable2C` for `Decoder` and an interpreter 90 | 91 | ```ts 92 | import * as D from 'io-ts/Decoder' 93 | import { pipe, unsafeCoerce } from 'fp-ts/function' 94 | import * as SC from 'io-ts/Schema' 95 | 96 | export const mySchemable: MySchemable2C = { 97 | ...D.Schemable, 98 | Int: pipe( 99 | D.number, 100 | D.refine((n): n is Int => Number.isInteger(n), 'Int') 101 | ) 102 | } 103 | 104 | const interpreter: { 105 | (S: MySchemable2C): (schema: MySchema) => Kind2 106 | (S: MySchemable1): (schema: MySchema) => Kind 107 | } = unsafeCoerce(SC.interpreter) 108 | ``` 109 | 110 | Now we can define a schema leveraging the new `Int` capability 111 | 112 | ```ts 113 | export const Person = make((S) => 114 | S.struct({ 115 | name: S.string, 116 | age: S.Int 117 | }) 118 | ) 119 | /* 120 | const Person: MySchema<{ 121 | name: string; 122 | age: Int; 123 | }> 124 | */ 125 | ``` 126 | 127 | and get the corresponding decoder using the `mySchemable` instance 128 | 129 | ```ts 130 | export const decoderPerson = interpreter(mySchemable)(Person) 131 | /* 132 | const decoderPerson: D.Decoder 136 | */ 137 | ``` 138 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | remote_theme: pmarsceill/just-the-docs 2 | 3 | # Enable or disable the site search 4 | search_enabled: true 5 | 6 | # Aux links for the upper right navigation 7 | aux_links: 8 | 'io-ts on GitHub': 9 | - 'https://github.com/gcanti/io-ts' 10 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Home 3 | --- 4 | 5 | # Runtime type system for IO decoding/encoding 6 | 7 | ## The idea 8 | 9 | A value of type `Type` (called "codec") is the runtime representation of the static type `A`. 10 | 11 | A codec can: 12 | 13 | - decode inputs of type `I` (through `decode`) 14 | - encode outputs of type `O` (through `encode`) 15 | - be used as a custom [type guard](https://basarat.gitbook.io/typescript/type-system/typeguard) (through `is`) 16 | 17 | ```ts 18 | class Type { 19 | constructor( 20 | /** a unique name for this codec */ 21 | readonly name: string, 22 | 23 | /** a custom type guard */ 24 | readonly is: (u: unknown) => u is A, 25 | 26 | /** succeeds if a value of type I can be decoded to a value of type A */ 27 | readonly validate: (input: I, context: Context) => Either, 28 | 29 | /** converts a value of type A to a value of type O */ 30 | readonly encode: (a: A) => O 31 | ) {} 32 | 33 | /** a version of `validate` with a default context */ 34 | decode(i: I): Either 35 | } 36 | ``` 37 | 38 | 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. 39 | 40 | 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`: 41 | 42 | ```ts 43 | type Either = 44 | | { 45 | readonly _tag: 'Left' 46 | readonly left: E 47 | } 48 | | { 49 | readonly _tag: 'Right' 50 | readonly right: A 51 | } 52 | ``` 53 | 54 | Convention dictates that `Left` is used for **failure** and `Right` is used for **success**. 55 | 56 | **Example** 57 | 58 | A codec representing `string` can be defined as: 59 | 60 | ```ts 61 | import * as t from 'io-ts' 62 | 63 | const string = new t.Type( 64 | 'string', 65 | (input: unknown): input is string => typeof input === 'string', 66 | // `t.success` and `t.failure` are helpers used to build `Either` instances 67 | (input, context) => (typeof input === 'string' ? t.success(input) : t.failure(input, context)), 68 | // `A` and `O` are the same, so `encode` is just the identity function 69 | t.identity 70 | ) 71 | ``` 72 | 73 | and we can use it as follows: 74 | 75 | ```ts 76 | import { isRight } from 'fp-ts/lib/Either' 77 | 78 | isRight(string.decode('a string')) // true 79 | isRight(string.decode(null)) // false 80 | ``` 81 | 82 | 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) 83 | 84 | ```ts 85 | import * as t from 'io-ts' 86 | import { pipe } from 'fp-ts/lib/pipeable' 87 | import { fold } from 'fp-ts/lib/Either' 88 | 89 | // failure handler 90 | const onLeft = (errors: t.Errors): string => `${errors.length} error(s) found` 91 | 92 | // success handler 93 | const onRight = (s: string) => `No errors: ${s}` 94 | 95 | pipe(t.string.decode('a string'), fold(onLeft, onRight)) 96 | // => "No errors: a string" 97 | 98 | pipe(t.string.decode(null), fold(onLeft, onRight)) 99 | // => "1 error(s) found" 100 | ``` 101 | 102 | 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: 103 | 104 | ```ts 105 | import * as t from 'io-ts' 106 | 107 | const User = t.type({ 108 | userId: t.number, 109 | name: t.string 110 | }) 111 | ``` 112 | 113 | So this is equivalent to defining something like: 114 | 115 | ```ts 116 | type User = { 117 | userId: number 118 | name: string 119 | } 120 | ``` 121 | 122 | 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. 123 | 124 | Static types can be extracted from codecs using the `TypeOf` operator: 125 | 126 | ```ts 127 | type User = t.TypeOf 128 | 129 | // same as 130 | type User = { 131 | userId: number 132 | name: string 133 | } 134 | ``` 135 | 136 | ## Error reporters 137 | 138 | A reporter implements the following interface 139 | 140 | ```ts 141 | interface Reporter { 142 | report: (validation: Validation) => A 143 | } 144 | ``` 145 | 146 | This package exports a default `PathReporter` reporter 147 | 148 | Example 149 | 150 | ```ts 151 | import { PathReporter } from 'io-ts/lib/PathReporter' 152 | 153 | const result = User.decode({ name: 'Giulio' }) 154 | 155 | console.log(PathReporter.report(result)) 156 | // => [ 'Invalid value undefined supplied to : { userId: number, name: string }/userId: number' ] 157 | ``` 158 | -------------------------------------------------------------------------------- /docs/modules/DecodeError.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: DecodeError.ts 3 | nav_order: 2 4 | parent: Modules 5 | --- 6 | 7 | ## DecodeError overview 8 | 9 | **This module is experimental** 10 | 11 | Experimental features are published in order to get early feedback from the community, see these tracking 12 | [issues](https://github.com/gcanti/io-ts/issues?q=label%3Av2.2+) for further discussions and enhancements. 13 | 14 | A feature tagged as _Experimental_ is in a high state of flux, you're at risk of it changing without notice. 15 | 16 | Added in v2.2.7 17 | 18 | --- 19 | 20 |

Table of contents

21 | 22 | - [constructors](#constructors) 23 | - [index](#index) 24 | - [key](#key) 25 | - [lazy](#lazy) 26 | - [leaf](#leaf) 27 | - [member](#member) 28 | - [wrap](#wrap) 29 | - [destructors](#destructors) 30 | - [fold](#fold) 31 | - [instances](#instances) 32 | - [getSemigroup](#getsemigroup) 33 | - [model](#model) 34 | - [DecodeError (type alias)](#decodeerror-type-alias) 35 | - [Index (interface)](#index-interface) 36 | - [Key (interface)](#key-interface) 37 | - [Kind (type alias)](#kind-type-alias) 38 | - [Lazy (interface)](#lazy-interface) 39 | - [Leaf (interface)](#leaf-interface) 40 | - [Member (interface)](#member-interface) 41 | - [Wrap (interface)](#wrap-interface) 42 | - [optional](#optional) 43 | - [required](#required) 44 | 45 | --- 46 | 47 | # constructors 48 | 49 | ## index 50 | 51 | **Signature** 52 | 53 | ```ts 54 | export declare const index: (index: number, kind: Kind, errors: FS.FreeSemigroup>) => DecodeError 55 | ``` 56 | 57 | Added in v2.2.7 58 | 59 | ## key 60 | 61 | **Signature** 62 | 63 | ```ts 64 | export declare const key: (key: string, kind: Kind, errors: FS.FreeSemigroup>) => DecodeError 65 | ``` 66 | 67 | Added in v2.2.7 68 | 69 | ## lazy 70 | 71 | **Signature** 72 | 73 | ```ts 74 | export declare const lazy: (id: string, errors: FS.FreeSemigroup>) => DecodeError 75 | ``` 76 | 77 | Added in v2.2.7 78 | 79 | ## leaf 80 | 81 | **Signature** 82 | 83 | ```ts 84 | export declare const leaf: (actual: unknown, error: E) => DecodeError 85 | ``` 86 | 87 | Added in v2.2.7 88 | 89 | ## member 90 | 91 | **Signature** 92 | 93 | ```ts 94 | export declare const member: (index: number, errors: FS.FreeSemigroup>) => DecodeError 95 | ``` 96 | 97 | Added in v2.2.7 98 | 99 | ## wrap 100 | 101 | **Signature** 102 | 103 | ```ts 104 | export declare const wrap: (error: E, errors: FS.FreeSemigroup>) => DecodeError 105 | ``` 106 | 107 | Added in v2.2.9 108 | 109 | # destructors 110 | 111 | ## fold 112 | 113 | **Signature** 114 | 115 | ```ts 116 | export declare const fold: (patterns: { 117 | Leaf: (input: unknown, error: E) => R 118 | Key: (key: string, kind: Kind, errors: FS.FreeSemigroup>) => R 119 | Index: (index: number, kind: Kind, errors: FS.FreeSemigroup>) => R 120 | Member: (index: number, errors: FS.FreeSemigroup>) => R 121 | Lazy: (id: string, errors: FS.FreeSemigroup>) => R 122 | Wrap: (error: E, errors: FS.FreeSemigroup>) => R 123 | }) => (e: DecodeError) => R 124 | ``` 125 | 126 | Added in v2.2.7 127 | 128 | # instances 129 | 130 | ## getSemigroup 131 | 132 | **Signature** 133 | 134 | ```ts 135 | export declare function getSemigroup(): Semigroup>> 136 | ``` 137 | 138 | Added in v2.2.7 139 | 140 | # model 141 | 142 | ## DecodeError (type alias) 143 | 144 | **Signature** 145 | 146 | ```ts 147 | export type DecodeError = Leaf | Key | Index | Member | Lazy | Wrap 148 | ``` 149 | 150 | Added in v2.2.7 151 | 152 | ## Index (interface) 153 | 154 | **Signature** 155 | 156 | ```ts 157 | export interface Index { 158 | readonly _tag: 'Index' 159 | readonly index: number 160 | readonly kind: Kind 161 | readonly errors: FS.FreeSemigroup> 162 | } 163 | ``` 164 | 165 | Added in v2.2.7 166 | 167 | ## Key (interface) 168 | 169 | **Signature** 170 | 171 | ```ts 172 | export interface Key { 173 | readonly _tag: 'Key' 174 | readonly key: string 175 | readonly kind: Kind 176 | readonly errors: FS.FreeSemigroup> 177 | } 178 | ``` 179 | 180 | Added in v2.2.7 181 | 182 | ## Kind (type alias) 183 | 184 | **Signature** 185 | 186 | ```ts 187 | export type Kind = 'required' | 'optional' 188 | ``` 189 | 190 | Added in v2.2.7 191 | 192 | ## Lazy (interface) 193 | 194 | **Signature** 195 | 196 | ```ts 197 | export interface Lazy { 198 | readonly _tag: 'Lazy' 199 | readonly id: string 200 | readonly errors: FS.FreeSemigroup> 201 | } 202 | ``` 203 | 204 | Added in v2.2.7 205 | 206 | ## Leaf (interface) 207 | 208 | **Signature** 209 | 210 | ```ts 211 | export interface Leaf { 212 | readonly _tag: 'Leaf' 213 | readonly actual: unknown 214 | readonly error: E 215 | } 216 | ``` 217 | 218 | Added in v2.2.7 219 | 220 | ## Member (interface) 221 | 222 | **Signature** 223 | 224 | ```ts 225 | export interface Member { 226 | readonly _tag: 'Member' 227 | readonly index: number 228 | readonly errors: FS.FreeSemigroup> 229 | } 230 | ``` 231 | 232 | Added in v2.2.7 233 | 234 | ## Wrap (interface) 235 | 236 | **Signature** 237 | 238 | ```ts 239 | export interface Wrap { 240 | readonly _tag: 'Wrap' 241 | readonly error: E 242 | readonly errors: FS.FreeSemigroup> 243 | } 244 | ``` 245 | 246 | Added in v2.2.9 247 | 248 | ## optional 249 | 250 | **Signature** 251 | 252 | ```ts 253 | export declare const optional: 'optional' 254 | ``` 255 | 256 | Added in v2.2.7 257 | 258 | ## required 259 | 260 | **Signature** 261 | 262 | ```ts 263 | export declare const required: 'required' 264 | ``` 265 | 266 | Added in v2.2.7 267 | -------------------------------------------------------------------------------- /docs/modules/Encoder.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Encoder.ts 3 | nav_order: 4 4 | parent: Modules 5 | --- 6 | 7 | ## Encoder overview 8 | 9 | **This module is experimental** 10 | 11 | Experimental features are published in order to get early feedback from the community, see these tracking 12 | [issues](https://github.com/gcanti/io-ts/issues?q=label%3Av2.2+) for further discussions and enhancements. 13 | 14 | A feature tagged as _Experimental_ is in a high state of flux, you're at risk of it changing without notice. 15 | 16 | Added in v2.2.3 17 | 18 | --- 19 | 20 |

Table of contents

21 | 22 | - [Category](#category) 23 | - [id](#id) 24 | - [Contravariant](#contravariant) 25 | - [contramap](#contramap) 26 | - [Semigroupoid](#semigroupoid) 27 | - [compose](#compose) 28 | - [combinators](#combinators) 29 | - [array](#array) 30 | - [intersect](#intersect) 31 | - [lazy](#lazy) 32 | - [nullable](#nullable) 33 | - [partial](#partial) 34 | - [readonly](#readonly) 35 | - [record](#record) 36 | - [struct](#struct) 37 | - [sum](#sum) 38 | - [tuple](#tuple) 39 | - [~~type~~](#type) 40 | - [instances](#instances) 41 | - [Category](#category-1) 42 | - [Contravariant](#contravariant-1) 43 | - [URI](#uri) 44 | - [URI (type alias)](#uri-type-alias) 45 | - [model](#model) 46 | - [Encoder (interface)](#encoder-interface) 47 | - [utils](#utils) 48 | - [OutputOf (type alias)](#outputof-type-alias) 49 | - [TypeOf (type alias)](#typeof-type-alias) 50 | 51 | --- 52 | 53 | # Category 54 | 55 | ## id 56 | 57 | **Signature** 58 | 59 | ```ts 60 | export declare function id
(): Encoder 61 | ``` 62 | 63 | Added in v2.2.3 64 | 65 | # Contravariant 66 | 67 | ## contramap 68 | 69 | **Signature** 70 | 71 | ```ts 72 | export declare const contramap: (f: (b: B) => A) => (fa: Encoder) => Encoder 73 | ``` 74 | 75 | Added in v2.2.3 76 | 77 | # Semigroupoid 78 | 79 | ## compose 80 | 81 | **Signature** 82 | 83 | ```ts 84 | export declare const compose: (ea: Encoder) => (ab: Encoder) => Encoder 85 | ``` 86 | 87 | Added in v2.2.3 88 | 89 | # combinators 90 | 91 | ## array 92 | 93 | **Signature** 94 | 95 | ```ts 96 | export declare function array(item: Encoder): Encoder, Array> 97 | ``` 98 | 99 | Added in v2.2.3 100 | 101 | ## intersect 102 | 103 | **Signature** 104 | 105 | ```ts 106 | export declare const intersect: (right: Encoder) => (left: Encoder) => Encoder 107 | ``` 108 | 109 | Added in v2.2.3 110 | 111 | ## lazy 112 | 113 | **Signature** 114 | 115 | ```ts 116 | export declare function lazy(f: () => Encoder): Encoder 117 | ``` 118 | 119 | Added in v2.2.3 120 | 121 | ## nullable 122 | 123 | **Signature** 124 | 125 | ```ts 126 | export declare function nullable(or: Encoder): Encoder 127 | ``` 128 | 129 | Added in v2.2.3 130 | 131 | ## partial 132 | 133 | **Signature** 134 | 135 | ```ts 136 | export declare function partial

>>( 137 | properties: P 138 | ): Encoder }>, Partial<{ [K in keyof P]: TypeOf }>> 139 | ``` 140 | 141 | Added in v2.2.3 142 | 143 | ## readonly 144 | 145 | **Signature** 146 | 147 | ```ts 148 | export declare const readonly: (decoder: Encoder) => Encoder> 149 | ``` 150 | 151 | Added in v2.2.16 152 | 153 | ## record 154 | 155 | **Signature** 156 | 157 | ```ts 158 | export declare function record(codomain: Encoder): Encoder, Record> 159 | ``` 160 | 161 | Added in v2.2.3 162 | 163 | ## struct 164 | 165 | **Signature** 166 | 167 | ```ts 168 | export declare function struct

>>( 169 | properties: P 170 | ): Encoder<{ [K in keyof P]: OutputOf }, { [K in keyof P]: TypeOf }> 171 | ``` 172 | 173 | Added in v2.2.15 174 | 175 | ## sum 176 | 177 | **Signature** 178 | 179 | ```ts 180 | export declare function sum( 181 | tag: T 182 | ): >>(members: MS) => Encoder, TypeOf> 183 | ``` 184 | 185 | Added in v2.2.3 186 | 187 | ## tuple 188 | 189 | **Signature** 190 | 191 | ```ts 192 | export declare function tuple>>( 193 | ...components: C 194 | ): Encoder<{ [K in keyof C]: OutputOf }, { [K in keyof C]: TypeOf }> 195 | ``` 196 | 197 | Added in v2.2.3 198 | 199 | ## ~~type~~ 200 | 201 | Use `struct` instead. 202 | 203 | **Signature** 204 | 205 | ```ts 206 | export declare const type: typeof struct 207 | ``` 208 | 209 | Added in v2.2.3 210 | 211 | # instances 212 | 213 | ## Category 214 | 215 | **Signature** 216 | 217 | ```ts 218 | export declare const Category: Category2<'io-ts/Encoder'> 219 | ``` 220 | 221 | Added in v2.2.8 222 | 223 | ## Contravariant 224 | 225 | **Signature** 226 | 227 | ```ts 228 | export declare const Contravariant: Contravariant2<'io-ts/Encoder'> 229 | ``` 230 | 231 | Added in v2.2.8 232 | 233 | ## URI 234 | 235 | **Signature** 236 | 237 | ```ts 238 | export declare const URI: 'io-ts/Encoder' 239 | ``` 240 | 241 | Added in v2.2.3 242 | 243 | ## URI (type alias) 244 | 245 | **Signature** 246 | 247 | ```ts 248 | export type URI = typeof URI 249 | ``` 250 | 251 | Added in v2.2.3 252 | 253 | # model 254 | 255 | ## Encoder (interface) 256 | 257 | **Signature** 258 | 259 | ```ts 260 | export interface Encoder { 261 | readonly encode: (a: A) => O 262 | } 263 | ``` 264 | 265 | Added in v2.2.3 266 | 267 | # utils 268 | 269 | ## OutputOf (type alias) 270 | 271 | **Signature** 272 | 273 | ```ts 274 | export type OutputOf = E extends Encoder ? O : never 275 | ``` 276 | 277 | Added in v2.2.3 278 | 279 | ## TypeOf (type alias) 280 | 281 | **Signature** 282 | 283 | ```ts 284 | export type TypeOf = E extends Encoder ? A : never 285 | ``` 286 | 287 | Added in v2.2.3 288 | -------------------------------------------------------------------------------- /docs/modules/Eq.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Eq.ts 3 | nav_order: 5 4 | parent: Modules 5 | --- 6 | 7 | ## Eq overview 8 | 9 | **This module is experimental** 10 | 11 | Experimental features are published in order to get early feedback from the community, see these tracking 12 | [issues](https://github.com/gcanti/io-ts/issues?q=label%3Av2.2+) for further discussions and enhancements. 13 | 14 | A feature tagged as _Experimental_ is in a high state of flux, you're at risk of it changing without notice. 15 | 16 | Added in v2.2.2 17 | 18 | --- 19 | 20 |

Table of contents

21 | 22 | - [combinators](#combinators) 23 | - [array](#array) 24 | - [intersect](#intersect) 25 | - [lazy](#lazy) 26 | - [nullable](#nullable) 27 | - [partial](#partial) 28 | - [readonly](#readonly) 29 | - [record](#record) 30 | - [struct](#struct) 31 | - [sum](#sum) 32 | - [tuple](#tuple) 33 | - [~~type~~](#type) 34 | - [instances](#instances) 35 | - [Schemable](#schemable) 36 | - [WithRefine](#withrefine) 37 | - [WithUnknownContainers](#withunknowncontainers) 38 | - [primitives](#primitives) 39 | - [UnknownArray](#unknownarray) 40 | - [UnknownRecord](#unknownrecord) 41 | - [boolean](#boolean) 42 | - [number](#number) 43 | - [string](#string) 44 | - [utils](#utils) 45 | - [TypeOf (type alias)](#typeof-type-alias) 46 | - [URI (type alias)](#uri-type-alias) 47 | 48 | --- 49 | 50 | # combinators 51 | 52 | ## array 53 | 54 | **Signature** 55 | 56 | ```ts 57 | export declare const array:
(eq: E.Eq) => E.Eq 58 | ``` 59 | 60 | Added in v2.2.2 61 | 62 | ## intersect 63 | 64 | **Signature** 65 | 66 | ```ts 67 | export declare const intersect: (right: E.Eq) => (left: E.Eq) => E.Eq 68 | ``` 69 | 70 | Added in v2.2.2 71 | 72 | ## lazy 73 | 74 | **Signature** 75 | 76 | ```ts 77 | export declare function lazy(f: () => Eq): Eq 78 | ``` 79 | 80 | Added in v2.2.2 81 | 82 | ## nullable 83 | 84 | **Signature** 85 | 86 | ```ts 87 | export declare function nullable(or: Eq): Eq 88 | ``` 89 | 90 | Added in v2.2.2 91 | 92 | ## partial 93 | 94 | **Signature** 95 | 96 | ```ts 97 | export declare function partial(properties: { [K in keyof A]: Eq }): Eq> 98 | ``` 99 | 100 | Added in v2.2.2 101 | 102 | ## readonly 103 | 104 | **Signature** 105 | 106 | ```ts 107 | export declare const readonly: (eq: E.Eq) => E.Eq> 108 | ``` 109 | 110 | Added in v2.2.15 111 | 112 | ## record 113 | 114 | **Signature** 115 | 116 | ```ts 117 | export declare const record: (codomain: E.Eq) => E.Eq> 118 | ``` 119 | 120 | Added in v2.2.2 121 | 122 | ## struct 123 | 124 | **Signature** 125 | 126 | ```ts 127 | export declare const struct: (eqs: { [K in keyof A]: E.Eq }) => E.Eq<{ [K in keyof A]: A[K] }> 128 | ``` 129 | 130 | Added in v2.2.15 131 | 132 | ## sum 133 | 134 | **Signature** 135 | 136 | ```ts 137 | export declare function sum( 138 | tag: T 139 | ): (members: { [K in keyof A]: Eq> }) => Eq 140 | ``` 141 | 142 | Added in v2.2.2 143 | 144 | ## tuple 145 | 146 | **Signature** 147 | 148 | ```ts 149 | export declare const tuple: (...components: { [K in keyof A]: E.Eq }) => E.Eq 150 | ``` 151 | 152 | Added in v2.2.2 153 | 154 | ## ~~type~~ 155 | 156 | Use `struct` instead. 157 | 158 | **Signature** 159 | 160 | ```ts 161 | export declare const type: (eqs: { [K in keyof A]: E.Eq }) => E.Eq<{ [K in keyof A]: A[K] }> 162 | ``` 163 | 164 | Added in v2.2.2 165 | 166 | # instances 167 | 168 | ## Schemable 169 | 170 | **Signature** 171 | 172 | ```ts 173 | export declare const Schemable: Schemable1<'Eq'> 174 | ``` 175 | 176 | Added in v2.2.8 177 | 178 | ## WithRefine 179 | 180 | **Signature** 181 | 182 | ```ts 183 | export declare const WithRefine: WithRefine1<'Eq'> 184 | ``` 185 | 186 | Added in v2.2.8 187 | 188 | ## WithUnknownContainers 189 | 190 | **Signature** 191 | 192 | ```ts 193 | export declare const WithUnknownContainers: WithUnknownContainers1<'Eq'> 194 | ``` 195 | 196 | Added in v2.2.8 197 | 198 | # primitives 199 | 200 | ## UnknownArray 201 | 202 | **Signature** 203 | 204 | ```ts 205 | export declare const UnknownArray: E.Eq 206 | ``` 207 | 208 | Added in v2.2.2 209 | 210 | ## UnknownRecord 211 | 212 | **Signature** 213 | 214 | ```ts 215 | export declare const UnknownRecord: E.Eq> 216 | ``` 217 | 218 | Added in v2.2.2 219 | 220 | ## boolean 221 | 222 | **Signature** 223 | 224 | ```ts 225 | export declare const boolean: E.Eq 226 | ``` 227 | 228 | Added in v2.2.2 229 | 230 | ## number 231 | 232 | **Signature** 233 | 234 | ```ts 235 | export declare const number: E.Eq 236 | ``` 237 | 238 | Added in v2.2.2 239 | 240 | ## string 241 | 242 | **Signature** 243 | 244 | ```ts 245 | export declare const string: E.Eq 246 | ``` 247 | 248 | Added in v2.2.2 249 | 250 | # utils 251 | 252 | ## TypeOf (type alias) 253 | 254 | **Signature** 255 | 256 | ```ts 257 | export type TypeOf = E extends Eq ? A : never 258 | ``` 259 | 260 | Added in v2.2.2 261 | 262 | ## URI (type alias) 263 | 264 | **Signature** 265 | 266 | ```ts 267 | export type URI = E.URI 268 | ``` 269 | 270 | Added in v2.2.3 271 | -------------------------------------------------------------------------------- /docs/modules/FreeSemigroup.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: FreeSemigroup.ts 3 | nav_order: 6 4 | parent: Modules 5 | --- 6 | 7 | ## FreeSemigroup overview 8 | 9 | **This module is experimental** 10 | 11 | Experimental features are published in order to get early feedback from the community, see these tracking 12 | [issues](https://github.com/gcanti/io-ts/issues?q=label%3Av2.2+) for further discussions and enhancements. 13 | 14 | A feature tagged as _Experimental_ is in a high state of flux, you're at risk of it changing without notice. 15 | 16 | Added in v2.2.7 17 | 18 | --- 19 | 20 |

Table of contents

21 | 22 | - [constructors](#constructors) 23 | - [concat](#concat) 24 | - [of](#of) 25 | - [destructors](#destructors) 26 | - [fold](#fold) 27 | - [instances](#instances) 28 | - [getSemigroup](#getsemigroup) 29 | - [model](#model) 30 | - [Concat (interface)](#concat-interface) 31 | - [FreeSemigroup (type alias)](#freesemigroup-type-alias) 32 | - [Of (interface)](#of-interface) 33 | 34 | --- 35 | 36 | # constructors 37 | 38 | ## concat 39 | 40 | **Signature** 41 | 42 | ```ts 43 | export declare const concat:
(left: FreeSemigroup, right: FreeSemigroup) => FreeSemigroup 44 | ``` 45 | 46 | Added in v2.2.7 47 | 48 | ## of 49 | 50 | **Signature** 51 | 52 | ```ts 53 | export declare const of: (a: A) => FreeSemigroup 54 | ``` 55 | 56 | Added in v2.2.7 57 | 58 | # destructors 59 | 60 | ## fold 61 | 62 | **Signature** 63 | 64 | ```ts 65 | export declare const fold: ( 66 | onOf: (value: A) => R, 67 | onConcat: (left: FreeSemigroup, right: FreeSemigroup) => R 68 | ) => (f: FreeSemigroup) => R 69 | ``` 70 | 71 | Added in v2.2.7 72 | 73 | # instances 74 | 75 | ## getSemigroup 76 | 77 | **Signature** 78 | 79 | ```ts 80 | export declare function getSemigroup(): Semigroup> 81 | ``` 82 | 83 | Added in v2.2.7 84 | 85 | # model 86 | 87 | ## Concat (interface) 88 | 89 | **Signature** 90 | 91 | ```ts 92 | export interface Concat { 93 | readonly _tag: 'Concat' 94 | readonly left: FreeSemigroup 95 | readonly right: FreeSemigroup 96 | } 97 | ``` 98 | 99 | Added in v2.2.7 100 | 101 | ## FreeSemigroup (type alias) 102 | 103 | **Signature** 104 | 105 | ```ts 106 | export type FreeSemigroup = Of | Concat 107 | ``` 108 | 109 | Added in v2.2.7 110 | 111 | ## Of (interface) 112 | 113 | **Signature** 114 | 115 | ```ts 116 | export interface Of { 117 | readonly _tag: 'Of' 118 | readonly value: A 119 | } 120 | ``` 121 | 122 | Added in v2.2.7 123 | -------------------------------------------------------------------------------- /docs/modules/PathReporter.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: PathReporter.ts 3 | nav_order: 10 4 | parent: Modules 5 | --- 6 | 7 | ## PathReporter overview 8 | 9 | Added in v1.0.0 10 | 11 | --- 12 | 13 |

Table of contents

14 | 15 | - [utils](#utils) 16 | - [PathReporter](#pathreporter) 17 | - [failure](#failure) 18 | - [success](#success) 19 | 20 | --- 21 | 22 | # utils 23 | 24 | ## PathReporter 25 | 26 | **Signature** 27 | 28 | ```ts 29 | export declare const PathReporter: Reporter 30 | ``` 31 | 32 | Added in v1.0.0 33 | 34 | ## failure 35 | 36 | **Signature** 37 | 38 | ```ts 39 | export declare function failure(es: Array): Array 40 | ``` 41 | 42 | Added in v1.0.0 43 | 44 | ## success 45 | 46 | **Signature** 47 | 48 | ```ts 49 | export declare function success(): Array 50 | ``` 51 | 52 | Added in v1.0.0 53 | -------------------------------------------------------------------------------- /docs/modules/Reporter.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Reporter.ts 3 | nav_order: 11 4 | parent: Modules 5 | --- 6 | 7 | ## Reporter overview 8 | 9 | Added in v1.0.0 10 | 11 | --- 12 | 13 |

Table of contents

14 | 15 | - [utils](#utils) 16 | - [Reporter (interface)](#reporter-interface) 17 | 18 | --- 19 | 20 | # utils 21 | 22 | ## Reporter (interface) 23 | 24 | **Signature** 25 | 26 | ```ts 27 | export interface Reporter
{ 28 | report: (validation: Validation) => A 29 | } 30 | ``` 31 | 32 | Added in v1.0.0 33 | -------------------------------------------------------------------------------- /docs/modules/Schema.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Schema.ts 3 | nav_order: 12 4 | parent: Modules 5 | --- 6 | 7 | ## Schema overview 8 | 9 | **This module is experimental** 10 | 11 | Experimental features are published in order to get early feedback from the community, see these tracking 12 | [issues](https://github.com/gcanti/io-ts/issues?q=label%3Av2.2+) for further discussions and enhancements. 13 | 14 | A feature tagged as _Experimental_ is in a high state of flux, you're at risk of it changing without notice. 15 | 16 | Added in v2.2.0 17 | 18 | --- 19 | 20 |

Table of contents

21 | 22 | - [constructors](#constructors) 23 | - [make](#make) 24 | - [model](#model) 25 | - [Schema (interface)](#schema-interface) 26 | - [utils](#utils) 27 | - [TypeOf (type alias)](#typeof-type-alias) 28 | - [interpreter](#interpreter) 29 | 30 | --- 31 | 32 | # constructors 33 | 34 | ## make 35 | 36 | **Signature** 37 | 38 | ```ts 39 | export declare function make
(schema: Schema): Schema 40 | ``` 41 | 42 | Added in v2.2.0 43 | 44 | # model 45 | 46 | ## Schema (interface) 47 | 48 | **Signature** 49 | 50 | ```ts 51 | export interface Schema { 52 | (S: Schemable): HKT 53 | } 54 | ``` 55 | 56 | Added in v2.2.0 57 | 58 | # utils 59 | 60 | ## TypeOf (type alias) 61 | 62 | **Signature** 63 | 64 | ```ts 65 | export type TypeOf = S extends Schema ? A : never 66 | ``` 67 | 68 | Added in v2.2.0 69 | 70 | ## interpreter 71 | 72 | **Signature** 73 | 74 | ```ts 75 | export declare function interpreter( 76 | S: Schemable2C 77 | ): (schema: Schema) => Kind2 78 | export declare function interpreter(S: Schemable1): (schema: Schema) => Kind 79 | ``` 80 | 81 | Added in v2.2.3 82 | -------------------------------------------------------------------------------- /docs/modules/Type.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Type.ts 3 | nav_order: 15 4 | parent: Modules 5 | --- 6 | 7 | ## Type overview 8 | 9 | **This module is experimental** 10 | 11 | Experimental features are published in order to get early feedback from the community, see these tracking 12 | [issues](https://github.com/gcanti/io-ts/issues?q=label%3Av2.2+) for further discussions and enhancements. 13 | 14 | A feature tagged as _Experimental_ is in a high state of flux, you're at risk of it changing without notice. 15 | 16 | Added in v2.2.3 17 | 18 | --- 19 | 20 |

Table of contents

21 | 22 | - [combinators](#combinators) 23 | - [array](#array) 24 | - [intersect](#intersect) 25 | - [lazy](#lazy) 26 | - [nullable](#nullable) 27 | - [partial](#partial) 28 | - [readonly](#readonly) 29 | - [record](#record) 30 | - [refine](#refine) 31 | - [struct](#struct) 32 | - [sum](#sum) 33 | - [tuple](#tuple) 34 | - [union](#union) 35 | - [~~type~~](#type) 36 | - [constructors](#constructors) 37 | - [literal](#literal) 38 | - [instances](#instances) 39 | - [Schemable](#schemable) 40 | - [URI](#uri) 41 | - [URI (type alias)](#uri-type-alias) 42 | - [WithRefine](#withrefine) 43 | - [WithUnion](#withunion) 44 | - [WithUnknownContainers](#withunknowncontainers) 45 | - [model](#model) 46 | - [Type (interface)](#type-interface) 47 | - [primitives](#primitives) 48 | - [UnknownArray](#unknownarray) 49 | - [UnknownRecord](#unknownrecord) 50 | - [boolean](#boolean) 51 | - [number](#number) 52 | - [string](#string) 53 | 54 | --- 55 | 56 | # combinators 57 | 58 | ## array 59 | 60 | **Signature** 61 | 62 | ```ts 63 | export declare const array:
(item: Type) => Type 64 | ``` 65 | 66 | Added in v2.2.3 67 | 68 | ## intersect 69 | 70 | **Signature** 71 | 72 | ```ts 73 | export declare const intersect: (right: Type) => (left: Type) => Type 74 | ``` 75 | 76 | Added in v2.2.3 77 | 78 | ## lazy 79 | 80 | **Signature** 81 | 82 | ```ts 83 | export declare const lazy: (id: string, f: () => Type) => Type 84 | ``` 85 | 86 | Added in v2.2.3 87 | 88 | ## nullable 89 | 90 | **Signature** 91 | 92 | ```ts 93 | export declare const nullable: (or: Type) => Type 94 | ``` 95 | 96 | Added in v2.2.3 97 | 98 | ## partial 99 | 100 | **Signature** 101 | 102 | ```ts 103 | export declare const partial: (properties: { [K in keyof A]: Type }) => Type> 104 | ``` 105 | 106 | Added in v2.2.3 107 | 108 | ## readonly 109 | 110 | **Signature** 111 | 112 | ```ts 113 | export declare const readonly: (type: Type) => Type> 114 | ``` 115 | 116 | Added in v2.2.15 117 | 118 | ## record 119 | 120 | **Signature** 121 | 122 | ```ts 123 | export declare const record: (codomain: Type) => Type> 124 | ``` 125 | 126 | Added in v2.2.3 127 | 128 | ## refine 129 | 130 | **Signature** 131 | 132 | ```ts 133 | export declare const refine: (refinement: Refinement, id: string) => (from: Type) => Type 134 | ``` 135 | 136 | Added in v2.2.3 137 | 138 | ## struct 139 | 140 | **Signature** 141 | 142 | ```ts 143 | export declare const struct: (properties: { [K in keyof A]: Type }) => Type<{ [K in keyof A]: A[K] }> 144 | ``` 145 | 146 | Added in v2.2.15 147 | 148 | ## sum 149 | 150 | **Signature** 151 | 152 | ```ts 153 | export declare const sum: ( 154 | _tag: T 155 | ) => (members: { [K in keyof A]: Type> }) => Type 156 | ``` 157 | 158 | Added in v2.2.3 159 | 160 | ## tuple 161 | 162 | **Signature** 163 | 164 | ```ts 165 | export declare const tuple: (...components: { [K in keyof A]: Type }) => Type 166 | ``` 167 | 168 | Added in v2.2.3 169 | 170 | ## union 171 | 172 | **Signature** 173 | 174 | ```ts 175 | export declare const union: ( 176 | ...members: { [K in keyof A]: Type } 177 | ) => Type 178 | ``` 179 | 180 | Added in v2.2.3 181 | 182 | ## ~~type~~ 183 | 184 | Use `struct` instead. 185 | 186 | **Signature** 187 | 188 | ```ts 189 | export declare const type: (properties: { [K in keyof A]: Type }) => Type<{ [K in keyof A]: A[K] }> 190 | ``` 191 | 192 | Added in v2.2.3 193 | 194 | # constructors 195 | 196 | ## literal 197 | 198 | **Signature** 199 | 200 | ```ts 201 | export declare const literal: ( 202 | ...values: A 203 | ) => Type 204 | ``` 205 | 206 | Added in v2.2.3 207 | 208 | # instances 209 | 210 | ## Schemable 211 | 212 | **Signature** 213 | 214 | ```ts 215 | export declare const Schemable: S.Schemable1<'io-ts/Type'> 216 | ``` 217 | 218 | Added in v2.2.8 219 | 220 | ## URI 221 | 222 | **Signature** 223 | 224 | ```ts 225 | export declare const URI: 'io-ts/Type' 226 | ``` 227 | 228 | Added in v2.2.3 229 | 230 | ## URI (type alias) 231 | 232 | **Signature** 233 | 234 | ```ts 235 | export type URI = typeof URI 236 | ``` 237 | 238 | Added in v2.2.3 239 | 240 | ## WithRefine 241 | 242 | **Signature** 243 | 244 | ```ts 245 | export declare const WithRefine: S.WithRefine1<'io-ts/Type'> 246 | ``` 247 | 248 | Added in v2.2.8 249 | 250 | ## WithUnion 251 | 252 | **Signature** 253 | 254 | ```ts 255 | export declare const WithUnion: S.WithUnion1<'io-ts/Type'> 256 | ``` 257 | 258 | Added in v2.2.8 259 | 260 | ## WithUnknownContainers 261 | 262 | **Signature** 263 | 264 | ```ts 265 | export declare const WithUnknownContainers: S.WithUnknownContainers1<'io-ts/Type'> 266 | ``` 267 | 268 | Added in v2.2.8 269 | 270 | # model 271 | 272 | ## Type (interface) 273 | 274 | **Signature** 275 | 276 | ```ts 277 | export interface Type extends t.Type {} 278 | ``` 279 | 280 | Added in v2.2.3 281 | 282 | # primitives 283 | 284 | ## UnknownArray 285 | 286 | **Signature** 287 | 288 | ```ts 289 | export declare const UnknownArray: Type 290 | ``` 291 | 292 | Added in v2.2.3 293 | 294 | ## UnknownRecord 295 | 296 | **Signature** 297 | 298 | ```ts 299 | export declare const UnknownRecord: Type> 300 | ``` 301 | 302 | Added in v2.2.3 303 | 304 | ## boolean 305 | 306 | **Signature** 307 | 308 | ```ts 309 | export declare const boolean: Type 310 | ``` 311 | 312 | Added in v2.2.3 313 | 314 | ## number 315 | 316 | **Signature** 317 | 318 | ```ts 319 | export declare const number: Type 320 | ``` 321 | 322 | Added in v2.2.3 323 | 324 | ## string 325 | 326 | **Signature** 327 | 328 | ```ts 329 | export declare const string: Type 330 | ``` 331 | 332 | Added in v2.2.3 333 | -------------------------------------------------------------------------------- /docs/modules/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Modules 3 | has_children: true 4 | permalink: /docs/modules 5 | nav_order: 2 6 | --- 7 | -------------------------------------------------------------------------------- /dtslint/Codec.ts: -------------------------------------------------------------------------------- 1 | import * as _ from '../src/Codec' 2 | 3 | declare const NumberFromString: _.Codec 4 | 5 | // 6 | // fromStruct 7 | // 8 | 9 | // $ExpectType Codec<{ a: unknown; b: { c: string; }; }, { a: string; b: { c: string; }; }, { a: string; b: { c: number; }; }> 10 | _.fromStruct({ 11 | a: _.string, 12 | b: _.fromStruct({ 13 | c: NumberFromString 14 | }) 15 | }) 16 | 17 | // 18 | // struct 19 | // 20 | 21 | // $ExpectType Codec 22 | _.struct({ 23 | a: _.string, 24 | b: _.struct({ 25 | c: _.number 26 | }) 27 | }) 28 | 29 | // 30 | // fromPartial 31 | // 32 | 33 | // $ExpectType Codec; }>, Partial<{ a: string; b: Partial<{ c: string; }>; }>, Partial<{ a: string; b: Partial<{ c: number; }>; }>> 34 | _.fromPartial({ 35 | a: _.string, 36 | b: _.fromPartial({ 37 | c: NumberFromString 38 | }) 39 | }) 40 | 41 | // 42 | // partial 43 | // 44 | 45 | // $ExpectType Codec; }>, Partial<{ a: string; b: Partial<{ c: number; }>; }>> 46 | _.partial({ 47 | a: _.string, 48 | b: _.partial({ 49 | c: _.number 50 | }) 51 | }) 52 | 53 | // 54 | // fromArray 55 | // 56 | 57 | // $ExpectType Codec 58 | _.fromArray(NumberFromString) 59 | 60 | // 61 | // array 62 | // 63 | 64 | // $ExpectType Codec 65 | _.array(_.string) 66 | 67 | // 68 | // fromRecord 69 | // 70 | 71 | // $ExpectType Codec, Record, Record> 72 | _.fromRecord(NumberFromString) 73 | 74 | // 75 | // record 76 | // 77 | 78 | // $ExpectType Codec, Record> 79 | _.record(_.string) 80 | 81 | // 82 | // fromTuple 83 | // 84 | 85 | // $ExpectType Codec<[unknown, string, unknown], [string, string, boolean], [string, number, boolean]> 86 | _.fromTuple(_.string, NumberFromString, _.boolean) 87 | 88 | // 89 | // tuple 90 | // 91 | 92 | // $ExpectType Codec 93 | _.tuple(_.string, _.number, _.boolean) 94 | 95 | // 96 | // fromSum 97 | // 98 | 99 | // $ExpectType Codec<{ _tag: unknown; a: unknown; } | { _tag: unknown; b: string; }, { _tag: "A"; a: string; } | { _tag: "B"; b: string; }, { _tag: "A"; a: string; } | { _tag: "B"; b: number; }> 100 | _.fromSum('_tag')({ 101 | A: _.fromStruct({ _tag: _.literal('A'), a: _.string }), 102 | B: _.fromStruct({ _tag: _.literal('B'), b: NumberFromString }) 103 | }) 104 | 105 | // 106 | // sum 107 | // 108 | 109 | const S1 = _.struct({ _tag: _.literal('A'), a: _.string }) 110 | const S2 = _.struct({ _tag: _.literal('B'), b: _.number }) 111 | 112 | // $ExpectType Codec 113 | _.sum('_tag')({ A: S1, B: S2 }) 114 | // // @ts-expect-error 115 | // _.sum('_tag')({ A: S1, B: S1 }) 116 | -------------------------------------------------------------------------------- /dtslint/Decoder.ts: -------------------------------------------------------------------------------- 1 | import { pipe } from 'fp-ts/lib/pipeable' 2 | 3 | import * as DE from '../src/DecodeError' 4 | import * as _ from '../src/Decoder' 5 | import * as FS from '../src/FreeSemigroup' 6 | import * as S from '../src/Schemable' 7 | 8 | declare const NumberFromString: _.Decoder 9 | 10 | // 11 | // TypeOf 12 | // 13 | 14 | // $ExpectType number 15 | export type TypeOfNumberFromString = _.TypeOf 16 | 17 | // 18 | // InputOf 19 | // 20 | 21 | // $ExpectType string 22 | export type InputOfNumberFromString = _.InputOf 23 | 24 | // 25 | // literal 26 | // 27 | 28 | // $ExpectType Decoder 29 | _.literal('A') 30 | 31 | // $ExpectType Decoder 32 | _.literal('A', 'B') 33 | 34 | // $ExpectType Decoder 35 | _.union(_.literal('A')) 36 | 37 | // $ExpectType Decoder 38 | _.union(_.literal('A'), _.literal('B')) 39 | 40 | // $ExpectType Decoder 41 | _.struct({ 42 | a: _.literal('A') 43 | }) 44 | 45 | // $ExpectType Decoder 46 | pipe( 47 | _.struct({ 48 | a: _.literal('A') 49 | }), 50 | _.intersect( 51 | _.struct({ 52 | b: _.literal('B') 53 | }) 54 | ) 55 | ) 56 | 57 | declare const literal: ], L extends S.Literal = S.Literal>( 58 | ...values: A 59 | ) => _.Decoder 60 | // $ExpectType Decoder 61 | _.struct({ 62 | a: literal('A') 63 | }) 64 | 65 | // 66 | // fromStruct 67 | // 68 | 69 | // $ExpectType Decoder<{ a: unknown; b: { c: string; }; }, { a: string; b: { c: number; }; }> 70 | _.fromStruct({ 71 | a: _.string, 72 | b: _.fromStruct({ 73 | c: NumberFromString 74 | }) 75 | }) 76 | 77 | // 78 | // struct 79 | // 80 | 81 | // $ExpectType Decoder 82 | _.struct({ 83 | a: _.string, 84 | b: _.struct({ 85 | c: _.number 86 | }) 87 | }) 88 | 89 | // 90 | // fromPartial 91 | // 92 | 93 | // $ExpectType Decoder; }>, Partial<{ a: string; b: Partial<{ c: number; }>; }>> 94 | _.fromPartial({ 95 | a: _.string, 96 | b: _.fromPartial({ 97 | c: _.number 98 | }) 99 | }) 100 | 101 | // 102 | // partial 103 | // 104 | 105 | // $ExpectType Decoder; }>> 106 | _.partial({ 107 | a: _.string, 108 | b: _.partial({ 109 | c: _.number 110 | }) 111 | }) 112 | 113 | // 114 | // fromArray 115 | // 116 | 117 | // $ExpectType Decoder 118 | _.fromArray(NumberFromString) 119 | 120 | // 121 | // array 122 | // 123 | 124 | // $ExpectType Decoder 125 | _.array(_.string) 126 | 127 | // 128 | // fromRecord 129 | // 130 | 131 | // $ExpectType Decoder, Record> 132 | _.fromRecord(NumberFromString) 133 | 134 | // 135 | // record 136 | // 137 | 138 | // $ExpectType Decoder> 139 | _.record(_.string) 140 | 141 | // 142 | // fromTuple 143 | // 144 | 145 | // $ExpectType Decoder<[unknown, string, unknown], [string, number, boolean]> 146 | _.fromTuple(_.string, NumberFromString, _.boolean) 147 | 148 | // 149 | // tuple 150 | // 151 | 152 | // $ExpectType Decoder 153 | _.tuple(_.string, _.number, _.boolean) 154 | 155 | // 156 | // fromSum 157 | // 158 | 159 | // $ExpectType Decoder<{ _tag: unknown; a: unknown; } | { _tag: unknown; b: string; }, { _tag: "A"; a: string; } | { _tag: "B"; b: number; }> 160 | _.fromSum('_tag')({ 161 | A: _.fromStruct({ _tag: _.literal('A'), a: _.string }), 162 | B: _.fromStruct({ _tag: _.literal('B'), b: NumberFromString }) 163 | }) 164 | 165 | // 166 | // sum 167 | // 168 | 169 | const S1 = _.struct({ _tag: _.literal('A'), a: _.string }) 170 | const S2 = _.struct({ _tag: _.literal('B'), b: _.number }) 171 | 172 | // $ExpectType Decoder 173 | _.sum('_tag')({ A: S1, B: S2 }) 174 | // @ts-expect-error 175 | _.sum('_tag')({ A: S1, B: S1 }) 176 | 177 | // 178 | // union 179 | // 180 | 181 | // $ExpectType Decoder 182 | _.union(_.number, _.string) 183 | 184 | // 185 | // mapLeftWithInput 186 | // 187 | 188 | // $ExpectType Decoder 189 | pipe( 190 | _.number, 191 | _.mapLeftWithInput((u) => FS.of(DE.leaf(u, 'not a number'))) 192 | ) 193 | 194 | // 195 | // readonly 196 | // 197 | 198 | // $ExpectType Decoder> 199 | _.readonly( 200 | _.struct({ 201 | a: _.string 202 | }) 203 | ) 204 | -------------------------------------------------------------------------------- /dtslint/Encoder.ts: -------------------------------------------------------------------------------- 1 | import { pipe } from 'fp-ts/lib/pipeable' 2 | 3 | import * as E from '../src/Encoder' 4 | 5 | const NumberToString: E.Encoder = { 6 | encode: String 7 | } 8 | 9 | const BooleanToNumber: E.Encoder = { 10 | encode: (b) => (b ? 1 : 0) 11 | } 12 | 13 | export const OfTest = E.struct({ a: E.id(), b: E.struct({ c: NumberToString }) }) 14 | 15 | // 16 | // TypeOf 17 | // 18 | export type OfTest = E.TypeOf // $ExpectType { a: string; b: { c: number; }; } 19 | 20 | // 21 | // OutputOf 22 | // 23 | export type OfTestOutput = E.OutputOf // $ExpectType { a: string; b: { c: string; }; } 24 | 25 | // 26 | // nullable 27 | // 28 | E.nullable(NumberToString) // $ExpectType Encoder 29 | 30 | // 31 | // struct 32 | // 33 | E.struct({ a: E.struct({ b: NumberToString }) }) // $ExpectType Encoder<{ a: { b: string; }; }, { a: { b: number; }; }> 34 | 35 | // 36 | // partial 37 | // 38 | E.partial({ a: E.partial({ b: NumberToString }) }) // $ExpectType Encoder; }>, Partial<{ a: Partial<{ b: number; }>; }>> 39 | 40 | // 41 | // record 42 | // 43 | E.record(NumberToString) // $ExpectType Encoder, Record> 44 | 45 | // 46 | // array 47 | // 48 | E.array(NumberToString) // $ExpectType Encoder 49 | 50 | // 51 | // tuple 52 | // 53 | E.tuple() // $ExpectType Encoder<[], []> 54 | E.tuple(NumberToString) // $ExpectType Encoder<[string], [number]> 55 | E.tuple(NumberToString, BooleanToNumber) // $ExpectType Encoder<[string, number], [number, boolean]> 56 | 57 | // 58 | // intersection 59 | // 60 | pipe(E.struct({ a: NumberToString }), E.intersect(E.struct({ b: BooleanToNumber }))) // $ExpectType Encoder<{ a: string; } & { b: number; }, { a: number; } & { b: boolean; }> 61 | 62 | // 63 | // sum 64 | // 65 | const S1 = E.struct({ _tag: E.id<'A'>(), a: NumberToString }) 66 | const S2 = E.struct({ _tag: E.id<'B'>(), b: BooleanToNumber }) 67 | const sum = E.sum('_tag') 68 | 69 | // $ExpectType Encoder<{ _tag: "A"; a: string; } | { _tag: "B"; b: number; }, { _tag: "A"; a: number; } | { _tag: "B"; b: boolean; }> 70 | sum({ A: S1, B: S2 }) 71 | 72 | export const S3 = E.struct({ _tag: E.id<'C'>(), c: E.id() }) 73 | 74 | // 75 | // lazy 76 | // 77 | interface A { 78 | a: number 79 | bs: Array 80 | } 81 | interface AOut { 82 | a: string 83 | bs: Array 84 | } 85 | interface B { 86 | b: boolean 87 | as: Array 88 | } 89 | interface BOut { 90 | b: number 91 | as: Array 92 | } 93 | const A: E.Encoder = E.lazy(() => 94 | E.struct({ 95 | a: NumberToString, 96 | bs: E.array(B) 97 | }) 98 | ) 99 | 100 | const B: E.Encoder = E.lazy(() => 101 | E.struct({ 102 | b: BooleanToNumber, 103 | as: E.array(A) 104 | }) 105 | ) 106 | -------------------------------------------------------------------------------- /dtslint/Eq.ts: -------------------------------------------------------------------------------- 1 | import * as _ from '../src/Eq' 2 | 3 | // $ExpectType Eq<{ a: string; b: { c: number; }; }> 4 | _.struct({ 5 | a: _.string, 6 | b: _.struct({ 7 | c: _.number 8 | }) 9 | }) 10 | 11 | // $ExpectType Eq; }>> 12 | _.partial({ 13 | a: _.string, 14 | b: _.partial({ 15 | c: _.number 16 | }) 17 | }) 18 | 19 | // 20 | // sum 21 | // 22 | 23 | const S1 = _.struct({ _tag: _.Schemable.literal('A'), a: _.string }) 24 | const S2 = _.struct({ _tag: _.Schemable.literal('B'), b: _.number }) 25 | 26 | // $ExpectType Eq<{ _tag: "A"; a: string; } | { _tag: "B"; b: number; }> 27 | _.sum('_tag')({ A: S1, B: S2 }) 28 | // // @ts-expect-error 29 | // _.sum('_tag')({ A: S1, B: S1 }) 30 | 31 | // 32 | // readonly 33 | // 34 | 35 | // $ExpectType Eq> 36 | _.readonly( 37 | _.struct({ 38 | a: _.string 39 | }) 40 | ) 41 | -------------------------------------------------------------------------------- /dtslint/Guard.ts: -------------------------------------------------------------------------------- 1 | import * as _ from '../src/Guard' 2 | 3 | // 4 | // struct 5 | // 6 | 7 | // $ExpectType Guard 8 | const A = _.struct({ 9 | a: _.string, 10 | b: _.struct({ 11 | c: _.number 12 | }) 13 | }) 14 | 15 | // 16 | // partial 17 | // 18 | 19 | // $ExpectType Guard; }>> 20 | _.partial({ 21 | a: _.string, 22 | b: _.partial({ 23 | c: _.number 24 | }) 25 | }) 26 | 27 | // 28 | // TypeOf 29 | // 30 | 31 | // $ExpectType { a: string; b: { c: number; }; } 32 | export type A = _.TypeOf 33 | 34 | // 35 | // sum 36 | // 37 | 38 | const S1 = _.struct({ _tag: _.literal('A'), a: _.string }) 39 | const S2 = _.struct({ _tag: _.literal('B'), b: _.number }) 40 | 41 | // $ExpectType Guard 42 | _.sum('_tag')({ A: S1, B: S2 }) 43 | // @ts-expect-error 44 | _.sum('_tag')({ A: S1, B: S1 }) 45 | 46 | // 47 | // readonly 48 | // 49 | 50 | // $ExpectType Guard> 51 | _.readonly( 52 | _.struct({ 53 | a: _.string 54 | }) 55 | ) 56 | -------------------------------------------------------------------------------- /dtslint/Schema.ts: -------------------------------------------------------------------------------- 1 | import { HKT } from 'fp-ts/lib/HKT' 2 | import { pipe } from 'fp-ts/lib/pipeable' 3 | 4 | import { memoize, Schemable, WithRefine, WithUnion, WithUnknownContainers } from '../src/Schemable' 5 | 6 | interface Schema { 7 | (S: Schemable & WithUnknownContainers & WithRefine & WithUnion): HKT 8 | } 9 | 10 | export type TypeOf = S extends Schema ? A : never 11 | 12 | function make(f: Schema): Schema { 13 | return memoize(f) 14 | } 15 | 16 | // 17 | // TypeOf 18 | // 19 | 20 | export const OfTest = make((S) => S.struct({ a: S.string, b: S.struct({ c: S.number }) })) 21 | export type OfTest = TypeOf // $ExpectType { a: string; b: { c: number; }; } 22 | 23 | // 24 | // literal 25 | // 26 | 27 | // @ts-expect-error 28 | make((S) => S.literal()) 29 | make((S) => S.literal('a')) // $ExpectType Schema<"a"> 30 | make((S) => S.literal('a', 'b', null)) // $ExpectType Schema<"a" | "b" | null> 31 | make((S) => S.literal<['a']>('a')) // $ExpectType Schema<"a"> 32 | 33 | // 34 | // string 35 | // 36 | 37 | make((S) => S.string) // $ExpectType Schema 38 | 39 | // 40 | // number 41 | // 42 | 43 | make((S) => S.number) // $ExpectType Schema 44 | 45 | // 46 | // boolean 47 | // 48 | 49 | make((S) => S.boolean) // $ExpectType Schema 50 | 51 | // 52 | // nullable 53 | // 54 | 55 | make((S) => S.nullable(S.string)) // $ExpectType Schema 56 | 57 | // 58 | // struct 59 | // 60 | 61 | make((S) => S.struct({ a: S.string, b: S.struct({ c: S.number }) })) // $ExpectType Schema<{ a: string; b: { c: number; }; }> 62 | make((S) => S.struct({ a: S.literal('a') })) // $ExpectType Schema<{ a: "a"; }> 63 | 64 | // 65 | // partial 66 | // 67 | 68 | make((S) => S.partial({ a: S.string, b: S.partial({ c: S.number }) })) // $ExpectType Schema; }>> 69 | 70 | // 71 | // record 72 | // 73 | 74 | make((S) => S.record(S.number)) // $ExpectType Schema> 75 | 76 | // 77 | // array 78 | // 79 | 80 | make((S) => S.array(S.number)) // $ExpectType Schema 81 | 82 | // 83 | // tuple 84 | // 85 | 86 | make((S) => S.tuple()) // $ExpectType Schema<[]> 87 | make((S) => S.tuple(S.string)) // $ExpectType Schema<[string]> 88 | make((S) => S.tuple(S.string, S.number)) // $ExpectType Schema<[string, number]> 89 | make((S) => S.tuple(S.string, S.number, S.boolean)) // $ExpectType Schema<[string, number, boolean]> 90 | 91 | // 92 | // intersection 93 | // 94 | 95 | make((S) => pipe(S.struct({ a: S.string }), S.intersect(S.struct({ b: S.number })))) // $ExpectType Schema<{ a: string; } & { b: number; }> 96 | 97 | // 98 | // sum 99 | // 100 | 101 | const S1 = make((S) => S.struct({ _tag: S.literal('A'), a: S.string })) 102 | const S2 = make((S) => S.struct({ _tag: S.literal('B'), b: S.number })) 103 | 104 | // $ExpectType Schema<{ _tag: "A"; a: string; } | { _tag: "B"; b: number; }> 105 | make((S) => S.sum('_tag')({ A: S1(S), B: S2(S) })) 106 | // @ts-expect-error 107 | make((S) => S.sum('_tag')({ A: S1(S), B: S1(S) })) 108 | 109 | // 110 | // lazy 111 | // 112 | 113 | interface A { 114 | a: string 115 | bs: Array 116 | } 117 | 118 | interface B { 119 | b: number 120 | as: Array 121 | } 122 | 123 | const A: Schema = make((S) => 124 | S.lazy('A', () => 125 | S.struct({ 126 | a: S.string, 127 | bs: S.array(B(S)) 128 | }) 129 | ) 130 | ) 131 | 132 | const B: Schema = make((S) => 133 | S.lazy('B', () => 134 | S.struct({ 135 | b: S.number, 136 | as: S.array(A(S)) 137 | }) 138 | ) 139 | ) 140 | 141 | // 142 | // UnknownArray 143 | // 144 | 145 | make((S) => S.UnknownArray) // $ExpectType Schema 146 | 147 | // 148 | // UnknownRecord 149 | // 150 | 151 | make((S) => S.UnknownRecord) // $ExpectType Schema> 152 | 153 | // 154 | // refinement 155 | // 156 | 157 | interface PositiveBrand { 158 | readonly Positive: unique symbol 159 | } 160 | 161 | type Positive = number & PositiveBrand 162 | 163 | make((S) => 164 | pipe( 165 | S.number, 166 | S.refine((n): n is Positive => n > 0, 'Positive') 167 | ) 168 | ) 169 | 170 | // 171 | // union 172 | // 173 | 174 | // @ts-expect-error 175 | make((S) => S.union()) 176 | make((S) => S.union(S.string)) // $ExpectType Schema 177 | make((S) => S.union(S.string, S.number)) // $ExpectType Schema 178 | 179 | // 180 | // readonly 181 | // 182 | 183 | // $ExpectType Schema> 184 | make((S) => S.readonly(S.struct({ a: S.string }))) 185 | -------------------------------------------------------------------------------- /dtslint/TaskDecoder.ts: -------------------------------------------------------------------------------- 1 | import * as _ from '../src/TaskDecoder' 2 | 3 | // 4 | // sum 5 | // 6 | 7 | const S1 = _.struct({ _tag: _.literal('A'), a: _.string }) 8 | const S2 = _.struct({ _tag: _.literal('B'), b: _.number }) 9 | 10 | // $ExpectType TaskDecoder 11 | _.sum('_tag')({ A: S1, B: S2 }) 12 | // // @ts-expect-error 13 | // _.sum('_tag')({ A: S1, B: S1 }) 14 | 15 | // 16 | // readonly 17 | // 18 | 19 | // $ExpectType TaskDecoder> 20 | _.readonly( 21 | _.struct({ 22 | a: _.string 23 | }) 24 | ) 25 | -------------------------------------------------------------------------------- /dtslint/Type.ts: -------------------------------------------------------------------------------- 1 | import * as _ from '../src/Type' 2 | 3 | // $ExpectType Type<{ a: string; b: { c: number; }; }> 4 | _.struct({ 5 | a: _.string, 6 | b: _.struct({ 7 | c: _.number 8 | }) 9 | }) 10 | 11 | // $ExpectType Type; }>> 12 | _.partial({ 13 | a: _.string, 14 | b: _.partial({ 15 | c: _.number 16 | }) 17 | }) 18 | 19 | // 20 | // readonly 21 | // 22 | 23 | // $ExpectType Type> 24 | _.readonly( 25 | _.struct({ 26 | a: _.string 27 | }) 28 | ) 29 | -------------------------------------------------------------------------------- /dtslint/index.d.ts: -------------------------------------------------------------------------------- 1 | // Minimum TypeScript Version: 3.9 2 | -------------------------------------------------------------------------------- /dtslint/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "strict": true, 5 | "noEmit": true, 6 | "target": "es5", 7 | "lib": ["es2015"], 8 | "skipLibCheck": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /images/inference.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/io-ts/864a3a2f03c5d7b974afeb1da0faf46c21758779/images/inference.png -------------------------------------------------------------------------------- /images/introspection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/io-ts/864a3a2f03c5d7b974afeb1da0faf46c21758779/images/introspection.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | collectCoverage: true, 5 | collectCoverageFrom: ['src/**/*.ts'], 6 | transform: { 7 | '^.+\\.tsx?$': 'ts-jest' 8 | }, 9 | testRegex: 'test', 10 | moduleFileExtensions: ['ts', 'js'], 11 | coverageThreshold: { 12 | global: { 13 | branches: 100, 14 | functions: 100, 15 | lines: 100, 16 | statements: 100 17 | } 18 | }, 19 | modulePathIgnorePatterns: ['2.1.x/helpers.ts', 'Arbitrary.ts', 'helpers.ts', 'JsonSchema.ts'] 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "io-ts", 3 | "version": "2.2.22", 4 | "description": "TypeScript runtime type system for IO decoding/encoding", 5 | "main": "lib/index.js", 6 | "module": "es6/index.js", 7 | "typings": "lib/index.d.ts", 8 | "sideEffects": false, 9 | "scripts": { 10 | "lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\" \"scripts/**/*.ts\"", 11 | "lint-fix": "eslint --fix \"src/**/*.ts\" \"test/**/*.ts\" \"scripts/**/*.ts\"", 12 | "prettier": "prettier --no-semi --single-quote --print-width 120 --parser typescript --list-different \"{src,test}/**/*.ts\"", 13 | "fix-prettier": "prettier --no-semi --single-quote --print-width 120 --parser typescript --write \"{src,test,examples,exercises}/**/*.ts\"", 14 | "eslint": "eslint \"src/**/*.ts\" \"test/**/*.ts\" \"scripts/**/*.ts\"", 15 | "test": "npm run lint && npm run prettier && npm run dtslint && npm run vitest && npm run docs", 16 | "clean": "rm -rf ./dist", 17 | "prebuild": "npm run clean", 18 | "build": "tsc -p ./tsconfig.build.json && tsc -p ./tsconfig.build-es6.json && npm run import-path-rewrite && ts-node scripts/build", 19 | "postbuild": "prettier --loglevel=silent --write \"./dist/**/*.ts\"", 20 | "prepublishOnly": "ts-node scripts/pre-publish", 21 | "perf": "ts-node perf/index", 22 | "dtslint": "dtslint --expectOnly dtslint", 23 | "mocha": "TS_NODE_CACHE=false mocha -r ts-node/register test/*.ts", 24 | "doctoc": "doctoc README.md index.md Decoder.md Encoder.md Codec.md Eq.md Schema.md", 25 | "docs": "docs-ts", 26 | "prerelease": "npm run build", 27 | "release": "ts-node scripts/release", 28 | "import-path-rewrite": "import-path-rewrite", 29 | "vitest": "vitest run", 30 | "coverage": "vitest run --coverage" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "https://github.com/gcanti/io-ts.git" 35 | }, 36 | "author": "Giulio Canti ", 37 | "license": "MIT", 38 | "bugs": { 39 | "url": "https://github.com/gcanti/io-ts/issues" 40 | }, 41 | "homepage": "https://github.com/gcanti/io-ts", 42 | "peerDependencies": { 43 | "fp-ts": "^2.5.0" 44 | }, 45 | "devDependencies": { 46 | "@definitelytyped/dtslint": "^0.0.163", 47 | "@types/benchmark": "^1.0.31", 48 | "@types/glob": "^7.1.3", 49 | "@types/node": "^16.18.25", 50 | "@typescript-eslint/eslint-plugin": "^5.59.0", 51 | "@typescript-eslint/parser": "^5.59.0", 52 | "@vitest/coverage-istanbul": "^0.23.4", 53 | "benchmark": "2.1.4", 54 | "docs-ts": "^0.7.2", 55 | "eslint": "^8.38.0", 56 | "eslint-plugin-deprecation": "^1.4.1", 57 | "eslint-plugin-import": "^2.27.5", 58 | "eslint-plugin-simple-import-sort": "^10.0.0", 59 | "fast-check": "^1.26.0", 60 | "glob": "^7.1.6", 61 | "import-path-rewrite": "github:gcanti/import-path-rewrite", 62 | "mocha": "^5.2.0", 63 | "prettier": "^2.7.1", 64 | "rimraf": "2.6.2", 65 | "ts-node": "^8.0.2", 66 | "tslint": "5.11.0", 67 | "typescript": "^5.1.3", 68 | "vite": "^4.3.3", 69 | "vitest": "^0.30.1" 70 | }, 71 | "tags": [ 72 | "typescript", 73 | "runtime", 74 | "decoder", 75 | "encoder", 76 | "schema" 77 | ], 78 | "keywords": [ 79 | "typescript", 80 | "runtime", 81 | "decoder", 82 | "encoder", 83 | "schema" 84 | ] 85 | } 86 | -------------------------------------------------------------------------------- /perf/Decoder.ts: -------------------------------------------------------------------------------- 1 | import * as Benchmark from 'benchmark' 2 | import * as t from '../src/Decoder' 3 | 4 | /* 5 | space-object (good) x 662,359 ops/sec ±0.65% (88 runs sampled) 6 | space-object (bad) x 379,528 ops/sec ±0.56% (89 runs sampled) 7 | 8 | space-object (good) x 295,284 ops/sec ±0.67% (85 runs sampled) 9 | space-object (bad) x 295,530 ops/sec ±0.53% (88 runs sampled) 10 | */ 11 | 12 | const suite = new Benchmark.Suite() 13 | 14 | const Vector = t.tuple(t.number, t.number, t.number) 15 | 16 | const Asteroid = t.type({ 17 | type: t.literal('asteroid'), 18 | location: Vector, 19 | mass: t.number 20 | }) 21 | 22 | const Planet = t.type({ 23 | type: t.literal('planet'), 24 | location: Vector, 25 | mass: t.number, 26 | population: t.number, 27 | habitable: t.boolean 28 | }) 29 | 30 | const Rank = t.literal('captain', 'first mate', 'officer', 'ensign') 31 | 32 | const CrewMember = t.type({ 33 | name: t.string, 34 | age: t.number, 35 | rank: Rank, 36 | home: Planet 37 | }) 38 | 39 | const Ship = t.type({ 40 | type: t.literal('ship'), 41 | location: Vector, 42 | mass: t.number, 43 | name: t.string, 44 | crew: t.array(CrewMember) 45 | }) 46 | 47 | const T = t.sum('type')({ 48 | asteroid: Asteroid, 49 | planet: Planet, 50 | ship: Ship 51 | }) 52 | 53 | const good = { 54 | type: 'ship', 55 | location: [1, 2, 3], 56 | mass: 4, 57 | name: 'foo', 58 | crew: [ 59 | { 60 | name: 'bar', 61 | age: 44, 62 | rank: 'captain', 63 | home: { 64 | type: 'planet', 65 | location: [5, 6, 7], 66 | mass: 8, 67 | population: 1000, 68 | habitable: true 69 | } 70 | } 71 | ] 72 | } 73 | 74 | const bad = { 75 | type: 'ship', 76 | location: [1, 2, 'a'], 77 | mass: 4, 78 | name: 'foo', 79 | crew: [ 80 | { 81 | name: 'bar', 82 | age: 44, 83 | rank: 'captain', 84 | home: { 85 | type: 'planet', 86 | location: [5, 6, 7], 87 | mass: 8, 88 | population: 'a', 89 | habitable: true 90 | } 91 | } 92 | ] 93 | } 94 | 95 | // console.log(T.decode(good)) 96 | // console.log(T.decode(bad)) 97 | 98 | suite 99 | .add('space-object (good)', function () { 100 | T.decode(good) 101 | }) 102 | .add('space-object (bad)', function () { 103 | T.decode(bad) 104 | }) 105 | .on('cycle', function (event: any) { 106 | console.log(String(event.target)) 107 | }) 108 | .on('complete', function (this: any) { 109 | console.log('Fastest is ' + this.filter('fastest').map('name')) 110 | }) 111 | .run({ async: true }) 112 | -------------------------------------------------------------------------------- /perf/Decoder2.ts: -------------------------------------------------------------------------------- 1 | import * as Benchmark from 'benchmark' 2 | import * as E from 'fp-ts/lib/Either' 3 | import { pipe } from 'fp-ts/lib/pipeable' 4 | import * as D from '../src/Decoder' 5 | import * as G from '../src/Guard' 6 | 7 | /* 8 | 9 | Guard (good) x 36,163,576 ops/sec ±1.45% (86 runs sampled) 10 | Decoder (good) x 2,365,294 ops/sec ±0.70% (87 runs sampled) 11 | Guard (bad) x 34,845,843 ops/sec ±1.27% (84 runs sampled) 12 | Decoder (bad) x 1,977,286 ops/sec ±0.86% (86 runs sampled) 13 | Decoder (draw) x 365,279 ops/sec ±1.17% (82 runs sampled) 14 | 15 | */ 16 | 17 | const decoder = D.type({ 18 | name: D.string, 19 | age: D.number 20 | }) 21 | 22 | const guard = G.type({ 23 | name: G.string, 24 | age: G.number 25 | }) 26 | 27 | const good = { 28 | name: 'name', 29 | age: 18 30 | } 31 | 32 | const bad = {} 33 | 34 | // console.log(decoder.decode(bad)) 35 | // console.log(JSON.stringify(freeDecoder.decode(bad), null, 2)) 36 | 37 | const suite = new Benchmark.Suite() 38 | 39 | suite 40 | .add('Guard (good)', function () { 41 | guard.is(bad) 42 | }) 43 | .add('Decoder (good)', function () { 44 | decoder.decode(good) 45 | }) 46 | .add('Guard (bad)', function () { 47 | guard.is(bad) 48 | }) 49 | .add('Decoder (bad)', function () { 50 | decoder.decode(bad) 51 | }) 52 | .add('Decoder (draw)', function () { 53 | pipe(decoder.decode(bad), E.mapLeft(D.draw)) 54 | }) 55 | .on('cycle', function (event: any) { 56 | console.log(String(event.target)) 57 | }) 58 | .on('complete', function (this: any) { 59 | console.log('Fastest is ' + this.filter('fastest').map('name')) 60 | }) 61 | .run({ async: true }) 62 | -------------------------------------------------------------------------------- /perf/index.ts: -------------------------------------------------------------------------------- 1 | import * as Benchmark from 'benchmark' 2 | import * as t from '../src' 3 | 4 | /* 5 | space-object (good) x 476,424 ops/sec ±0.45% (92 runs sampled) 6 | space-object (bad) x 434,563 ops/sec ±0.58% (87 runs sampled) 7 | */ 8 | 9 | const suite = new Benchmark.Suite() 10 | 11 | const Vector = t.tuple([t.number, t.number, t.number]) 12 | 13 | const Asteroid = t.type({ 14 | type: t.literal('asteroid'), 15 | location: Vector, 16 | mass: t.number 17 | }) 18 | 19 | const Planet = t.type({ 20 | type: t.literal('planet'), 21 | location: Vector, 22 | mass: t.number, 23 | population: t.number, 24 | habitable: t.boolean 25 | }) 26 | 27 | const Rank = t.keyof({ 28 | captain: null, 29 | 'first mate': null, 30 | officer: null, 31 | ensign: null 32 | }) 33 | 34 | const CrewMember = t.type({ 35 | name: t.string, 36 | age: t.number, 37 | rank: Rank, 38 | home: Planet 39 | }) 40 | 41 | const Ship = t.type({ 42 | type: t.literal('ship'), 43 | location: Vector, 44 | mass: t.number, 45 | name: t.string, 46 | crew: t.array(CrewMember) 47 | }) 48 | 49 | const T = t.union([Asteroid, Planet, Ship]) 50 | 51 | const good = { 52 | type: 'ship', 53 | location: [1, 2, 3], 54 | mass: 4, 55 | name: 'foo', 56 | crew: [ 57 | { 58 | name: 'bar', 59 | age: 44, 60 | rank: 'captain', 61 | home: { 62 | type: 'planet', 63 | location: [5, 6, 7], 64 | mass: 8, 65 | population: 1000, 66 | habitable: true 67 | } 68 | } 69 | ] 70 | } 71 | 72 | const bad = { 73 | type: 'ship', 74 | location: [1, 2, 'a'], 75 | mass: 4, 76 | name: 'foo', 77 | crew: [ 78 | { 79 | name: 'bar', 80 | age: 44, 81 | rank: 'captain', 82 | home: { 83 | type: 'planet', 84 | location: [5, 6, 7], 85 | mass: 8, 86 | population: 'a', 87 | habitable: true 88 | } 89 | } 90 | ] 91 | } 92 | 93 | // console.log(T.decode(good)) 94 | // console.log(T.decode(bad)) 95 | 96 | suite 97 | .add('space-object (good)', function () { 98 | T.decode(good) 99 | }) 100 | .add('space-object (bad)', function () { 101 | T.decode(bad) 102 | }) 103 | .on('cycle', function (event: any) { 104 | console.log(String(event.target)) 105 | }) 106 | .on('complete', function (this: any) { 107 | console.log('Fastest is ' + this.filter('fastest').map('name')) 108 | }) 109 | .run({ async: true }) 110 | -------------------------------------------------------------------------------- /perf/typescript-runtime-type-benchmarks.ts: -------------------------------------------------------------------------------- 1 | import * as Benchmark from 'benchmark' 2 | import * as t from '../src/Decoder' 3 | import * as G from '../src/Guard' 4 | 5 | /* 6 | decode x 1,138,915 ops/sec ±0.44% (87 runs sampled) 7 | is x 1,975,690 ops/sec ±0.44% (91 runs sampled) 8 | */ 9 | 10 | const suite = new Benchmark.Suite() 11 | 12 | const decoder = t.type({ 13 | number: t.number, 14 | negNumber: t.number, 15 | maxNumber: t.number, 16 | string: t.string, 17 | longString: t.string, 18 | boolean: t.boolean, 19 | deeplyNested: t.type({ 20 | foo: t.string, 21 | num: t.number, 22 | bool: t.boolean 23 | }) 24 | }) 25 | 26 | const guard = G.type({ 27 | number: G.number, 28 | negNumber: G.number, 29 | maxNumber: G.number, 30 | string: G.string, 31 | longString: G.string, 32 | boolean: G.boolean, 33 | deeplyNested: G.type({ 34 | foo: G.string, 35 | num: G.number, 36 | bool: G.boolean 37 | }) 38 | }) 39 | 40 | export const good = { 41 | number: 1, 42 | negNumber: -1, 43 | maxNumber: Number.MAX_VALUE, 44 | string: 'string', 45 | longString: 46 | 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Vivendum intellegat et qui, ei denique consequuntur vix. Semper aeterno percipit ut his, sea ex utinam referrentur repudiandae. No epicuri hendrerit consetetur sit, sit dicta adipiscing ex, in facete detracto deterruisset duo. Quot populo ad qui. Sit fugit nostrum et. Ad per diam dicant interesset, lorem iusto sensibus ut sed. No dicam aperiam vis. Pri posse graeco definitiones cu, id eam populo quaestio adipiscing, usu quod malorum te. Ex nam agam veri, dicunt efficiantur ad qui, ad legere adversarium sit. Commune platonem mel id, brute adipiscing duo an. Vivendum intellegat et qui, ei denique consequuntur vix. Offendit eleifend moderatius ex vix, quem odio mazim et qui, purto expetendis cotidieque quo cu, veri persius vituperata ei nec. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.', 47 | boolean: true, 48 | deeplyNested: { 49 | foo: 'bar', 50 | num: 1, 51 | bool: false 52 | } 53 | } 54 | 55 | // console.log(decoder.decode(good)) 56 | // console.log(guard.is(good)) 57 | 58 | suite 59 | .add('decode', function () { 60 | decoder.decode(good) 61 | }) 62 | .add('is', function () { 63 | guard.is(good) 64 | }) 65 | .on('cycle', function (event: any) { 66 | console.log(String(event.target)) 67 | }) 68 | .on('complete', function (this: any) { 69 | console.log('Fastest is ' + this.filter('fastest').map('name')) 70 | }) 71 | .run({ async: true }) 72 | -------------------------------------------------------------------------------- /scripts/FileSystem.ts: -------------------------------------------------------------------------------- 1 | import { flow } from 'fp-ts/lib/function' 2 | import * as TE from 'fp-ts/lib/TaskEither' 3 | import * as fs from 'fs' 4 | import G from 'glob' 5 | 6 | export interface FileSystem { 7 | readonly readFile: (path: string) => TE.TaskEither 8 | readonly writeFile: (path: string, content: string) => TE.TaskEither 9 | readonly copyFile: (from: string, to: string) => TE.TaskEither 10 | readonly glob: (pattern: string) => TE.TaskEither> 11 | readonly mkdir: (path: string) => TE.TaskEither 12 | } 13 | 14 | const readFile = TE.taskify(fs.readFile) 15 | const writeFile = TE.taskify(fs.writeFile) 16 | const copyFile = TE.taskify(fs.copyFile) 17 | const glob = TE.taskify>(G) 18 | const mkdirTE = TE.taskify(fs.mkdir) 19 | 20 | export const fileSystem: FileSystem = { 21 | readFile: (path) => readFile(path, 'utf8'), 22 | writeFile, 23 | copyFile, 24 | glob, 25 | mkdir: flow( 26 | mkdirTE, 27 | TE.map(() => undefined) 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /scripts/build.ts: -------------------------------------------------------------------------------- 1 | import * as E from 'fp-ts/lib/Either' 2 | import { pipe } from 'fp-ts/lib/pipeable' 3 | import * as RTE from 'fp-ts/lib/ReaderTaskEither' 4 | import * as A from 'fp-ts/lib/ReadonlyArray' 5 | import * as TE from 'fp-ts/lib/TaskEither' 6 | import * as path from 'path' 7 | 8 | import { FileSystem, fileSystem } from './FileSystem' 9 | import { run } from './run' 10 | 11 | interface Build extends RTE.ReaderTaskEither {} 12 | 13 | const OUTPUT_FOLDER = 'dist' 14 | const PKG = 'package.json' 15 | 16 | export const copyPackageJson: Build = (C) => 17 | pipe( 18 | C.readFile(PKG), 19 | TE.chain((s) => TE.fromEither(E.parseJSON(s, E.toError))), 20 | TE.map((v) => { 21 | const clone = Object.assign({}, v as any) 22 | 23 | delete clone.scripts 24 | delete clone.files 25 | delete clone.devDependencies 26 | 27 | return clone 28 | }), 29 | TE.chain((json) => C.writeFile(path.join(OUTPUT_FOLDER, PKG), JSON.stringify(json, null, 2))) 30 | ) 31 | 32 | export const FILES: ReadonlyArray = ['CHANGELOG.md', 'LICENSE', 'README.md'] 33 | 34 | export const copyFiles: Build> = (C) => 35 | A.readonlyArray.traverse(TE.taskEither)(FILES, (from) => C.copyFile(from, path.resolve(OUTPUT_FOLDER, from))) 36 | 37 | const traverse = A.readonlyArray.traverse(TE.taskEither) 38 | 39 | export const makeModules: Build = (C) => 40 | pipe( 41 | C.glob(`${OUTPUT_FOLDER}/lib/*.js`), 42 | TE.map(getModules), 43 | TE.chain((modules) => traverse(modules, makeSingleModule(C))), 44 | TE.map(() => undefined) 45 | ) 46 | 47 | function getModules(paths: ReadonlyArray): ReadonlyArray { 48 | return paths.map((filePath) => path.basename(filePath, '.js')).filter((x) => x !== 'index') 49 | } 50 | 51 | function makeSingleModule(C: FileSystem): (module: string) => TE.TaskEither { 52 | return (m) => 53 | pipe( 54 | C.mkdir(path.join(OUTPUT_FOLDER, m)), 55 | TE.chain(() => makePkgJson(m)), 56 | TE.chain((data) => C.writeFile(path.join(OUTPUT_FOLDER, m, 'package.json'), data)) 57 | ) 58 | } 59 | 60 | function makePkgJson(module: string): TE.TaskEither { 61 | return pipe( 62 | JSON.stringify( 63 | { 64 | main: `../lib/${module}.js`, 65 | module: `../es6/${module}.js`, 66 | typings: `../lib/${module}.d.ts`, 67 | sideEffects: false 68 | }, 69 | null, 70 | 2 71 | ), 72 | TE.right 73 | ) 74 | } 75 | 76 | const main: Build = pipe( 77 | copyPackageJson, 78 | RTE.chain(() => copyFiles), 79 | RTE.chain(() => makeModules) 80 | ) 81 | 82 | run( 83 | main({ 84 | ...fileSystem 85 | }) 86 | ) 87 | -------------------------------------------------------------------------------- /scripts/pre-publish.ts: -------------------------------------------------------------------------------- 1 | import { left } from 'fp-ts/lib/TaskEither' 2 | 3 | import { run } from './run' 4 | 5 | const main = left(new Error('"npm publish" can not be run from root, run "npm run release" instead')) 6 | 7 | run(main) 8 | -------------------------------------------------------------------------------- /scripts/release.ts: -------------------------------------------------------------------------------- 1 | import * as child_process from 'child_process' 2 | import { left, right } from 'fp-ts/lib/Either' 3 | import * as TE from 'fp-ts/lib/TaskEither' 4 | 5 | import { run } from './run' 6 | 7 | const DIST = 'dist' 8 | 9 | const exec = (cmd: string, args?: child_process.ExecOptions): TE.TaskEither => () => 10 | new Promise((resolve) => { 11 | child_process.exec(cmd, args, (err) => { 12 | if (err !== null) { 13 | return resolve(left(err)) 14 | } 15 | 16 | return resolve(right(undefined)) 17 | }) 18 | }) 19 | 20 | export const main = exec('npm publish', { 21 | cwd: DIST 22 | }) 23 | 24 | run(main) 25 | -------------------------------------------------------------------------------- /scripts/run.ts: -------------------------------------------------------------------------------- 1 | import { fold } from 'fp-ts/lib/Either' 2 | import { TaskEither } from 'fp-ts/lib/TaskEither' 3 | 4 | export function run(eff: TaskEither): void { 5 | eff() 6 | .then( 7 | fold( 8 | (e) => { 9 | throw e 10 | }, 11 | (_) => { 12 | process.exitCode = 0 13 | } 14 | ) 15 | ) 16 | .catch((e) => { 17 | console.error(e) // tslint:disable-line no-console 18 | 19 | process.exitCode = 1 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /src/DecodeError.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * **This module is experimental** 3 | * 4 | * Experimental features are published in order to get early feedback from the community, see these tracking 5 | * [issues](https://github.com/gcanti/io-ts/issues?q=label%3Av2.2+) for further discussions and enhancements. 6 | * 7 | * A feature tagged as _Experimental_ is in a high state of flux, you're at risk of it changing without notice. 8 | * 9 | * @since 2.2.7 10 | */ 11 | import { Semigroup } from 'fp-ts/lib/Semigroup' 12 | 13 | import * as FS from './FreeSemigroup' 14 | 15 | /** 16 | * @category model 17 | * @since 2.2.7 18 | */ 19 | export interface Leaf { 20 | readonly _tag: 'Leaf' 21 | readonly actual: unknown 22 | readonly error: E 23 | } 24 | 25 | /** 26 | * @category model 27 | * @since 2.2.7 28 | */ 29 | // eslint-disable-next-line @typescript-eslint/prefer-as-const 30 | export const required: 'required' = 'required' 31 | 32 | /** 33 | * @category model 34 | * @since 2.2.7 35 | */ 36 | // eslint-disable-next-line @typescript-eslint/prefer-as-const 37 | export const optional: 'optional' = 'optional' 38 | 39 | /** 40 | * @category model 41 | * @since 2.2.7 42 | */ 43 | export type Kind = 'required' | 'optional' 44 | 45 | /** 46 | * @category model 47 | * @since 2.2.7 48 | */ 49 | export interface Key { 50 | readonly _tag: 'Key' 51 | readonly key: string 52 | readonly kind: Kind 53 | readonly errors: FS.FreeSemigroup> 54 | } 55 | 56 | /** 57 | * @category model 58 | * @since 2.2.7 59 | */ 60 | export interface Index { 61 | readonly _tag: 'Index' 62 | readonly index: number 63 | readonly kind: Kind 64 | readonly errors: FS.FreeSemigroup> 65 | } 66 | 67 | /** 68 | * @category model 69 | * @since 2.2.7 70 | */ 71 | export interface Member { 72 | readonly _tag: 'Member' 73 | readonly index: number 74 | readonly errors: FS.FreeSemigroup> 75 | } 76 | 77 | /** 78 | * @category model 79 | * @since 2.2.7 80 | */ 81 | export interface Lazy { 82 | readonly _tag: 'Lazy' 83 | readonly id: string 84 | readonly errors: FS.FreeSemigroup> 85 | } 86 | 87 | /** 88 | * @category model 89 | * @since 2.2.9 90 | */ 91 | export interface Wrap { 92 | readonly _tag: 'Wrap' 93 | readonly error: E 94 | readonly errors: FS.FreeSemigroup> 95 | } 96 | 97 | /** 98 | * @category model 99 | * @since 2.2.7 100 | */ 101 | export type DecodeError = Leaf | Key | Index | Member | Lazy | Wrap 102 | 103 | /** 104 | * @category constructors 105 | * @since 2.2.7 106 | */ 107 | export const leaf = (actual: unknown, error: E): DecodeError => ({ _tag: 'Leaf', actual, error }) 108 | 109 | /** 110 | * @category constructors 111 | * @since 2.2.7 112 | */ 113 | export const key = (key: string, kind: Kind, errors: FS.FreeSemigroup>): DecodeError => ({ 114 | _tag: 'Key', 115 | key, 116 | kind, 117 | errors 118 | }) 119 | 120 | /** 121 | * @category constructors 122 | * @since 2.2.7 123 | */ 124 | export const index = (index: number, kind: Kind, errors: FS.FreeSemigroup>): DecodeError => ({ 125 | _tag: 'Index', 126 | index, 127 | kind, 128 | errors 129 | }) 130 | 131 | /** 132 | * @category constructors 133 | * @since 2.2.7 134 | */ 135 | export const member = (index: number, errors: FS.FreeSemigroup>): DecodeError => ({ 136 | _tag: 'Member', 137 | index, 138 | errors 139 | }) 140 | 141 | /** 142 | * @category constructors 143 | * @since 2.2.7 144 | */ 145 | export const lazy = (id: string, errors: FS.FreeSemigroup>): DecodeError => ({ 146 | _tag: 'Lazy', 147 | id, 148 | errors 149 | }) 150 | 151 | /** 152 | * @category constructors 153 | * @since 2.2.9 154 | */ 155 | export const wrap = (error: E, errors: FS.FreeSemigroup>): DecodeError => ({ 156 | _tag: 'Wrap', 157 | error, 158 | errors 159 | }) 160 | 161 | /** 162 | * @category destructors 163 | * @since 2.2.7 164 | */ 165 | export const fold = (patterns: { 166 | Leaf: (input: unknown, error: E) => R 167 | Key: (key: string, kind: Kind, errors: FS.FreeSemigroup>) => R 168 | Index: (index: number, kind: Kind, errors: FS.FreeSemigroup>) => R 169 | Member: (index: number, errors: FS.FreeSemigroup>) => R 170 | Lazy: (id: string, errors: FS.FreeSemigroup>) => R 171 | Wrap: (error: E, errors: FS.FreeSemigroup>) => R 172 | }): ((e: DecodeError) => R) => { 173 | const f = (e: DecodeError): R => { 174 | switch (e._tag) { 175 | case 'Leaf': 176 | return patterns.Leaf(e.actual, e.error) 177 | case 'Key': 178 | return patterns.Key(e.key, e.kind, e.errors) 179 | case 'Index': 180 | return patterns.Index(e.index, e.kind, e.errors) 181 | case 'Member': 182 | return patterns.Member(e.index, e.errors) 183 | case 'Lazy': 184 | return patterns.Lazy(e.id, e.errors) 185 | case 'Wrap': 186 | return patterns.Wrap(e.error, e.errors) 187 | } 188 | } 189 | return f 190 | } 191 | 192 | /** 193 | * @category instances 194 | * @since 2.2.7 195 | */ 196 | export function getSemigroup(): Semigroup>> { 197 | return FS.getSemigroup() 198 | } 199 | -------------------------------------------------------------------------------- /src/Encoder.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * **This module is experimental** 3 | * 4 | * Experimental features are published in order to get early feedback from the community, see these tracking 5 | * [issues](https://github.com/gcanti/io-ts/issues?q=label%3Av2.2+) for further discussions and enhancements. 6 | * 7 | * A feature tagged as _Experimental_ is in a high state of flux, you're at risk of it changing without notice. 8 | * 9 | * @since 2.2.3 10 | */ 11 | import { Category2 } from 'fp-ts/lib/Category' 12 | import { Contravariant2 } from 'fp-ts/lib/Contravariant' 13 | import { identity } from 'fp-ts/lib/function' 14 | 15 | import { intersect_, memoize } from './Schemable' 16 | 17 | // ------------------------------------------------------------------------------------- 18 | // model 19 | // ------------------------------------------------------------------------------------- 20 | 21 | /** 22 | * @category model 23 | * @since 2.2.3 24 | */ 25 | export interface Encoder { 26 | readonly encode: (a: A) => O 27 | } 28 | 29 | // ------------------------------------------------------------------------------------- 30 | // combinators 31 | // ------------------------------------------------------------------------------------- 32 | 33 | /** 34 | * @category combinators 35 | * @since 2.2.3 36 | */ 37 | export function nullable(or: Encoder): Encoder { 38 | return { 39 | encode: (a) => (a === null ? null : or.encode(a)) 40 | } 41 | } 42 | 43 | /** 44 | * @category combinators 45 | * @since 2.2.15 46 | */ 47 | export function struct

>>( 48 | properties: P 49 | ): Encoder<{ [K in keyof P]: OutputOf }, { [K in keyof P]: TypeOf }> { 50 | return { 51 | encode: (a) => { 52 | const o: Record = {} as any 53 | for (const k in properties) { 54 | o[k] = properties[k].encode(a[k]) 55 | } 56 | return o 57 | } 58 | } 59 | } 60 | 61 | /** 62 | * Use `struct` instead. 63 | * 64 | * @category combinators 65 | * @since 2.2.3 66 | * @deprecated 67 | */ 68 | export const type = struct 69 | 70 | /** 71 | * @category combinators 72 | * @since 2.2.3 73 | */ 74 | export function partial

>>( 75 | properties: P 76 | ): Encoder }>, Partial<{ [K in keyof P]: TypeOf }>> { 77 | return { 78 | encode: (a) => { 79 | const o: Record = {} as any 80 | for (const k in properties) { 81 | const v = a[k] 82 | // don't add missing properties 83 | if (k in a) { 84 | // don't strip undefined properties 85 | o[k] = v === undefined ? undefined : properties[k].encode(v) 86 | } 87 | } 88 | return o 89 | } 90 | } 91 | } 92 | 93 | /** 94 | * @category combinators 95 | * @since 2.2.3 96 | */ 97 | export function record(codomain: Encoder): Encoder, Record> { 98 | return { 99 | encode: (r) => { 100 | const o: Record = {} 101 | for (const k in r) { 102 | o[k] = codomain.encode(r[k]) 103 | } 104 | return o 105 | } 106 | } 107 | } 108 | 109 | /** 110 | * @category combinators 111 | * @since 2.2.3 112 | */ 113 | export function array(item: Encoder): Encoder, Array> { 114 | return { 115 | encode: (as) => as.map(item.encode) 116 | } 117 | } 118 | 119 | /** 120 | * @category combinators 121 | * @since 2.2.3 122 | */ 123 | export function tuple>>( 124 | ...components: C 125 | ): Encoder<{ [K in keyof C]: OutputOf }, { [K in keyof C]: TypeOf }> { 126 | return { 127 | encode: (as) => components.map((c, i) => c.encode(as[i])) as any 128 | } 129 | } 130 | 131 | /** 132 | * @category combinators 133 | * @since 2.2.3 134 | */ 135 | export const intersect = 136 | (right: Encoder) => 137 | (left: Encoder): Encoder => ({ 138 | encode: (ab) => intersect_(left.encode(ab), right.encode(ab)) 139 | }) 140 | 141 | /** 142 | * @category combinators 143 | * @since 2.2.3 144 | */ 145 | export function sum( 146 | tag: T 147 | ): >>( 148 | members: MS 149 | ) => Encoder, TypeOf> { 150 | return (members) => { 151 | return { 152 | encode: (a) => members[a[tag]].encode(a) 153 | } 154 | } 155 | } 156 | 157 | /** 158 | * @category combinators 159 | * @since 2.2.3 160 | */ 161 | export function lazy(f: () => Encoder): Encoder { 162 | const get = memoize>(f) 163 | return { 164 | encode: (a) => get().encode(a) 165 | } 166 | } 167 | 168 | /** 169 | * @category combinators 170 | * @since 2.2.16 171 | */ 172 | export const readonly: (decoder: Encoder) => Encoder> = identity 173 | 174 | // ------------------------------------------------------------------------------------- 175 | // non-pipeables 176 | // ------------------------------------------------------------------------------------- 177 | 178 | const contramap_: (ea: Encoder, f: (b: B) => A) => Encoder = (ea, f) => ({ 179 | encode: (b) => ea.encode(f(b)) 180 | }) 181 | 182 | const compose_: (ab: Encoder, ea: Encoder) => Encoder = (ab, ea) => contramap_(ea, ab.encode) 183 | 184 | // ------------------------------------------------------------------------------------- 185 | // pipeables 186 | // ------------------------------------------------------------------------------------- 187 | 188 | /** 189 | * @category Contravariant 190 | * @since 2.2.3 191 | */ 192 | export const contramap: (f: (b: B) => A) => (fa: Encoder) => Encoder = (f) => (fa) => 193 | contramap_(fa, f) 194 | 195 | /** 196 | * @category Semigroupoid 197 | * @since 2.2.3 198 | */ 199 | export const compose: (ea: Encoder) => (ab: Encoder) => Encoder = (ea) => (ab) => 200 | compose_(ab, ea) 201 | 202 | /** 203 | * @category Category 204 | * @since 2.2.3 205 | */ 206 | export function id(): Encoder { 207 | return { 208 | encode: identity 209 | } 210 | } 211 | 212 | // ------------------------------------------------------------------------------------- 213 | // instances 214 | // ------------------------------------------------------------------------------------- 215 | 216 | /** 217 | * @category instances 218 | * @since 2.2.3 219 | */ 220 | export const URI = 'io-ts/Encoder' 221 | 222 | /** 223 | * @category instances 224 | * @since 2.2.3 225 | */ 226 | export type URI = typeof URI 227 | 228 | declare module 'fp-ts/lib/HKT' { 229 | interface URItoKind2 { 230 | readonly [URI]: Encoder 231 | } 232 | } 233 | 234 | /** 235 | * @category instances 236 | * @since 2.2.8 237 | */ 238 | export const Contravariant: Contravariant2 = { 239 | URI, 240 | contramap: contramap_ 241 | } 242 | 243 | /** 244 | * @category instances 245 | * @since 2.2.8 246 | */ 247 | export const Category: Category2 = { 248 | URI, 249 | compose: compose_, 250 | id 251 | } 252 | 253 | // ------------------------------------------------------------------------------------- 254 | // utils 255 | // ------------------------------------------------------------------------------------- 256 | 257 | /** 258 | * @since 2.2.3 259 | */ 260 | export type TypeOf = E extends Encoder ? A : never 261 | 262 | /** 263 | * @since 2.2.3 264 | */ 265 | export type OutputOf = E extends Encoder ? O : never 266 | -------------------------------------------------------------------------------- /src/Eq.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * **This module is experimental** 3 | * 4 | * Experimental features are published in order to get early feedback from the community, see these tracking 5 | * [issues](https://github.com/gcanti/io-ts/issues?q=label%3Av2.2+) for further discussions and enhancements. 6 | * 7 | * A feature tagged as _Experimental_ is in a high state of flux, you're at risk of it changing without notice. 8 | * 9 | * @since 2.2.2 10 | */ 11 | import * as A from 'fp-ts/lib/Array' 12 | import * as E from 'fp-ts/lib/Eq' 13 | import { identity } from 'fp-ts/lib/function' 14 | import * as R from 'fp-ts/lib/Record' 15 | 16 | import { memoize, Schemable1, WithRefine1, WithUnknownContainers1 } from './Schemable' 17 | import Eq = E.Eq 18 | 19 | // ------------------------------------------------------------------------------------- 20 | // utils 21 | // ------------------------------------------------------------------------------------- 22 | 23 | /** 24 | * @since 2.2.3 25 | */ 26 | export type URI = E.URI 27 | 28 | /** 29 | * @since 2.2.2 30 | */ 31 | export type TypeOf = E extends Eq ? A : never 32 | 33 | // ------------------------------------------------------------------------------------- 34 | // primitives 35 | // ------------------------------------------------------------------------------------- 36 | 37 | /** 38 | * @category primitives 39 | * @since 2.2.2 40 | */ 41 | export const string: Eq = E.eqString 42 | 43 | /** 44 | * @category primitives 45 | * @since 2.2.2 46 | */ 47 | export const number: Eq = E.eqNumber 48 | 49 | /** 50 | * @category primitives 51 | * @since 2.2.2 52 | */ 53 | export const boolean: Eq = E.eqBoolean 54 | 55 | /** 56 | * @category primitives 57 | * @since 2.2.2 58 | */ 59 | export const UnknownArray: Eq> = E.fromEquals((x, y) => x.length === y.length) 60 | 61 | /** 62 | * @category primitives 63 | * @since 2.2.2 64 | */ 65 | export const UnknownRecord: Eq> = E.fromEquals((x, y) => { 66 | for (const k in x) { 67 | if (!(k in y)) { 68 | return false 69 | } 70 | } 71 | for (const k in y) { 72 | if (!(k in x)) { 73 | return false 74 | } 75 | } 76 | return true 77 | }) 78 | 79 | // ------------------------------------------------------------------------------------- 80 | // combinators 81 | // ------------------------------------------------------------------------------------- 82 | 83 | /** 84 | * @category combinators 85 | * @since 2.2.2 86 | */ 87 | export function nullable(or: Eq): Eq { 88 | return { 89 | equals: (x, y) => (x === null || y === null ? x === y : or.equals(x, y)) 90 | } 91 | } 92 | 93 | /** 94 | * @category combinators 95 | * @since 2.2.15 96 | */ 97 | export const struct: (eqs: { [K in keyof A]: Eq }) => Eq<{ [K in keyof A]: A[K] }> = E.getStructEq 98 | 99 | /** 100 | * Use `struct` instead. 101 | * 102 | * @category combinators 103 | * @since 2.2.2 104 | * @deprecated 105 | */ 106 | export const type = struct 107 | 108 | /** 109 | * @category combinators 110 | * @since 2.2.2 111 | */ 112 | export function partial(properties: { [K in keyof A]: Eq }): Eq> { 113 | return { 114 | equals: (x, y) => { 115 | for (const k in properties) { 116 | const xk = x[k] 117 | const yk = y[k] 118 | if (!(xk === undefined || yk === undefined ? xk === yk : properties[k].equals(xk as any, yk as any))) { 119 | return false 120 | } 121 | } 122 | return true 123 | } 124 | } 125 | } 126 | 127 | /** 128 | * @category combinators 129 | * @since 2.2.2 130 | */ 131 | export const record: (codomain: Eq) => Eq> = R.getEq 132 | 133 | /** 134 | * @category combinators 135 | * @since 2.2.2 136 | */ 137 | export const array: (eq: Eq) => Eq> = A.getEq 138 | 139 | /** 140 | * @category combinators 141 | * @since 2.2.2 142 | */ 143 | export const tuple: >(...components: { [K in keyof A]: Eq }) => Eq = 144 | E.getTupleEq as any 145 | 146 | /** 147 | * @category combinators 148 | * @since 2.2.2 149 | */ 150 | export const intersect = 151 | (right: Eq) => 152 | (left: Eq): Eq => ({ 153 | equals: (x, y) => left.equals(x, y) && right.equals(x, y) 154 | }) 155 | 156 | /** 157 | * @category combinators 158 | * @since 2.2.2 159 | */ 160 | export function sum( 161 | tag: T 162 | ): (members: { [K in keyof A]: Eq> }) => Eq { 163 | return (members: Record>) => { 164 | return { 165 | equals: (x: any, y: any) => { 166 | const vx = x[tag] 167 | const vy = y[tag] 168 | if (vx !== vy) { 169 | return false 170 | } 171 | return members[vx].equals(x, y) 172 | } 173 | } 174 | } 175 | } 176 | 177 | /** 178 | * @category combinators 179 | * @since 2.2.2 180 | */ 181 | export function lazy(f: () => Eq): Eq { 182 | const get = memoize>(f) 183 | return { 184 | equals: (x, y) => get().equals(x, y) 185 | } 186 | } 187 | 188 | /** 189 | * @category combinators 190 | * @since 2.2.15 191 | */ 192 | export const readonly: (eq: Eq) => Eq> = identity 193 | 194 | // ------------------------------------------------------------------------------------- 195 | // instances 196 | // ------------------------------------------------------------------------------------- 197 | 198 | /** 199 | * @category instances 200 | * @since 2.2.8 201 | */ 202 | export const Schemable: Schemable1 = { 203 | URI: E.URI, 204 | literal: () => E.eqStrict, 205 | string, 206 | number, 207 | boolean, 208 | nullable, 209 | type, 210 | struct, 211 | partial, 212 | record, 213 | array, 214 | tuple, 215 | intersect, 216 | sum, 217 | lazy: (_, f) => lazy(f), 218 | readonly 219 | } 220 | 221 | /** 222 | * @category instances 223 | * @since 2.2.8 224 | */ 225 | export const WithUnknownContainers: WithUnknownContainers1 = { 226 | UnknownArray, 227 | UnknownRecord 228 | } 229 | 230 | /** 231 | * @category instances 232 | * @since 2.2.8 233 | */ 234 | export const WithRefine: WithRefine1 = { 235 | refine: () => (from) => from 236 | } 237 | -------------------------------------------------------------------------------- /src/FreeSemigroup.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * **This module is experimental** 3 | * 4 | * Experimental features are published in order to get early feedback from the community, see these tracking 5 | * [issues](https://github.com/gcanti/io-ts/issues?q=label%3Av2.2+) for further discussions and enhancements. 6 | * 7 | * A feature tagged as _Experimental_ is in a high state of flux, you're at risk of it changing without notice. 8 | * 9 | * @since 2.2.7 10 | */ 11 | import { Semigroup } from 'fp-ts/lib/Semigroup' 12 | 13 | /** 14 | * @category model 15 | * @since 2.2.7 16 | */ 17 | export interface Of { 18 | readonly _tag: 'Of' 19 | readonly value: A 20 | } 21 | 22 | /** 23 | * @category model 24 | * @since 2.2.7 25 | */ 26 | export interface Concat { 27 | readonly _tag: 'Concat' 28 | readonly left: FreeSemigroup 29 | readonly right: FreeSemigroup 30 | } 31 | 32 | /** 33 | * @category model 34 | * @since 2.2.7 35 | */ 36 | export type FreeSemigroup = Of | Concat 37 | 38 | /** 39 | * @category constructors 40 | * @since 2.2.7 41 | */ 42 | export const of = (a: A): FreeSemigroup => ({ _tag: 'Of', value: a }) 43 | 44 | /** 45 | * @category constructors 46 | * @since 2.2.7 47 | */ 48 | export const concat = (left: FreeSemigroup, right: FreeSemigroup): FreeSemigroup => ({ 49 | _tag: 'Concat', 50 | left, 51 | right 52 | }) 53 | 54 | /** 55 | * @category destructors 56 | * @since 2.2.7 57 | */ 58 | export const fold = 59 | (onOf: (value: A) => R, onConcat: (left: FreeSemigroup, right: FreeSemigroup) => R) => 60 | (f: FreeSemigroup): R => { 61 | switch (f._tag) { 62 | case 'Of': 63 | return onOf(f.value) 64 | case 'Concat': 65 | return onConcat(f.left, f.right) 66 | } 67 | } 68 | 69 | /** 70 | * @category instances 71 | * @since 2.2.7 72 | */ 73 | export function getSemigroup(): Semigroup> { 74 | return { concat } 75 | } 76 | -------------------------------------------------------------------------------- /src/PathReporter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 1.0.0 3 | */ 4 | import { fold } from 'fp-ts/lib/Either' 5 | 6 | import { Context, getFunctionName, ValidationError } from '.' 7 | import { Reporter } from './Reporter' 8 | 9 | function stringify(v: any): string { 10 | if (typeof v === 'function') { 11 | return getFunctionName(v) 12 | } 13 | if (typeof v === 'number' && !isFinite(v)) { 14 | if (isNaN(v)) { 15 | return 'NaN' 16 | } 17 | return v > 0 ? 'Infinity' : '-Infinity' 18 | } 19 | return JSON.stringify(v) 20 | } 21 | 22 | function getContextPath(context: Context): string { 23 | return context.map(({ key, type }) => `${key}: ${type.name}`).join('/') 24 | } 25 | 26 | function getMessage(e: ValidationError): string { 27 | return e.message !== undefined 28 | ? e.message 29 | : `Invalid value ${stringify(e.value)} supplied to ${getContextPath(e.context)}` 30 | } 31 | 32 | /** 33 | * @since 1.0.0 34 | */ 35 | export function failure(es: Array): Array { 36 | return es.map(getMessage) 37 | } 38 | 39 | /** 40 | * @since 1.0.0 41 | */ 42 | export function success(): Array { 43 | return ['No errors!'] 44 | } 45 | 46 | /** 47 | * @since 1.0.0 48 | */ 49 | export const PathReporter: Reporter> = { 50 | report: fold(failure, success) 51 | } 52 | -------------------------------------------------------------------------------- /src/Reporter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 1.0.0 3 | */ 4 | import { Validation } from './index' 5 | 6 | /** 7 | * @since 1.0.0 8 | */ 9 | export interface Reporter { 10 | report: (validation: Validation) => A 11 | } 12 | -------------------------------------------------------------------------------- /src/Schema.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * **This module is experimental** 3 | * 4 | * Experimental features are published in order to get early feedback from the community, see these tracking 5 | * [issues](https://github.com/gcanti/io-ts/issues?q=label%3Av2.2+) for further discussions and enhancements. 6 | * 7 | * A feature tagged as _Experimental_ is in a high state of flux, you're at risk of it changing without notice. 8 | * 9 | * @since 2.2.0 10 | */ 11 | import { HKT, Kind, Kind2, URIS, URIS2 } from 'fp-ts/lib/HKT' 12 | 13 | import { memoize, Schemable, Schemable1, Schemable2C } from './Schemable' 14 | 15 | // ------------------------------------------------------------------------------------- 16 | // model 17 | // ------------------------------------------------------------------------------------- 18 | 19 | /** 20 | * @category model 21 | * @since 2.2.0 22 | */ 23 | export interface Schema { 24 | (S: Schemable): HKT 25 | } 26 | 27 | // ------------------------------------------------------------------------------------- 28 | // constructors 29 | // ------------------------------------------------------------------------------------- 30 | 31 | /** 32 | * @category constructors 33 | * @since 2.2.0 34 | */ 35 | export function make(schema: Schema): Schema { 36 | return memoize(schema) 37 | } 38 | 39 | // ------------------------------------------------------------------------------------- 40 | // utils 41 | // ------------------------------------------------------------------------------------- 42 | 43 | /** 44 | * @since 2.2.0 45 | */ 46 | export type TypeOf = S extends Schema ? A : never 47 | 48 | /** 49 | * @since 2.2.3 50 | */ 51 | export function interpreter(S: Schemable2C): (schema: Schema) => Kind2 52 | export function interpreter(S: Schemable1): (schema: Schema) => Kind 53 | export function interpreter(S: Schemable): (schema: Schema) => HKT { 54 | return (schema) => schema(S) 55 | } 56 | -------------------------------------------------------------------------------- /src/ThrowReporter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @deprecated 3 | * @since 1.0.0 4 | */ 5 | import { isLeft } from 'fp-ts/lib/Either' 6 | 7 | import { PathReporter } from './PathReporter' 8 | import { Reporter } from './Reporter' 9 | 10 | /** 11 | * @category deprecated 12 | * @since 1.0.0 13 | * @deprecated 14 | */ 15 | export const ThrowReporter: Reporter = { 16 | report: (validation) => { 17 | if (isLeft(validation)) { 18 | throw new Error(PathReporter.report(validation).join('\n')) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * **This module is experimental** 3 | * 4 | * Experimental features are published in order to get early feedback from the community, see these tracking 5 | * [issues](https://github.com/gcanti/io-ts/issues?q=label%3Av2.2+) for further discussions and enhancements. 6 | * 7 | * A feature tagged as _Experimental_ is in a high state of flux, you're at risk of it changing without notice. 8 | * 9 | * @since 2.2.3 10 | */ 11 | import * as E from 'fp-ts/lib/Either' 12 | import { identity, Refinement } from 'fp-ts/lib/function' 13 | import { pipe } from 'fp-ts/lib/pipeable' 14 | 15 | import * as t from './index' 16 | import * as S from './Schemable' 17 | 18 | // ------------------------------------------------------------------------------------- 19 | // model 20 | // ------------------------------------------------------------------------------------- 21 | 22 | /** 23 | * @category model 24 | * @since 2.2.3 25 | */ 26 | export interface Type extends t.Type {} 27 | 28 | // ------------------------------------------------------------------------------------- 29 | // constructors 30 | // ------------------------------------------------------------------------------------- 31 | 32 | /** 33 | * @category constructors 34 | * @since 2.2.3 35 | */ 36 | export const literal = ], L extends S.Literal = S.Literal>( 37 | ...values: A 38 | ): Type => t.union(values.map((v) => t.literal(v as any)) as any) 39 | 40 | // ------------------------------------------------------------------------------------- 41 | // primitives 42 | // ------------------------------------------------------------------------------------- 43 | 44 | /** 45 | * @category primitives 46 | * @since 2.2.3 47 | */ 48 | export const string: Type = t.string 49 | 50 | /** 51 | * @category primitives 52 | * @since 2.2.3 53 | */ 54 | export const number: Type = new t.Type( 55 | t.number.name, 56 | t.number.is, 57 | (u, c) => 58 | pipe( 59 | t.number.decode(u), 60 | E.chain((n) => (isNaN(n) ? t.failure(u, c) : t.success(n))) 61 | ), 62 | t.number.encode 63 | ) 64 | 65 | /** 66 | * @category primitives 67 | * @since 2.2.3 68 | */ 69 | export const boolean: Type = t.boolean 70 | 71 | /** 72 | * @category primitives 73 | * @since 2.2.3 74 | */ 75 | export const UnknownArray: Type> = t.UnknownArray 76 | 77 | /** 78 | * @category primitives 79 | * @since 2.2.3 80 | */ 81 | export const UnknownRecord: Type> = t.UnknownRecord 82 | 83 | // ------------------------------------------------------------------------------------- 84 | // combinators 85 | // ------------------------------------------------------------------------------------- 86 | 87 | /** 88 | * @category combinators 89 | * @since 2.2.3 90 | */ 91 | export const refine = 92 | (refinement: Refinement, id: string) => 93 | (from: Type): Type => 94 | // tslint:disable-next-line: deprecation 95 | t.refinement(from, refinement, id) as any 96 | 97 | /** 98 | * @category combinators 99 | * @since 2.2.3 100 | */ 101 | export const nullable = (or: Type): Type => t.union([t.null, or]) 102 | 103 | /** 104 | * @category combinators 105 | * @since 2.2.15 106 | */ 107 | export const struct = (properties: { [K in keyof A]: Type }): Type<{ [K in keyof A]: A[K] }> => 108 | t.type(properties) as any 109 | 110 | /** 111 | * Use `struct` instead. 112 | * 113 | * @category combinators 114 | * @since 2.2.3 115 | * @deprecated 116 | */ 117 | export const type = struct 118 | 119 | /** 120 | * @category combinators 121 | * @since 2.2.3 122 | */ 123 | export const partial = (properties: { [K in keyof A]: Type }): Type> => 124 | t.partial(properties) 125 | 126 | /** 127 | * @category combinators 128 | * @since 2.2.3 129 | */ 130 | export const record = (codomain: Type): Type> => t.record(t.string, codomain) 131 | 132 | /** 133 | * @category combinators 134 | * @since 2.2.3 135 | */ 136 | export const array = (item: Type): Type> => t.array(item) 137 | 138 | /** 139 | * @category combinators 140 | * @since 2.2.3 141 | */ 142 | export const tuple = >(...components: { [K in keyof A]: Type }): Type => 143 | t.tuple(components as any) as any 144 | 145 | /** 146 | * @category combinators 147 | * @since 2.2.3 148 | */ 149 | export const intersect = 150 | (right: Type) => 151 | (left: Type): Type => 152 | t.intersection([left, right]) 153 | 154 | /** 155 | * @category combinators 156 | * @since 2.2.3 157 | */ 158 | export const lazy = (id: string, f: () => Type): Type => t.recursion(id, f) 159 | 160 | /** 161 | * @category combinators 162 | * @since 2.2.15 163 | */ 164 | export const readonly: (type: Type) => Type> = identity 165 | 166 | /** 167 | * @category combinators 168 | * @since 2.2.3 169 | */ 170 | export const sum = 171 | (_tag: T) => 172 | (members: { [K in keyof A]: Type> }): Type => 173 | t.union(Object.values(members) as any) 174 | 175 | /** 176 | * @category combinators 177 | * @since 2.2.3 178 | */ 179 | export const union = ]>( 180 | ...members: { [K in keyof A]: Type } 181 | ): Type => t.union(members as any) 182 | 183 | // ------------------------------------------------------------------------------------- 184 | // instances 185 | // ------------------------------------------------------------------------------------- 186 | 187 | /** 188 | * @category instances 189 | * @since 2.2.3 190 | */ 191 | export const URI = 'io-ts/Type' 192 | 193 | /** 194 | * @category instances 195 | * @since 2.2.3 196 | */ 197 | export type URI = typeof URI 198 | 199 | declare module 'fp-ts/lib/HKT' { 200 | interface URItoKind { 201 | readonly [URI]: Type 202 | } 203 | } 204 | 205 | /** 206 | * @category instances 207 | * @since 2.2.8 208 | */ 209 | export const Schemable: S.Schemable1 = { 210 | URI, 211 | literal, 212 | string, 213 | number, 214 | boolean, 215 | nullable, 216 | type, 217 | struct, 218 | partial, 219 | record, 220 | array, 221 | tuple: tuple as S.Schemable1['tuple'], 222 | intersect, 223 | sum, 224 | lazy, 225 | readonly 226 | } 227 | 228 | /** 229 | * @category instances 230 | * @since 2.2.8 231 | */ 232 | export const WithUnknownContainers: S.WithUnknownContainers1 = { 233 | UnknownArray, 234 | UnknownRecord 235 | } 236 | 237 | /** 238 | * @category instances 239 | * @since 2.2.8 240 | */ 241 | export const WithUnion: S.WithUnion1 = { 242 | union: union as S.WithUnion1['union'] 243 | } 244 | 245 | /** 246 | * @category instances 247 | * @since 2.2.8 248 | */ 249 | export const WithRefine: S.WithRefine1 = { 250 | refine: refine as S.WithRefine1['refine'] 251 | } 252 | -------------------------------------------------------------------------------- /test/2.1.x/PathReporter.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | 3 | import * as t from '../../src' 4 | import { PathReporter } from '../../src/PathReporter' 5 | import { NumberFromString } from './helpers' 6 | 7 | describe.concurrent('PathReporter', () => { 8 | it('should use the function name as error message', () => { 9 | // eslint-disable-next-line @typescript-eslint/no-empty-function 10 | assert.deepStrictEqual(PathReporter.report(t.number.decode(function () {})), [ 11 | 'Invalid value supplied to : number' 12 | ]) 13 | // eslint-disable-next-line @typescript-eslint/no-empty-function 14 | assert.deepStrictEqual(PathReporter.report(t.number.decode(function f() {})), [ 15 | 'Invalid value f supplied to : number' 16 | ]) 17 | }) 18 | 19 | it('should say something when there are no errors', () => { 20 | assert.deepStrictEqual(PathReporter.report(t.number.decode(1)), ['No errors!']) 21 | }) 22 | 23 | it('should account for the optional message field', () => { 24 | assert.deepStrictEqual(PathReporter.report(NumberFromString.decode('a')), ['cannot parse to a number']) 25 | }) 26 | 27 | it('should handle NaN', () => { 28 | assert.deepStrictEqual(PathReporter.report(t.string.decode(NaN)), ['Invalid value NaN supplied to : string']) 29 | }) 30 | 31 | it('should handle Infinity', () => { 32 | assert.deepStrictEqual(PathReporter.report(t.string.decode(Infinity)), [ 33 | 'Invalid value Infinity supplied to : string' 34 | ]) 35 | assert.deepStrictEqual(PathReporter.report(t.string.decode(-Infinity)), [ 36 | 'Invalid value -Infinity supplied to : string' 37 | ]) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /test/2.1.x/ThrowReporter.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | 3 | import * as t from '../../src' 4 | import { ThrowReporter } from '../../src/ThrowReporter' 5 | 6 | describe.concurrent('ThrowReporter', () => { 7 | it('should throw on invalid inputs', () => { 8 | assert.throws(() => { 9 | // tslint:disable-next-line: deprecation 10 | ThrowReporter.report(t.string.decode(1)) 11 | }) 12 | }) 13 | 14 | it('should not throw on invalid inputs', () => { 15 | assert.doesNotThrow(() => { 16 | // tslint:disable-next-line: deprecation 17 | ThrowReporter.report(t.string.decode('a')) 18 | }) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /test/2.1.x/TypeClass.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import { Either, fold, right } from 'fp-ts/lib/Either' 3 | import { pipe } from 'fp-ts/lib/pipeable' 4 | 5 | import * as t from '../../src/index' 6 | import { assertFailure, assertSuccess } from './helpers' 7 | 8 | const BAA = new t.Type( 9 | 'BAA', 10 | t.number.is, 11 | (s, c) => { 12 | const n = parseFloat(s) 13 | return isNaN(n) ? t.failure(s, c) : t.success(n) 14 | }, 15 | (n) => String(n) 16 | ) 17 | 18 | const BAI = t.string.pipe(BAA, 'T') 19 | 20 | describe.concurrent('Type', () => { 21 | it('should auto bind decode', () => { 22 | function clone(t: C): C { 23 | const r = Object.create(Object.getPrototypeOf(t)) 24 | ;(Object as any).assign(r, t) 25 | return r 26 | } 27 | 28 | const T = t.string 29 | const decode = (f: (u: unknown) => Either, u: unknown): boolean => 30 | pipe( 31 | f(u), 32 | fold( 33 | () => false, 34 | () => true 35 | ) 36 | ) 37 | assert.strictEqual(decode(T.decode, 'a'), true) 38 | assert.strictEqual(decode(clone(T).decode, 'a'), true) 39 | type A = { 40 | a: A | null 41 | } 42 | const A: t.Type = t.recursion('A', () => 43 | t.type({ 44 | a: t.union([A, t.null]) 45 | }) 46 | ) 47 | assert.strictEqual(decode(clone(A).decode, { a: { a: null } }), true) 48 | }) 49 | 50 | describe.concurrent('pipe', () => { 51 | it('should assign a default name', () => { 52 | const AOI = t.string 53 | const T = AOI.pipe(BAA) 54 | assert.strictEqual(T.name, 'pipe(string, BAA)') 55 | }) 56 | 57 | it('should combine two types', () => { 58 | assertSuccess(BAI.decode('1')) 59 | assertFailure(BAI, 1, ['Invalid value 1 supplied to : T']) 60 | assertFailure(BAI, 'a', ['Invalid value "a" supplied to : T']) 61 | assert.strictEqual(BAI.encode(2), '2') 62 | }) 63 | 64 | it('should ude identity as decoder function', () => { 65 | assert.strictEqual(t.string.pipe(t.string as t.Type).encode, t.identity) 66 | }) 67 | 68 | it('accept to pipe a type with a wider input', () => { 69 | const T = t.string.pipe(t.string) 70 | assert.deepStrictEqual(T.decode('a'), right('a')) 71 | assert.strictEqual(T.encode('a'), 'a') 72 | }) 73 | 74 | it('accept to pipe a type with a narrower output', () => { 75 | const T = t.string.pipe(t.literal('foo')) 76 | assert.deepStrictEqual(T.decode('foo'), right('foo')) 77 | assert.strictEqual(T.encode('foo'), 'foo') 78 | }) 79 | }) 80 | 81 | describe.concurrent('asDecoder', () => { 82 | it('should return a decoder', () => { 83 | assertSuccess(t.string.asDecoder().decode('1')) 84 | }) 85 | }) 86 | 87 | describe.concurrent('asEncoder', () => { 88 | it('should return an encoder', () => { 89 | assert.strictEqual(BAI.asEncoder().encode(2), '2') 90 | }) 91 | }) 92 | }) 93 | 94 | describe.concurrent('getContextEntry', () => { 95 | it('should return a ContextEntry', () => { 96 | assert.deepStrictEqual(t.getContextEntry('key', t.string), { 97 | key: 'key', 98 | type: t.string 99 | }) 100 | }) 101 | }) 102 | 103 | describe.concurrent('clean', () => { 104 | it('should return the same type', () => { 105 | const T = t.type({ a: t.string }) 106 | // tslint:disable-next-line: deprecation 107 | assert.strictEqual(t.clean(T), T) 108 | }) 109 | }) 110 | 111 | describe.concurrent('alias', () => { 112 | it('should return the same type', () => { 113 | const T = t.type({ a: t.string }) 114 | assert.strictEqual(t.alias(T)(), T) 115 | }) 116 | }) 117 | -------------------------------------------------------------------------------- /test/2.1.x/array.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | 3 | import * as t from '../../src/index' 4 | import { assertFailure, assertStrictEqual, assertStrictSuccess, assertSuccess, NumberFromString } from './helpers' 5 | 6 | describe.concurrent('array', () => { 7 | describe.concurrent('name', () => { 8 | it('should assign a default name', () => { 9 | const T = t.array(t.number) 10 | assert.strictEqual(T.name, 'Array') 11 | }) 12 | 13 | it('should accept a name', () => { 14 | const T = t.array(t.number, 'T') 15 | assert.strictEqual(T.name, 'T') 16 | }) 17 | }) 18 | 19 | describe.concurrent('is', () => { 20 | it('should check a isomorphic value', () => { 21 | const T = t.array(t.number) 22 | assert.strictEqual(T.is([]), true) 23 | assert.strictEqual(T.is([0]), true) 24 | assert.strictEqual(T.is([0, 'foo']), false) 25 | }) 26 | }) 27 | 28 | describe.concurrent('decode', () => { 29 | it('should decode a isomorphic value', () => { 30 | const T = t.array(t.number) 31 | assertSuccess(T.decode([]), []) 32 | assertSuccess(T.decode([1, 2, 3]), [1, 2, 3]) 33 | }) 34 | 35 | it('should return the same reference while decoding isomorphic values', () => { 36 | const T = t.array(t.number) 37 | const value = [1, 2, 3] 38 | assertStrictSuccess(T.decode(value), value) 39 | }) 40 | 41 | it('should decode a prismatic value', () => { 42 | const T = t.array(NumberFromString) 43 | assertSuccess(T.decode([]), []) 44 | assertSuccess(T.decode(['1', '2', '3']), [1, 2, 3]) 45 | }) 46 | 47 | it('should fail decoding an invalid value', () => { 48 | const T = t.array(t.number) 49 | assertFailure(T, 1, ['Invalid value 1 supplied to : Array']) 50 | assertFailure(T, [1, 's', 3], ['Invalid value "s" supplied to : Array/1: number']) 51 | }) 52 | }) 53 | 54 | describe.concurrent('encode', () => { 55 | it('should encode a isomorphic value', () => { 56 | const T = t.array(t.number) 57 | assert.deepStrictEqual(T.encode([1, 2, 3]), [1, 2, 3]) 58 | }) 59 | 60 | it('should return the same reference while encoding isomorphic values', () => { 61 | const T = t.array(t.number) 62 | const value = [1, 2, 3] 63 | assert.strictEqual(T.encode(value), value) 64 | }) 65 | 66 | it('should encode a prismatic value', () => { 67 | const T = t.array(NumberFromString) 68 | assert.deepStrictEqual(T.encode([1, 2, 3]), ['1', '2', '3']) 69 | }) 70 | }) 71 | 72 | it('should return the same reference if validation succeeded and nothing changed', () => { 73 | const T = t.array(t.number) 74 | const value = [1, 2, 3] 75 | assertStrictEqual(T.decode(value), value) 76 | }) 77 | 78 | it('should return the same reference while encoding', () => { 79 | const T = t.array(t.number) 80 | assert.strictEqual(T.encode, t.identity) 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /test/2.1.x/brand.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | 3 | import * as t from '../../src/index' 4 | import { assertFailure, assertStrictEqual, assertSuccess, NumberFromString } from './helpers' 5 | 6 | interface PositiveBrand { 7 | readonly Positive: unique symbol 8 | } 9 | 10 | const Positive = t.brand(t.number, (n): n is t.Branded => n >= 0, 'Positive') 11 | 12 | interface MyDictionaryBrand { 13 | readonly MyDictionary: unique symbol 14 | } 15 | 16 | const MyDictionary = t.brand( 17 | t.UnknownRecord, 18 | (_): _ is t.Branded, MyDictionaryBrand> => true, 19 | 'MyDictionary' 20 | ) 21 | 22 | interface MyNumberArrayBrand { 23 | readonly MyNumberArray: unique symbol 24 | } 25 | 26 | const MyNumberArray = t.brand( 27 | t.array(t.number), 28 | (_): _ is t.Branded, MyNumberArrayBrand> => true, 29 | 'MyNumberArray' 30 | ) 31 | 32 | interface MyNumberFromStringArrayBrand { 33 | readonly MyNumberFromStringArray: unique symbol 34 | } 35 | 36 | const MyNumberFromStringArray = t.brand( 37 | t.array(NumberFromString), 38 | (_): _ is t.Branded, MyNumberFromStringArrayBrand> => true, 39 | 'MyNumberFromStringArray' 40 | ) 41 | 42 | const IntFromString = NumberFromString.pipe(t.Int, 'IntFromString') 43 | 44 | describe.concurrent('brand', () => { 45 | describe.concurrent('name', () => { 46 | it('should accept a name', () => { 47 | const T = Positive 48 | assert.strictEqual(T.name, 'Positive') 49 | }) 50 | }) 51 | 52 | describe.concurrent('is', () => { 53 | it('should check a isomorphic value', () => { 54 | const T = t.Int 55 | assert.strictEqual(T.is(1.2), false) 56 | assert.strictEqual(T.is('a'), false) 57 | assert.strictEqual(T.is(1), true) 58 | }) 59 | 60 | it('should check a prismatic value', () => { 61 | const T = IntFromString 62 | assert.strictEqual(T.is(1.2), false) 63 | assert.strictEqual(T.is('a'), false) 64 | assert.strictEqual(T.is(1), true) 65 | }) 66 | }) 67 | 68 | describe.concurrent('decode', () => { 69 | it('should succeed validating a valid value', () => { 70 | const T = Positive 71 | assertSuccess(T.decode(0)) 72 | assertSuccess(T.decode(1)) 73 | }) 74 | 75 | it('should return the same reference if validation succeeded', () => { 76 | const T = MyDictionary 77 | const value = {} 78 | assertStrictEqual(T.decode(value), value) 79 | }) 80 | 81 | it('should fail validating an invalid value', () => { 82 | const T = t.Int 83 | assertFailure(T, 'a', ['Invalid value "a" supplied to : Int']) 84 | assertFailure(T, 1.2, ['Invalid value 1.2 supplied to : Int']) 85 | }) 86 | 87 | it('should fail with the last deserialized value', () => { 88 | const T = IntFromString 89 | assertFailure(T, 'a', ['cannot parse to a number']) 90 | assertFailure(T, '1.2', ['Invalid value 1.2 supplied to : IntFromString']) 91 | }) 92 | }) 93 | 94 | describe.concurrent('encode', () => { 95 | it('should encode a prismatic value', () => { 96 | const T = MyNumberFromStringArray 97 | assert.deepStrictEqual(T.encode([1] as any), ['1']) 98 | }) 99 | 100 | it('should return the same reference while encoding', () => { 101 | const T = MyNumberArray 102 | assert.strictEqual(T.encode, t.identity) 103 | }) 104 | }) 105 | }) 106 | -------------------------------------------------------------------------------- /test/2.1.x/exact.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | 3 | import * as t from '../../src/index' 4 | import { assertFailure, assertStrictEqual, assertSuccess, NumberFromString } from './helpers' 5 | 6 | describe.concurrent('exact', () => { 7 | describe.concurrent('name', () => { 8 | it('should assign a default name', () => { 9 | const T = t.exact(t.type({ foo: t.string })) 10 | assert.strictEqual(T.name, '{| foo: string |}') 11 | }) 12 | 13 | it('should accept a name', () => { 14 | const T = t.exact(t.type({ foo: t.string }), 'Foo') 15 | assert.strictEqual(T.name, 'Foo') 16 | }) 17 | }) 18 | 19 | describe.concurrent('is', () => { 20 | it('should check a isomorphic value', () => { 21 | const T = t.exact(t.type({ a: t.number })) 22 | assert.strictEqual(T.is({ a: 0 }), true) 23 | assert.strictEqual(T.is({ a: 0, b: 1 }), true) 24 | assert.strictEqual(T.is(undefined), false) 25 | }) 26 | 27 | it('should check a prismatic value', () => { 28 | const T = t.exact(t.type({ a: NumberFromString })) 29 | assert.strictEqual(T.is({ a: 1 }), true) 30 | assert.strictEqual(T.is({ a: 1, b: 1 }), true) 31 | assert.strictEqual(T.is(undefined), false) 32 | }) 33 | }) 34 | 35 | describe.concurrent('decode', () => { 36 | it('should succeed validating a valid value (type)', () => { 37 | const T = t.exact(t.type({ foo: t.string })) 38 | assertSuccess(T.decode({ foo: 'foo' })) 39 | }) 40 | 41 | it('should succeed validating a valid value (partial)', () => { 42 | const T = t.exact(t.partial({ foo: t.string })) 43 | assertSuccess(T.decode({ foo: 'foo' })) 44 | assertSuccess(T.decode({ foo: undefined })) 45 | assertSuccess(T.decode({})) 46 | }) 47 | 48 | it('should succeed validating a valid value (intersection)', () => { 49 | const T = t.exact(t.intersection([t.type({ foo: t.string }), t.partial({ bar: t.number })])) 50 | assertSuccess(T.decode({ foo: 'foo', bar: 1 })) 51 | assertSuccess(T.decode({ foo: 'foo', bar: undefined })) 52 | assertSuccess(T.decode({ foo: 'foo' })) 53 | }) 54 | 55 | it('should succeed validating a valid value (refinement)', () => { 56 | const T = t.exact(t.refinement(t.type({ foo: t.string }), (p) => p.foo.length > 2)) 57 | assertSuccess(T.decode({ foo: 'foo' })) 58 | }) 59 | 60 | it('should succeed validating a valid value (readonly)', () => { 61 | const T = t.exact(t.readonly(t.type({ foo: t.string }))) 62 | assertSuccess(T.decode({ foo: 'foo' })) 63 | }) 64 | 65 | it('should succeed validating an undefined field', () => { 66 | const T = t.exact(t.type({ foo: t.string, bar: t.union([t.string, t.undefined]) })) 67 | assertSuccess(T.decode({ foo: 'foo' })) 68 | }) 69 | 70 | it('should return the same reference if validation succeeded', () => { 71 | const T = t.exact(t.type({ foo: t.string })) 72 | const value = { foo: 'foo' } 73 | assertStrictEqual(T.decode(value), value) 74 | }) 75 | 76 | it('should fail validating an invalid value (type)', () => { 77 | const T = t.exact(t.type({ foo: t.string })) 78 | assertFailure(T, null, ['Invalid value null supplied to : {| foo: string |}']) 79 | assertFailure(T, undefined, ['Invalid value undefined supplied to : {| foo: string |}']) 80 | assertFailure(T, 1, ['Invalid value 1 supplied to : {| foo: string |}']) 81 | assertFailure(T, {}, ['Invalid value undefined supplied to : {| foo: string |}/foo: string']) 82 | }) 83 | 84 | it('should strip additional properties (type)', () => { 85 | const T = t.exact(t.type({ foo: t.string })) 86 | assertSuccess(T.decode({ foo: 'foo', bar: 1, baz: true }), { foo: 'foo' }) 87 | }) 88 | 89 | it('should fail validating an invalid value (partial)', () => { 90 | const T = t.exact(t.partial({ foo: t.string })) 91 | assertFailure(T, null, ['Invalid value null supplied to : Partial<{| foo: string |}>']) 92 | }) 93 | 94 | it('should strip additional properties (partial)', () => { 95 | const T = t.exact(t.partial({ foo: t.string })) 96 | assertSuccess(T.decode({ bar: 1 }), {}) 97 | }) 98 | 99 | it('should fail validating an invalid value (intersection)', () => { 100 | const T = t.exact(t.intersection([t.type({ foo: t.string }), t.partial({ bar: t.number })])) 101 | assertFailure(T, null, ['Invalid value null supplied to : Exact<({ foo: string } & Partial<{ bar: number }>)>']) 102 | }) 103 | 104 | it('should strip additional properties (intersection)', () => { 105 | const T = t.exact(t.intersection([t.type({ foo: t.string }), t.partial({ bar: t.number })])) 106 | assertSuccess(T.decode({ foo: 'foo', baz: true }), { foo: 'foo' }) 107 | }) 108 | 109 | it('should fail validating an invalid value (refinement)', () => { 110 | const T = t.exact(t.refinement(t.type({ foo: t.string }), (p) => p.foo.length > 2)) 111 | assertFailure(T, null, ['Invalid value null supplied to : Exact<({ foo: string } | )>']) 112 | assertFailure(T, { foo: 'a' }, ['Invalid value {"foo":"a"} supplied to : Exact<({ foo: string } | )>']) 113 | }) 114 | 115 | it('should strip additional properties (refinement)', () => { 116 | const T = t.exact(t.refinement(t.type({ foo: t.string }), (p) => p.foo.length > 2)) 117 | assertSuccess(T.decode({ foo: 'foo', bar: 1 }), { foo: 'foo' }) 118 | }) 119 | 120 | it('should fail validating an invalid value (readonly)', () => { 121 | const T = t.exact(t.readonly(t.type({ foo: t.string }))) 122 | assertFailure(T, null, ['Invalid value null supplied to : Exact>']) 123 | }) 124 | 125 | it('should strip additional properties (readonly)', () => { 126 | const T = t.exact(t.readonly(t.type({ foo: t.string }))) 127 | assertSuccess(T.decode({ foo: 'foo', bar: 1 }), { foo: 'foo' }) 128 | }) 129 | }) 130 | 131 | describe.concurrent('encode', () => { 132 | it('should encode a prismatic value', () => { 133 | const T = t.exact(t.type({ a: NumberFromString })) 134 | assert.deepStrictEqual(T.encode({ a: 1 }), { a: '1' }) 135 | }) 136 | 137 | it('should return the same reference while encoding', () => { 138 | const T = t.exact(t.type({ a: t.string })) 139 | const x = { a: 'a' } 140 | assert.strictEqual(T.encode(x), x) 141 | }) 142 | 143 | it('should strip additional properties', () => { 144 | const T = t.exact(t.type({ a: t.string })) 145 | const x = { a: 'a', b: 1 } 146 | assert.deepStrictEqual(T.encode(x), { a: 'a' }) 147 | }) 148 | }) 149 | }) 150 | -------------------------------------------------------------------------------- /test/2.1.x/helpers.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import { either, fold, right } from 'fp-ts/lib/Either' 3 | import { pipe } from 'fp-ts/lib/pipeable' 4 | 5 | import * as t from '../../src/index' 6 | import { PathReporter } from '../../src/PathReporter' 7 | 8 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types 9 | export function assertStrictEqual(result: t.Validation, expected: any): void { 10 | pipe( 11 | result, 12 | fold( 13 | /* istanbul ignore next */ 14 | () => { 15 | throw new Error(`${result} is not a right`) 16 | }, 17 | (a) => { 18 | assert.deepStrictEqual(a, expected) 19 | } 20 | ) 21 | ) 22 | } 23 | 24 | export function assertSuccess(result: t.Validation, expected?: T): void { 25 | pipe( 26 | result, 27 | fold( 28 | /* istanbul ignore next */ 29 | () => { 30 | throw new Error(`${result} is not a right`) 31 | }, 32 | (a) => { 33 | if (expected !== undefined) { 34 | assert.deepStrictEqual(a, expected) 35 | } 36 | } 37 | ) 38 | ) 39 | } 40 | 41 | export function assertStrictSuccess(result: t.Validation, expected: T): void { 42 | pipe( 43 | result, 44 | fold( 45 | /* istanbul ignore next */ 46 | () => { 47 | throw new Error(`${result} is not a right`) 48 | }, 49 | (a) => { 50 | /* istanbul ignore next */ 51 | if (expected !== undefined) { 52 | assert.strictEqual(a, expected) 53 | } 54 | } 55 | ) 56 | ) 57 | } 58 | 59 | export function assertFailure(codec: t.Any, value: unknown, errors: Array): void { 60 | const result = codec.decode(value) 61 | pipe( 62 | result, 63 | fold( 64 | () => { 65 | assert.deepStrictEqual(PathReporter.report(result), errors) 66 | }, 67 | /* istanbul ignore next */ 68 | () => { 69 | throw new Error(`${result} is not a left`) 70 | } 71 | ) 72 | ) 73 | } 74 | 75 | export const NumberFromString = new t.Type( 76 | 'NumberFromString', 77 | t.number.is, 78 | (u, c) => 79 | either.chain(t.string.validate(u, c), (s) => { 80 | const n = +s 81 | return isNaN(n) ? t.failure(u, c, 'cannot parse to a number') : t.success(n) 82 | }), 83 | String 84 | ) 85 | 86 | export const HyphenatedString = t.refinement( 87 | t.string, 88 | (v): v is `${string}-${string}` => v.length === 3 && v[1] === '-', 89 | '`${string}-${string}`' 90 | ) 91 | 92 | export const HyphenatedStringFromNonHyphenated = new t.Type<`${string}-${string}`, string, unknown>( 93 | 'HyphenatedStringFromNonHyphenated', 94 | HyphenatedString.is, 95 | (u, c) => 96 | either.chain(t.string.validate(u, c), (s) => { 97 | if (s.length === 2) { 98 | return right(`${s[0]}-${s[1]}` as const) 99 | } else { 100 | return t.failure(s, c) 101 | } 102 | }), 103 | (a) => a[0] + a[2] 104 | ) 105 | 106 | export const IntegerFromString = t.refinement(NumberFromString, t.Integer.is, 'IntegerFromString') 107 | 108 | export function withDefault( 109 | type: T, 110 | defaultValue: t.TypeOf 111 | ): t.Type, t.TypeOf, unknown> { 112 | return new t.Type( 113 | `withDefault(${type.name}, ${JSON.stringify(defaultValue)})`, 114 | type.is, 115 | (v) => type.decode(v != null ? v : defaultValue), 116 | type.encode 117 | ) 118 | } 119 | -------------------------------------------------------------------------------- /test/2.1.x/keyof.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | 3 | import * as t from '../../src/index' 4 | import { assertFailure, assertSuccess } from './helpers' 5 | 6 | describe.concurrent('keyof', () => { 7 | describe.concurrent('name', () => { 8 | it('should assign a default name', () => { 9 | const T = t.keyof({ a: 1, b: 2 }) 10 | assert.strictEqual(T.name, '"a" | "b"') 11 | }) 12 | 13 | it('should accept a name', () => { 14 | const T = t.keyof({ a: 1, b: 2 }, 'T') 15 | assert.strictEqual(T.name, 'T') 16 | }) 17 | }) 18 | 19 | describe.concurrent('is', () => { 20 | it('should check a isomorphic value', () => { 21 | const T = t.keyof({ a: 1, b: 2 }) 22 | assert.strictEqual(T.is(null), false) 23 | assert.strictEqual(T.is('a'), true) 24 | assert.strictEqual(T.is('b'), true) 25 | assert.strictEqual(T.is('c'), false) 26 | }) 27 | }) 28 | 29 | describe.concurrent('decode', () => { 30 | it('should decode a isomorphic value', () => { 31 | const T = t.keyof({ a: 1, b: 2 }) 32 | assertSuccess(T.decode('a')) 33 | assertSuccess(T.decode('b')) 34 | }) 35 | 36 | it('should fail decoding an invalid value', () => { 37 | const T = t.keyof({ a: 1, b: 2 }) 38 | assertFailure(T, 'c', ['Invalid value "c" supplied to : "a" | "b"']) 39 | // check for hasOwnProperty oddity: { a: 1 }.hasOwnProperty(['a'] as any) === true 40 | assertFailure(T, ['a'], ['Invalid value ["a"] supplied to : "a" | "b"']) 41 | }) 42 | }) 43 | 44 | it('should return the same reference while encoding', () => { 45 | const T = t.keyof({ a: 1, b: 2 }) 46 | assert.strictEqual(T.encode, t.identity) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /test/2.1.x/literal.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | 3 | import * as t from '../../src/index' 4 | import { assertFailure, assertSuccess } from './helpers' 5 | 6 | describe.concurrent('literal', () => { 7 | describe.concurrent('name', () => { 8 | it('should assign a default name', () => { 9 | const T = t.literal('a') 10 | assert.strictEqual(T.name, '"a"') 11 | }) 12 | 13 | it('should accept a name', () => { 14 | const T = t.literal('a', 'T') 15 | assert.strictEqual(T.name, 'T') 16 | }) 17 | }) 18 | 19 | describe.concurrent('is', () => { 20 | it('should check a isomorphic value', () => { 21 | const T = t.literal('a') 22 | assert.strictEqual(T.is('a'), true) 23 | assert.strictEqual(T.is('b'), false) 24 | }) 25 | }) 26 | 27 | describe.concurrent('decode', () => { 28 | it('should decode a isomorphic value', () => { 29 | const T = t.literal('a') 30 | assertSuccess(T.decode('a')) 31 | }) 32 | 33 | it('should fail validating an invalid value', () => { 34 | const T = t.literal('a') 35 | assertFailure(T, 1, ['Invalid value 1 supplied to : "a"']) 36 | }) 37 | }) 38 | 39 | describe.concurrent('encode', () => { 40 | it('should encode a isomorphic value', () => { 41 | const T = t.literal('a') 42 | assert.strictEqual(T.encode('a'), 'a') 43 | }) 44 | }) 45 | 46 | it('should return the same reference while encoding', () => { 47 | const T = t.literal('a') 48 | assert.strictEqual(T.encode, t.identity) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /test/2.1.x/partial.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | 3 | import * as t from '../../src/index' 4 | import { assertFailure, assertStrictEqual, assertSuccess, NumberFromString, withDefault } from './helpers' 5 | 6 | describe.concurrent('partial', () => { 7 | describe.concurrent('name', () => { 8 | it('should assign a default name', () => { 9 | const T = t.partial({ a: t.number }) 10 | assert.strictEqual(T.name, 'Partial<{ a: number }>') 11 | }) 12 | 13 | it('should accept a name', () => { 14 | const T = t.partial({ a: t.number }, 'T') 15 | assert.strictEqual(T.name, 'T') 16 | }) 17 | }) 18 | 19 | describe.concurrent('is', () => { 20 | it('should return `true` on valid inputs', () => { 21 | const T1 = t.partial({ a: t.number }) 22 | assert.strictEqual(T1.is({}), true) 23 | assert.strictEqual(T1.is({ a: 1 }), true) 24 | 25 | const T2 = t.partial({ a: NumberFromString }) 26 | assert.strictEqual(T2.is({}), true) 27 | assert.strictEqual(T2.is({ a: 1 }), true) 28 | }) 29 | 30 | it('should return `false` on invalid inputs', () => { 31 | const T1 = t.partial({ a: t.number }) 32 | assert.strictEqual(T1.is(undefined), false) 33 | assert.strictEqual(T1.is({ a: 'foo' }), false) 34 | // #407 35 | assert.strictEqual(T1.is([]), false) 36 | 37 | const T2 = t.partial({ a: NumberFromString }) 38 | assert.strictEqual(T2.is(undefined), false) 39 | assert.strictEqual(T2.is({ a: 'foo' }), false) 40 | // #407 41 | assert.strictEqual(T2.is([]), false) 42 | }) 43 | }) 44 | 45 | describe.concurrent('decode', () => { 46 | it('should succeed on valid inputs', () => { 47 | const T = t.partial({ a: t.number }) 48 | assertSuccess(T.decode({}), {}) 49 | assertSuccess(T.decode({ a: undefined }), { a: undefined }) 50 | assertSuccess(T.decode({ a: 1 }), { a: 1 }) 51 | }) 52 | 53 | it('should fail on invalid inputs', () => { 54 | const T = t.partial({ a: t.number }) 55 | assertFailure(T, null, ['Invalid value null supplied to : Partial<{ a: number }>']) 56 | assertFailure(T, { a: 's' }, ['Invalid value "s" supplied to : Partial<{ a: number }>/a: number']) 57 | // #407 58 | assertFailure(T, [], ['Invalid value [] supplied to : Partial<{ a: number }>']) 59 | }) 60 | 61 | it('should not add optional keys', () => { 62 | const T = t.partial({ a: t.number }) 63 | const input1 = {} 64 | assertStrictEqual(T.decode(input1), input1) 65 | const input2 = { a: undefined } 66 | assertStrictEqual(T.decode(input2), input2) 67 | assertSuccess(T.decode({ b: 1 }), { b: 1 } as any) 68 | const input3 = { a: undefined, b: 1 } 69 | assertStrictEqual(T.decode(input3), input3) 70 | }) 71 | 72 | it('should return the same reference if validation succeeded', () => { 73 | const T = t.partial({ a: t.number }) 74 | const value = {} 75 | assertStrictEqual(T.decode(value), value) 76 | }) 77 | 78 | it('should support default values', () => { 79 | const T = t.partial({ 80 | name: withDefault(t.string, 'foo') 81 | }) 82 | assertSuccess(T.decode({}), { name: 'foo' }) 83 | assertSuccess(T.decode({ name: 'a' }), { name: 'a' }) 84 | }) 85 | }) 86 | 87 | describe.concurrent('encode', () => { 88 | it('should encode a isomorphic value', () => { 89 | const T = t.partial({ a: t.number }) 90 | assert.deepStrictEqual(T.encode({}), {}) 91 | assert.deepStrictEqual(T.encode({ a: undefined }), { a: undefined }) 92 | assert.deepStrictEqual(T.encode({ a: 1 }), { a: 1 }) 93 | }) 94 | 95 | it('should encode a prismatic value', () => { 96 | const T = t.partial({ a: NumberFromString }) 97 | assert.deepStrictEqual(T.encode({}), {}) 98 | assert.deepStrictEqual(T.encode({ a: undefined }), { a: undefined }) 99 | assert.deepStrictEqual(T.encode({ a: 1 }), { a: '1' }) 100 | }) 101 | 102 | it('should return the same reference while encoding', () => { 103 | const T = t.partial({ a: t.number }) 104 | assert.strictEqual(T.encode, t.identity) 105 | }) 106 | 107 | it('should preserve additional properties while encoding', () => { 108 | const T = t.partial({ a: NumberFromString }) 109 | const x = { a: 1, b: 'foo' } 110 | assert.deepStrictEqual(T.encode(x), { a: '1', b: 'foo' }) 111 | }) 112 | }) 113 | }) 114 | -------------------------------------------------------------------------------- /test/2.1.x/readonly.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | 3 | import * as t from '../../src/index' 4 | import { assertFailure, assertSuccess, NumberFromString } from './helpers' 5 | 6 | describe.concurrent('readonly', () => { 7 | describe.concurrent('name', () => { 8 | it('should assign a default name', () => { 9 | const T = t.readonly(t.type({ a: t.number })) 10 | assert.strictEqual(T.name, 'Readonly<{ a: number }>') 11 | }) 12 | 13 | it('should accept a name', () => { 14 | const T = t.readonly(t.type({ a: t.number }), 'T2') 15 | assert.strictEqual(T.name, 'T2') 16 | }) 17 | }) 18 | 19 | describe.concurrent('is', () => { 20 | it('should check a isomorphic value', () => { 21 | const T = t.readonly(t.type({ a: t.number })) 22 | assert.strictEqual(T.is({ a: 1 }), true) 23 | assert.strictEqual(T.is({ a: 'foo' }), false) 24 | assert.strictEqual(T.is(undefined), false) 25 | }) 26 | 27 | it('should check a prismatic value', () => { 28 | const T = t.readonly(t.type({ a: NumberFromString })) 29 | assert.strictEqual(T.is({ a: 1 }), true) 30 | assert.strictEqual(T.is({ a: '1' }), false) 31 | assert.strictEqual(T.is(undefined), false) 32 | }) 33 | }) 34 | 35 | describe.concurrent('decode', () => { 36 | it('should succeed validating a valid value', () => { 37 | const T = t.readonly(t.type({ a: t.number })) 38 | assertSuccess(T.decode({ a: 1 })) 39 | }) 40 | 41 | it('should fail validating an invalid value', () => { 42 | const T = t.readonly(t.type({ a: t.number })) 43 | assertFailure(T, {}, ['Invalid value undefined supplied to : Readonly<{ a: number }>/a: number']) 44 | }) 45 | }) 46 | 47 | describe.concurrent('encode', () => { 48 | it('should encode a prismatic value', () => { 49 | const T = t.readonly(t.type({ a: NumberFromString })) 50 | assert.deepStrictEqual(T.encode({ a: 1 }), { a: '1' }) 51 | }) 52 | 53 | it('should return the same reference when serializing', () => { 54 | const T = t.readonly(t.type({ a: t.number })) 55 | assert.strictEqual(T.encode, t.identity) 56 | }) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /test/2.1.x/readonlyArray.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | 3 | import * as t from '../../src/index' 4 | import { assertFailure, assertSuccess, NumberFromString } from './helpers' 5 | 6 | describe.concurrent('readonlyArray', () => { 7 | describe.concurrent('name', () => { 8 | it('should assign a default name', () => { 9 | const T = t.readonlyArray(t.number) 10 | assert.strictEqual(T.name, 'ReadonlyArray') 11 | }) 12 | 13 | it('should accept a name', () => { 14 | const T = t.readonlyArray(t.number, 'T2') 15 | assert.strictEqual(T.name, 'T2') 16 | }) 17 | }) 18 | 19 | describe.concurrent('is', () => { 20 | it('should check a isomorphic value', () => { 21 | const T = t.readonlyArray(t.number) 22 | assert.strictEqual(T.is([]), true) 23 | assert.strictEqual(T.is([0]), true) 24 | assert.strictEqual(T.is([0, 'foo']), false) 25 | assert.strictEqual(T.is(undefined), false) 26 | }) 27 | 28 | it('should check a prismatic value', () => { 29 | const T = t.readonlyArray(NumberFromString) 30 | assert.strictEqual(T.is([]), true) 31 | assert.strictEqual(T.is([1]), true) 32 | assert.strictEqual(T.is([1, 'foo']), false) 33 | assert.strictEqual(T.is(undefined), false) 34 | }) 35 | }) 36 | 37 | describe.concurrent('decode', () => { 38 | it('should succeed validating a valid value', () => { 39 | const T = t.readonlyArray(t.number) 40 | assertSuccess(T.decode([1])) 41 | }) 42 | 43 | it('should fail validating an invalid value', () => { 44 | const T = t.readonlyArray(t.number) 45 | assertFailure(T, ['s'], ['Invalid value "s" supplied to : ReadonlyArray/0: number']) 46 | }) 47 | }) 48 | 49 | describe.concurrent('encode', () => { 50 | it('should encode a prismatic value', () => { 51 | const T = t.readonlyArray(NumberFromString) 52 | assert.deepStrictEqual(T.encode([0, 1]), ['0', '1']) 53 | }) 54 | 55 | it('should return the same reference when serializing', () => { 56 | const T = t.readonlyArray(t.number) 57 | assert.strictEqual(T.encode, t.identity) 58 | }) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /test/2.1.x/recursion.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | 3 | import * as t from '../../src/index' 4 | import { assertFailure, assertStrictEqual, assertSuccess, NumberFromString } from './helpers' 5 | 6 | type T = { 7 | a: number 8 | b: T | undefined | null 9 | } 10 | const T: t.Type = t.recursion('T', (self) => 11 | t.type({ 12 | a: t.number, 13 | b: t.union([self, t.undefined, t.null]) 14 | }) 15 | ) 16 | 17 | describe.concurrent('recursion', () => { 18 | describe.concurrent('is', () => { 19 | it('should check a isomorphic value', () => { 20 | type A = { 21 | a: number 22 | b: A | null 23 | } 24 | const T: t.Type = t.recursion('T', (self) => 25 | t.type({ 26 | a: t.number, 27 | b: t.union([self, t.null]) 28 | }) 29 | ) 30 | assert.strictEqual(T.is({ a: 0, b: null }), true) 31 | assert.strictEqual(T.is({ a: 0 }), false) 32 | }) 33 | 34 | it('should check a prismatic value', () => { 35 | type A = { 36 | a: number 37 | b: A | null 38 | } 39 | type O = { 40 | a: string 41 | b: O | null 42 | } 43 | const T: t.Type = t.recursion('T', (self) => 44 | t.type({ 45 | a: NumberFromString, 46 | b: t.union([self, t.null]) 47 | }) 48 | ) 49 | assert.strictEqual(T.is({ a: 0, b: null }), true) 50 | assert.strictEqual(T.is({ a: 0 }), false) 51 | }) 52 | }) 53 | 54 | describe.concurrent('decode', () => { 55 | it('should succeed validating a valid value', () => { 56 | assertSuccess(T.decode({ a: 1, b: null })) 57 | assertSuccess(T.decode({ a: 1, b: { a: 2, b: null } })) 58 | }) 59 | 60 | it('should return the same reference if validation succeeded', () => { 61 | type T = { 62 | a: number 63 | b: T | null | undefined 64 | } 65 | const T: t.Type = t.recursion('T', (self) => 66 | t.type({ 67 | a: t.number, 68 | b: t.union([self, t.undefined, t.null]) 69 | }) 70 | ) 71 | const value = { a: 1, b: { a: 2, b: null } } 72 | assertStrictEqual(T.decode(value), value) 73 | }) 74 | 75 | it('should fail validating an invalid value', () => { 76 | assertFailure(T, 1, ['Invalid value 1 supplied to : T']) 77 | assertFailure(T, {}, ['Invalid value undefined supplied to : T/a: number']) 78 | assertFailure(T, { a: 1, b: {} }, [ 79 | 'Invalid value undefined supplied to : T/b: (T | undefined | null)/0: T/a: number', 80 | 'Invalid value {} supplied to : T/b: (T | undefined | null)/1: undefined', 81 | 'Invalid value {} supplied to : T/b: (T | undefined | null)/2: null' 82 | ]) 83 | }) 84 | }) 85 | 86 | describe.concurrent('encode', () => { 87 | it('should encode a prismatic value', () => { 88 | type A = { 89 | a: number 90 | b: A | null 91 | } 92 | type O = { 93 | a: string 94 | b: O | null 95 | } 96 | const T: t.Type = t.recursion('T', (self) => 97 | t.type({ 98 | a: NumberFromString, 99 | b: t.union([self, t.null]) 100 | }) 101 | ) 102 | assert.deepStrictEqual(T.encode({ a: 0, b: null }), { a: '0', b: null }) 103 | assert.deepStrictEqual(T.encode({ a: 0, b: { a: 1, b: null } }), { a: '0', b: { a: '1', b: null } }) 104 | }) 105 | }) 106 | 107 | it('should have a `type` field', () => { 108 | type T = { 109 | a: number 110 | b: T | null 111 | } 112 | const T = t.recursion('T', (self) => 113 | t.type({ 114 | a: t.number, 115 | b: t.union([self, t.null]) 116 | }) 117 | ) 118 | assert.strictEqual(T.type instanceof t.Type, true) 119 | assert.strictEqual(T.type.name, 'T') 120 | assert.strictEqual((T.type as any).props.a._tag, 'NumberType') 121 | }) 122 | 123 | it('should support mutually recursive types', () => { 124 | type A = { 125 | b: A | B | null 126 | } 127 | type B = { 128 | a: A | null 129 | } 130 | const A: t.Type = t.recursion('A', () => 131 | t.type({ 132 | b: t.union([A, B, t.null]) 133 | }) 134 | ) 135 | const B: t.Type = t.recursion('B', () => 136 | t.type({ 137 | a: t.union([A, t.null]) 138 | }) 139 | ) 140 | assert.strictEqual(A.is({ b: { b: null } }), true) 141 | assert.strictEqual(A.is({ b: { a: { b: { a: null } } } }), true) 142 | 143 | // #354 144 | interface C1A { 145 | a: C1A | string 146 | } 147 | const C1: t.Type = t.recursion('C1', () => 148 | t.type({ 149 | a: t.union([C2, t.string]) 150 | }) 151 | ) 152 | const C2: t.Type = t.recursion('C2', () => C1) 153 | const C3 = t.union([C1, t.string]) 154 | 155 | assert.strictEqual(C3.is({ a: 'a' }), true) 156 | assert.strictEqual(C3.is('a'), true) 157 | assert.strictEqual(C3.is({ a: { a: 'a' } }), true) 158 | }) 159 | }) 160 | -------------------------------------------------------------------------------- /test/2.1.x/refinement.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | 3 | import * as t from '../../src/index' 4 | import { assertFailure, assertStrictEqual, assertSuccess, IntegerFromString, NumberFromString } from './helpers' 5 | 6 | describe.concurrent('refinement', () => { 7 | describe.concurrent('name', () => { 8 | it('should assign a default name', () => { 9 | const T = t.refinement(t.number, (n) => n >= 0) 10 | assert.strictEqual(T.name, '(number | )') 11 | }) 12 | 13 | it('should accept a name', () => { 14 | const T = t.refinement(t.number, (n) => n >= 0, 'T') 15 | assert.strictEqual(T.name, 'T') 16 | }) 17 | }) 18 | 19 | describe.concurrent('is', () => { 20 | it('should check a isomorphic value', () => { 21 | const T = t.Integer 22 | assert.strictEqual(T.is(1.2), false) 23 | assert.strictEqual(T.is('a'), false) 24 | assert.strictEqual(T.is(1), true) 25 | }) 26 | 27 | it('should check a prismatic value', () => { 28 | const T = t.refinement(NumberFromString, (n) => n % 1 === 0) 29 | assert.strictEqual(T.is(1.2), false) 30 | assert.strictEqual(T.is('a'), false) 31 | assert.strictEqual(T.is(1), true) 32 | }) 33 | }) 34 | 35 | describe.concurrent('decode', () => { 36 | it('should succeed validating a valid value', () => { 37 | const T = t.refinement(t.number, (n) => n >= 0) 38 | assertSuccess(T.decode(0)) 39 | assertSuccess(T.decode(1)) 40 | }) 41 | 42 | it('should return the same reference if validation succeeded', () => { 43 | const T = t.refinement( 44 | // tslint:disable-next-line: deprecation 45 | t.Dictionary, 46 | () => true 47 | ) 48 | const value = {} 49 | assertStrictEqual(T.decode(value), value) 50 | }) 51 | 52 | it('should fail validating an invalid value', () => { 53 | const T = t.Integer 54 | assertFailure(T, 'a', ['Invalid value "a" supplied to : Integer']) 55 | assertFailure(T, 1.2, ['Invalid value 1.2 supplied to : Integer']) 56 | }) 57 | 58 | it('should fail with the last deserialized value', () => { 59 | const T = IntegerFromString 60 | assertFailure(T, 'a', ['cannot parse to a number']) 61 | assertFailure(T, '1.2', ['Invalid value 1.2 supplied to : IntegerFromString']) 62 | }) 63 | }) 64 | 65 | describe.concurrent('encode', () => { 66 | it('should encode a prismatic value', () => { 67 | const T = t.refinement(t.array(NumberFromString), () => true) 68 | assert.deepStrictEqual(T.encode([1]), ['1']) 69 | }) 70 | 71 | it('should return the same reference while encoding', () => { 72 | const T = t.refinement(t.array(t.number), () => true) 73 | assert.strictEqual(T.encode, t.identity) 74 | }) 75 | }) 76 | }) 77 | -------------------------------------------------------------------------------- /test/2.1.x/strict.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | 3 | import * as t from '../../src/index' 4 | import { assertFailure, assertStrictEqual, assertSuccess, NumberFromString } from './helpers' 5 | 6 | describe.concurrent('strict', () => { 7 | describe.concurrent('name', () => { 8 | it('should assign a default name', () => { 9 | const T = t.strict({ foo: t.string }) 10 | assert.strictEqual(T.name, '{| foo: string |}') 11 | }) 12 | 13 | it('should accept a name', () => { 14 | const T = t.strict({ foo: t.string }, 'Foo') 15 | assert.strictEqual(T.name, 'Foo') 16 | }) 17 | }) 18 | 19 | describe.concurrent('is', () => { 20 | it('should check a isomorphic value', () => { 21 | const T = t.strict({ a: t.number }) 22 | assert.strictEqual(T.is({ a: 0 }), true) 23 | assert.strictEqual(T.is({ a: 0, b: 1 }), true) 24 | assert.strictEqual(T.is(undefined), false) 25 | }) 26 | 27 | it('should check a prismatic value', () => { 28 | const T = t.strict({ a: NumberFromString }) 29 | assert.strictEqual(T.is({ a: 1 }), true) 30 | assert.strictEqual(T.is({ a: 1, b: 1 }), true) 31 | assert.strictEqual(T.is(undefined), false) 32 | }) 33 | 34 | it('#423', () => { 35 | class A { 36 | get a() { 37 | return 'a' 38 | } 39 | get b() { 40 | return 'b' 41 | } 42 | } 43 | const T = t.strict({ a: t.string, b: t.string }) 44 | assert.strictEqual(T.is(new A()), true) 45 | }) 46 | }) 47 | 48 | describe.concurrent('decode', () => { 49 | it('should succeed validating a valid value', () => { 50 | const T = t.strict({ foo: t.string }) 51 | assertSuccess(T.decode({ foo: 'foo' })) 52 | }) 53 | 54 | it('should succeed validating an undefined field', () => { 55 | const T = t.strict({ foo: t.string, bar: t.union([t.string, t.undefined]) }) 56 | assertSuccess(T.decode({ foo: 'foo' })) 57 | }) 58 | 59 | it('should return the same reference if validation succeeded', () => { 60 | const T = t.strict({ foo: t.string }) 61 | const value = { foo: 'foo' } 62 | assertStrictEqual(T.decode(value), value) 63 | }) 64 | 65 | it('should fail validating an invalid value', () => { 66 | const T = t.strict({ foo: t.string }) 67 | assertFailure(T, { foo: 1 }, ['Invalid value 1 supplied to : {| foo: string |}/foo: string']) 68 | }) 69 | 70 | it('should strip additional properties', () => { 71 | const T = t.strict({ foo: t.string }) 72 | assertSuccess(T.decode({ foo: 'foo', bar: 1, baz: true }), { foo: 'foo' }) 73 | }) 74 | 75 | it('#423', () => { 76 | class A { 77 | get a() { 78 | return 'a' 79 | } 80 | get b() { 81 | return 'b' 82 | } 83 | } 84 | const T = t.strict({ a: t.string, b: t.string }) 85 | assertSuccess(T.decode(new A())) 86 | }) 87 | }) 88 | 89 | describe.concurrent('encode', () => { 90 | it('should encode a prismatic value', () => { 91 | const T = t.strict({ a: NumberFromString }) 92 | assert.deepStrictEqual(T.encode({ a: 1 }), { a: '1' }) 93 | }) 94 | 95 | it('should return the same reference while encoding', () => { 96 | const T = t.strict({ a: t.string }) 97 | const x = { a: 'a' } 98 | assert.strictEqual(T.encode(x), x) 99 | }) 100 | }) 101 | 102 | it('should export a StrictType class', () => { 103 | // tslint:disable-next-line: deprecation 104 | const T = new t.StrictType<{}, {}, {}, unknown>( 105 | 'name', 106 | (_): _ is {} => true, 107 | (u, c) => t.failure(u, c), 108 | t.identity, 109 | {} 110 | ) 111 | assert.strictEqual(T.name, 'name') 112 | }) 113 | }) 114 | -------------------------------------------------------------------------------- /test/2.1.x/strictInterfaceWithOptionals.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | 3 | import * as t from '../../src/index' 4 | import { assertFailure, assertSuccess, NumberFromString } from './helpers' 5 | 6 | export function strictInterfaceWithOptionals( 7 | required: R, 8 | optional: O, 9 | name?: string 10 | ): t.Type & t.TypeOfPartialProps, t.OutputOfProps & t.OutputOfPartialProps> { 11 | return t.exact(t.intersection([t.type(required), t.partial(optional)]), name) 12 | } 13 | 14 | describe.concurrent('strictInterfaceWithOptionals', () => { 15 | it('should succeed validating a valid value', () => { 16 | const T = strictInterfaceWithOptionals({ foo: t.string }, { bar: t.string }, 'T') 17 | assertSuccess(T.decode({ foo: 'foo' })) 18 | assertSuccess(T.decode({ foo: 'foo', bar: 'a' })) 19 | }) 20 | 21 | it('should fail validating an invalid value', () => { 22 | const T = strictInterfaceWithOptionals({ foo: t.string }, { bar: t.string }, 'T') 23 | assertFailure(T, { foo: 'foo', bar: 1 }, [ 24 | 'Invalid value 1 supplied to : T/1: Partial<{ bar: string }>/bar: string' 25 | ]) 26 | }) 27 | 28 | it('should strip additional properties', () => { 29 | const T = strictInterfaceWithOptionals({ foo: t.string }, { bar: t.string }, 'T') 30 | assertSuccess(T.decode({ foo: 'foo', a: 1 }), { foo: 'foo' }) 31 | }) 32 | 33 | it('should return the same reference when serializing', () => { 34 | const T = strictInterfaceWithOptionals({ foo: t.string }, { bar: t.string }, 'T') 35 | const x = { foo: 'foo' } 36 | assert.strictEqual(T.encode(x), x) 37 | }) 38 | 39 | it('should return the a new reference if validation succeeded and something changed', () => { 40 | const T = strictInterfaceWithOptionals({ foo: NumberFromString }, { bar: t.string }, 'T') 41 | assertSuccess(T.decode({ foo: '1' }), { foo: 1 }) 42 | }) 43 | 44 | it('should serialize a deserialized', () => { 45 | const T = strictInterfaceWithOptionals({ foo: NumberFromString }, { bar: t.string }, 'T') 46 | assert.deepStrictEqual(T.encode({ foo: 1 }), { foo: '1' }) 47 | }) 48 | 49 | it('should type guard', () => { 50 | const T = strictInterfaceWithOptionals({ foo: t.string }, { bar: t.string }, 'T') 51 | assert.strictEqual(T.is({ foo: 'foo' }), true) 52 | assert.strictEqual(T.is({ foo: 'foo', bar: 'a' }), true) 53 | assert.strictEqual(T.is({ foo: 'foo', a: 1 }), true) 54 | assert.strictEqual(T.is({ foo: 'foo', bar: 1 }), false) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /test/2.1.x/tuple.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | 3 | import * as t from '../../src/index' 4 | import { assertFailure, assertStrictEqual, assertSuccess, NumberFromString } from './helpers' 5 | 6 | describe.concurrent('tuple', () => { 7 | describe.concurrent('name', () => { 8 | it('should assign a default name', () => { 9 | const T = t.tuple([t.number, t.string]) 10 | assert.strictEqual(T.name, '[number, string]') 11 | }) 12 | 13 | it('should accept a name', () => { 14 | const T = t.tuple([t.number, t.string], 'T') 15 | assert.strictEqual(T.name, 'T') 16 | }) 17 | }) 18 | 19 | describe.concurrent('is', () => { 20 | it('should check a isomorphic value', () => { 21 | const T = t.tuple([t.number, t.string]) 22 | assert.strictEqual(T.is([0, 'foo']), true) 23 | assert.strictEqual(T.is([0, 2]), false) 24 | assert.strictEqual(T.is(undefined), false) 25 | assert.strictEqual(T.is([0]), false) 26 | }) 27 | 28 | it('should check a prismatic value', () => { 29 | const T = t.tuple([NumberFromString, t.string]) 30 | assert.strictEqual(T.is([0, 'foo']), true) 31 | assert.strictEqual(T.is([0, 2]), false) 32 | assert.strictEqual(T.is(undefined), false) 33 | assert.strictEqual(T.is([0]), false) 34 | }) 35 | 36 | it('should check for additional components', () => { 37 | const T = t.tuple([t.number, t.string]) 38 | assert.strictEqual(T.is([0, 'foo', true]), false) 39 | }) 40 | }) 41 | 42 | describe.concurrent('decode', () => { 43 | it('should decode a isomorphic value', () => { 44 | const T0 = t.tuple([] as any) 45 | assertSuccess(T0.decode([])) 46 | const T1 = t.tuple([t.number]) 47 | assertSuccess(T1.decode([1])) 48 | const T2 = t.tuple([t.number, t.string]) 49 | assertSuccess(T2.decode([1, 'a'])) 50 | }) 51 | 52 | it('should decode a prismatic value', () => { 53 | const T = t.tuple([NumberFromString, t.string]) 54 | assertSuccess(T.decode(['1', 'a']), [1, 'a']) 55 | }) 56 | 57 | it('should fail validating an invalid value', () => { 58 | const T = t.tuple([t.number, t.string]) 59 | assertFailure(T, 1, ['Invalid value 1 supplied to : [number, string]']) 60 | assertFailure( 61 | T, 62 | [], 63 | [ 64 | 'Invalid value undefined supplied to : [number, string]/0: number', 65 | 'Invalid value undefined supplied to : [number, string]/1: string' 66 | ] 67 | ) 68 | assertFailure(T, [1], ['Invalid value undefined supplied to : [number, string]/1: string']) 69 | assertFailure(T, [1, 1], ['Invalid value 1 supplied to : [number, string]/1: string']) 70 | }) 71 | 72 | it('should strip additional components', () => { 73 | const T = t.tuple([t.number, t.string]) 74 | assertSuccess(T.decode([1, 'foo', true]), [1, 'foo']) 75 | assertSuccess(T.decode([1, 'foo', true, 'a']), [1, 'foo']) 76 | }) 77 | 78 | it('should return the same reference if validation succeeded and nothing changed', () => { 79 | const T = t.tuple([t.number, t.string]) 80 | const value = [1, 'a'] 81 | assertStrictEqual(T.decode(value), value) 82 | }) 83 | }) 84 | 85 | describe.concurrent('encode', () => { 86 | it('should encode a isomorphic value', () => { 87 | const T = t.tuple([t.number, t.string]) 88 | assert.deepStrictEqual(T.encode([1, 'a']), [1, 'a']) 89 | }) 90 | 91 | it('should encode a prismatic value', () => { 92 | const T = t.tuple([NumberFromString, t.string]) 93 | assert.deepStrictEqual(T.encode([1, 'a']), ['1', 'a']) 94 | }) 95 | 96 | it('should return the same reference while encoding', () => { 97 | const T = t.tuple([t.number, t.string]) 98 | const x: [number, string] = [1, 'a'] 99 | assert.strictEqual(T.encode(x), x) 100 | }) 101 | }) 102 | }) 103 | -------------------------------------------------------------------------------- /test/2.1.x/type.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import { fold } from 'fp-ts/lib/Either' 3 | import { pipe } from 'fp-ts/lib/pipeable' 4 | 5 | import * as t from '../../src/index' 6 | import { assertFailure, assertStrictEqual, assertSuccess, NumberFromString } from './helpers' 7 | 8 | describe.concurrent('type', () => { 9 | describe.concurrent('name', () => { 10 | it('should assign a default name', () => { 11 | const T = t.type({ a: t.string }) 12 | assert.strictEqual(T.name, '{ a: string }') 13 | }) 14 | 15 | it('should accept a name', () => { 16 | const T = t.type({ a: t.string }, 'T') 17 | assert.strictEqual(T.name, 'T') 18 | }) 19 | }) 20 | 21 | describe.concurrent('is', () => { 22 | it('should return `true` on valid inputs', () => { 23 | const T = t.type({ a: t.string }) 24 | assert.strictEqual(T.is({ a: 'a' }), true) 25 | }) 26 | 27 | it('should return `false` on invalid inputs', () => { 28 | const T = t.type({ a: t.string }) 29 | assert.strictEqual(T.is({}), false) 30 | assert.strictEqual(T.is({ a: 1 }), false) 31 | // #407 32 | assert.strictEqual(T.is([]), false) 33 | }) 34 | 35 | // #434 36 | it('should return `false` on missing fields', () => { 37 | const T = t.type({ a: t.unknown }) 38 | assert.strictEqual(T.is({}), false) 39 | }) 40 | 41 | it('should allow additional properties', () => { 42 | const T = t.type({ a: t.string }) 43 | assert.strictEqual(T.is({ a: 'a', b: 1 }), true) 44 | }) 45 | 46 | it('#423', () => { 47 | class A { 48 | get a() { 49 | return 'a' 50 | } 51 | get b() { 52 | return 'b' 53 | } 54 | } 55 | const T = t.type({ a: t.string, b: t.string }) 56 | assert.strictEqual(T.is(new A()), true) 57 | }) 58 | }) 59 | 60 | describe.concurrent('decode', () => { 61 | it('should decode a isomorphic value', () => { 62 | const T = t.type({ a: t.string }) 63 | assertSuccess(T.decode({ a: 'a' })) 64 | }) 65 | 66 | it('should decode a prismatic value', () => { 67 | const T = t.type({ a: NumberFromString }) 68 | assertSuccess(T.decode({ a: '1' }), { a: 1 }) 69 | }) 70 | 71 | it('should decode undefined properties as always present keys', () => { 72 | const T1 = t.type({ a: t.undefined }) 73 | assertSuccess(T1.decode({ a: undefined }), { a: undefined }) 74 | assertSuccess(T1.decode({}), { a: undefined }) 75 | 76 | const T2 = t.type({ a: t.union([t.number, t.undefined]) }) 77 | assertSuccess(T2.decode({ a: undefined }), { a: undefined }) 78 | assertSuccess(T2.decode({ a: 1 }), { a: 1 }) 79 | assertSuccess(T2.decode({}), { a: undefined }) 80 | 81 | const T3 = t.type({ a: t.unknown }) 82 | assertSuccess(T3.decode({}), { a: undefined }) 83 | }) 84 | 85 | it('should fail decoding an invalid value', () => { 86 | const T = t.type({ a: t.string }) 87 | assertFailure(T, 1, ['Invalid value 1 supplied to : { a: string }']) 88 | assertFailure(T, {}, ['Invalid value undefined supplied to : { a: string }/a: string']) 89 | assertFailure(T, { a: 1 }, ['Invalid value 1 supplied to : { a: string }/a: string']) 90 | // #407 91 | assertFailure(T, [], ['Invalid value [] supplied to : { a: string }']) 92 | }) 93 | 94 | it('should support the alias `interface`', () => { 95 | const T = t.interface({ a: t.string }) 96 | assertSuccess(T.decode({ a: 'a' })) 97 | }) 98 | 99 | it('#423', () => { 100 | class A { 101 | get a() { 102 | return 'a' 103 | } 104 | get b() { 105 | return 'b' 106 | } 107 | } 108 | const T = t.type({ a: t.string, b: t.string }) 109 | assertSuccess(T.decode(new A())) 110 | }) 111 | }) 112 | 113 | describe.concurrent('encode', () => { 114 | it('should encode a isomorphic value', () => { 115 | const T = t.type({ a: t.string }) 116 | assert.deepStrictEqual(T.encode({ a: 'a' }), { a: 'a' }) 117 | }) 118 | 119 | it('should encode a prismatic value', () => { 120 | const T = t.type({ a: NumberFromString }) 121 | assert.deepStrictEqual(T.encode({ a: 1 }), { a: '1' }) 122 | }) 123 | }) 124 | 125 | it('should keep unknown properties', () => { 126 | const T = t.type({ a: t.string }) 127 | const validation = T.decode({ a: 's', b: 1 }) 128 | pipe( 129 | validation, 130 | fold( 131 | () => { 132 | assert.ok(false) 133 | }, 134 | (a) => { 135 | assert.deepStrictEqual(a, { a: 's', b: 1 }) 136 | } 137 | ) 138 | ) 139 | }) 140 | 141 | it('should return the same reference if validation succeeded and nothing changed', () => { 142 | const T = t.type({ a: t.string }) 143 | const value = { a: 's' } 144 | assertStrictEqual(T.decode(value), value) 145 | }) 146 | 147 | it('should return the same reference while encoding', () => { 148 | const T = t.type({ a: t.number }) 149 | assert.strictEqual(T.encode, t.identity) 150 | }) 151 | }) 152 | -------------------------------------------------------------------------------- /test/Arbitrary.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * An instance of `Schemable` for `fast-check` arbitraries that emit valid values 3 | */ 4 | import * as fc from 'fast-check' 5 | import { identity, Refinement } from 'fp-ts/lib/function' 6 | 7 | import * as S from '../src/Schemable' 8 | 9 | // ------------------------------------------------------------------------------------- 10 | // model 11 | // ------------------------------------------------------------------------------------- 12 | 13 | export interface Arbitrary extends fc.Arbitrary {} 14 | 15 | // ------------------------------------------------------------------------------------- 16 | // constructors 17 | // ------------------------------------------------------------------------------------- 18 | 19 | export function literal], L extends S.Literal = S.Literal>( 20 | ...values: A 21 | ): Arbitrary { 22 | return fc.oneof(...values.map((v) => fc.constant(v))) 23 | } 24 | 25 | // ------------------------------------------------------------------------------------- 26 | // primitives 27 | // ------------------------------------------------------------------------------------- 28 | 29 | export const string: Arbitrary = fc.oneof( 30 | fc.string(), 31 | fc.asciiString(), 32 | fc.fullUnicodeString(), 33 | fc.hexaString(), 34 | fc.lorem() 35 | ) 36 | 37 | export const number: Arbitrary = fc.oneof(fc.float(), fc.double(), fc.integer()) 38 | 39 | export const boolean: Arbitrary = fc.boolean() 40 | 41 | export const UnknownArray: Arbitrary> = fc.array(fc.anything()) 42 | 43 | export const UnknownRecord: Arbitrary> = fc.dictionary(string, fc.anything()) 44 | 45 | // ------------------------------------------------------------------------------------- 46 | // combinators 47 | // ------------------------------------------------------------------------------------- 48 | 49 | export const refine = 50 | (refinement: Refinement) => 51 | (from: Arbitrary): Arbitrary => 52 | from.filter(refinement) 53 | 54 | export function nullable(or: Arbitrary): Arbitrary { 55 | return fc.oneof(fc.constant(null), or) 56 | } 57 | 58 | export function struct(properties: { [K in keyof A]: Arbitrary }): Arbitrary { 59 | return fc.record(properties) 60 | } 61 | 62 | export function partial(properties: { [K in keyof A]: Arbitrary }): Arbitrary> { 63 | const keys = fc.oneof(...Object.keys(properties).map((p) => fc.constant(p))) 64 | return keys.chain((key) => { 65 | const p: any = { ...properties } 66 | delete p[key] 67 | return fc.record(p) 68 | }) 69 | } 70 | 71 | export function record(codomain: Arbitrary): Arbitrary> { 72 | return fc.dictionary(string, codomain) 73 | } 74 | 75 | export function array(item: Arbitrary): Arbitrary> { 76 | return fc.array(item) 77 | } 78 | 79 | export function tuple>( 80 | ...components: { [K in keyof A]: Arbitrary } 81 | ): Arbitrary { 82 | if (components.length === 0) { 83 | return fc.constant([]) as any 84 | } 85 | return (fc.tuple as any)(...components) 86 | } 87 | 88 | export const intersect = 89 | (right: Arbitrary) => 90 | (left: Arbitrary): Arbitrary => 91 | fc.tuple(left, right).map(([a, b]) => S.intersect_(a, b)) 92 | 93 | export function sum( 94 | _tag: T 95 | ): (members: { [K in keyof A]: Arbitrary> }) => Arbitrary { 96 | return (members: Record>) => fc.oneof(...Object.keys(members).map((k) => members[k])) 97 | } 98 | 99 | export function lazy(f: () => Arbitrary): Arbitrary { 100 | const get = S.memoize>(f) 101 | return fc.constant(null).chain(() => get()) 102 | } 103 | 104 | export const readonly: (arb: Arbitrary) => Arbitrary> = identity 105 | 106 | export function union, ...Array>]>( 107 | ...members: { [K in keyof A]: Arbitrary } 108 | ): Arbitrary { 109 | return fc.oneof(...members) as any 110 | } 111 | 112 | // ------------------------------------------------------------------------------------- 113 | // instances 114 | // ------------------------------------------------------------------------------------- 115 | 116 | export const URI = 'Arbitrary' 117 | 118 | export type URI = typeof URI 119 | 120 | declare module 'fp-ts/lib/HKT' { 121 | interface URItoKind { 122 | readonly Arbitrary: Arbitrary 123 | } 124 | } 125 | 126 | export const Schemable: S.Schemable1 & S.WithUnknownContainers1 & S.WithUnion1 & S.WithRefine1 = { 127 | URI, 128 | literal, 129 | string, 130 | number, 131 | boolean, 132 | nullable, 133 | type: struct, 134 | struct, 135 | partial, 136 | record, 137 | array, 138 | tuple: tuple as S.Schemable1['tuple'], 139 | intersect, 140 | sum, 141 | lazy: (_, f) => lazy(f), 142 | readonly, 143 | UnknownArray, 144 | UnknownRecord, 145 | union: union as S.WithUnion1['union'], 146 | refine: refine as S.WithRefine1['refine'] 147 | } 148 | -------------------------------------------------------------------------------- /test/Encoder.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import { pipe } from 'fp-ts/lib/pipeable' 3 | 4 | import * as E from '../src/Encoder' 5 | import * as H from './helpers' 6 | 7 | describe.concurrent('Encoder', () => { 8 | it('contramap', () => { 9 | const encoder = E.contramap((s: string) => s.length)(H.encoderNumberToString) 10 | assert.deepStrictEqual(encoder.encode('aaa'), '3') 11 | }) 12 | 13 | it('compose', () => { 14 | const encoder = pipe(H.encoderBooleanToNumber, E.compose(H.encoderNumberToString)) 15 | assert.deepStrictEqual(encoder.encode(true), '1') 16 | }) 17 | 18 | it('nullable', () => { 19 | const encoder = E.nullable(H.encoderNumberToString) 20 | assert.deepStrictEqual(encoder.encode(1), '1') 21 | assert.deepStrictEqual(encoder.encode(null), null) 22 | }) 23 | 24 | it('struct', () => { 25 | const encoder = E.struct({ a: H.encoderNumberToString, b: H.encoderBooleanToNumber }) 26 | assert.deepStrictEqual(encoder.encode({ a: 1, b: true }), { a: '1', b: 1 }) 27 | }) 28 | 29 | it('partial', () => { 30 | const encoder = E.partial({ a: H.encoderNumberToString, b: H.encoderBooleanToNumber }) 31 | assert.deepStrictEqual(encoder.encode({ a: 1, b: true }), { a: '1', b: 1 }) 32 | assert.deepStrictEqual(encoder.encode({ a: 1 }), { a: '1' }) 33 | assert.deepStrictEqual(encoder.encode({ a: 1, b: undefined }), { a: '1', b: undefined }) 34 | assert.deepStrictEqual(encoder.encode({ b: true }), { b: 1 }) 35 | assert.deepStrictEqual(encoder.encode({}), {}) 36 | }) 37 | 38 | it('record', () => { 39 | const encoder = E.record(H.encoderNumberToString) 40 | assert.deepStrictEqual(encoder.encode({ a: 1, b: 2 }), { a: '1', b: '2' }) 41 | }) 42 | 43 | it('array', () => { 44 | const encoder = E.array(H.encoderNumberToString) 45 | assert.deepStrictEqual(encoder.encode([1, 2]), ['1', '2']) 46 | }) 47 | 48 | it('tuple', () => { 49 | const encoder = E.tuple(H.encoderNumberToString, H.encoderBooleanToNumber) 50 | assert.deepStrictEqual(encoder.encode([3, true]), ['3', 1]) 51 | }) 52 | 53 | it('intersect', () => { 54 | const encoder = pipe( 55 | E.struct({ a: H.encoderNumberToString }), 56 | E.intersect(E.struct({ b: H.encoderBooleanToNumber })) 57 | ) 58 | assert.deepStrictEqual(encoder.encode({ a: 1, b: true }), { a: '1', b: 1 }) 59 | }) 60 | 61 | it('sum', () => { 62 | const S1 = E.struct({ _tag: E.id<'A'>(), a: H.encoderNumberToString }) 63 | const S2 = E.struct({ _tag: E.id<'B'>(), b: H.encoderBooleanToNumber }) 64 | const sum = E.sum('_tag') 65 | const encoder = sum({ A: S1, B: S2 }) 66 | assert.deepStrictEqual(encoder.encode({ _tag: 'A', a: 1 }), { _tag: 'A', a: '1' }) 67 | assert.deepStrictEqual(encoder.encode({ _tag: 'B', b: true }), { _tag: 'B', b: 1 }) 68 | }) 69 | 70 | it('lazy', () => { 71 | interface A { 72 | a: number 73 | bs: Array 74 | } 75 | interface AOut { 76 | a: string 77 | bs: Array 78 | } 79 | interface B { 80 | b: boolean 81 | as: Array 82 | } 83 | interface BOut { 84 | b: number 85 | as: Array 86 | } 87 | const A: E.Encoder = E.lazy(() => 88 | E.struct({ 89 | a: H.encoderNumberToString, 90 | bs: E.array(B) 91 | }) 92 | ) 93 | 94 | const B: E.Encoder = E.lazy(() => 95 | E.struct({ 96 | b: H.encoderBooleanToNumber, 97 | as: E.array(A) 98 | }) 99 | ) 100 | assert.deepStrictEqual(A.encode({ a: 1, bs: [] }), { a: '1', bs: [] }) 101 | assert.deepStrictEqual(A.encode({ a: 1, bs: [{ b: true, as: [] }] }), { a: '1', bs: [{ b: 1, as: [] }] }) 102 | assert.deepStrictEqual(A.encode({ a: 1, bs: [{ b: true, as: [{ a: 2, bs: [] }] }] }), { 103 | a: '1', 104 | bs: [{ b: 1, as: [{ a: '2', bs: [] }] }] 105 | }) 106 | }) 107 | }) 108 | -------------------------------------------------------------------------------- /test/Eq.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import { Eq } from 'fp-ts/lib/Eq' 3 | import { pipe } from 'fp-ts/lib/pipeable' 4 | 5 | import * as E from '../src/Eq' 6 | 7 | describe.concurrent('Eq', () => { 8 | it('literal', () => { 9 | const eq = E.Schemable.literal('a', null) 10 | assert.deepStrictEqual(eq.equals('a', 'a'), true) 11 | assert.deepStrictEqual(eq.equals(null, null), true) 12 | assert.deepStrictEqual(eq.equals('a', null), false) 13 | }) 14 | 15 | it('UnknownArray', () => { 16 | const eq = E.UnknownArray 17 | assert.deepStrictEqual(eq.equals(['a'], ['a']), true) 18 | assert.deepStrictEqual(eq.equals(['a'], ['b']), true) 19 | assert.deepStrictEqual(eq.equals(['a'], ['a', 'b']), false) 20 | }) 21 | 22 | it('UnknownRecord', () => { 23 | const eq = E.UnknownRecord 24 | assert.deepStrictEqual(eq.equals({}, {}), true) 25 | assert.deepStrictEqual(eq.equals({ a: 1 }, { a: 1 }), true) 26 | assert.deepStrictEqual(eq.equals({ a: 1 }, { a: 2 }), true) 27 | assert.deepStrictEqual(eq.equals({ a: 1 }, { a: 1, b: true }), false) 28 | assert.deepStrictEqual(eq.equals({ a: 1, b: true }, { a: 1 }), false) 29 | }) 30 | 31 | it('partial', () => { 32 | const eq = E.partial({ a: E.number }) 33 | assert.deepStrictEqual(eq.equals({ a: 1 }, { a: 1 }), true) 34 | assert.deepStrictEqual(eq.equals({ a: undefined }, { a: undefined }), true) 35 | assert.deepStrictEqual(eq.equals({}, { a: undefined }), true) 36 | assert.deepStrictEqual(eq.equals({}, {}), true) 37 | assert.deepStrictEqual(eq.equals({ a: 1 }, {}), false) 38 | }) 39 | 40 | it('tuple', () => { 41 | const eq = E.tuple(E.string, E.number) 42 | assert.deepStrictEqual(eq.equals(['a', 1], ['a', 1]), true) 43 | assert.deepStrictEqual(eq.equals(['a', 1], ['b', 1]), false) 44 | assert.deepStrictEqual(eq.equals(['a', 1], ['a', 2]), false) 45 | }) 46 | 47 | it('intersect', () => { 48 | const eq = pipe(E.struct({ a: E.string }), E.intersect(E.struct({ b: E.number }))) 49 | assert.deepStrictEqual(eq.equals({ a: 'a', b: 1 }, { a: 'a', b: 1 }), true) 50 | assert.deepStrictEqual(eq.equals({ a: 'a', b: 1 }, { a: 'c', b: 1 }), false) 51 | assert.deepStrictEqual(eq.equals({ a: 'a', b: 1 }, { a: 'a', b: 2 }), false) 52 | }) 53 | 54 | it('lazy', () => { 55 | interface A { 56 | a: number 57 | b: Array 58 | } 59 | 60 | const eq: Eq = E.Schemable.lazy('A', () => 61 | E.struct({ 62 | a: E.number, 63 | b: E.array(eq) 64 | }) 65 | ) 66 | assert.strictEqual(eq.equals({ a: 1, b: [] }, { a: 1, b: [] }), true) 67 | assert.strictEqual(eq.equals({ a: 1, b: [{ a: 2, b: [] }] }, { a: 1, b: [{ a: 2, b: [] }] }), true) 68 | assert.strictEqual(eq.equals({ a: 1, b: [] }, { a: 2, b: [] }), false) 69 | assert.strictEqual(eq.equals({ a: 1, b: [{ a: 2, b: [] }] }, { a: 1, b: [{ a: 3, b: [] }] }), false) 70 | }) 71 | 72 | it('sum', () => { 73 | const sum = E.sum('_tag') 74 | const eq = sum({ 75 | A: E.struct({ _tag: E.Schemable.literal('A'), a: E.string }), 76 | B: E.struct({ _tag: E.Schemable.literal('B'), b: E.number }) 77 | }) 78 | assert.strictEqual(eq.equals({ _tag: 'A', a: 'a' }, { _tag: 'A', a: 'a' }), true) 79 | assert.strictEqual(eq.equals({ _tag: 'B', b: 1 }, { _tag: 'B', b: 1 }), true) 80 | assert.strictEqual(eq.equals({ _tag: 'A', a: 'a' }, { _tag: 'B', b: 1 }), false) 81 | assert.strictEqual(eq.equals({ _tag: 'A', a: 'a' }, { _tag: 'A', a: 'b' }), false) 82 | assert.strictEqual(eq.equals({ _tag: 'B', b: 1 }, { _tag: 'B', b: 2 }), false) 83 | }) 84 | 85 | it('refine', () => { 86 | interface NonEmptyStringBrand { 87 | readonly NonEmptyString: unique symbol 88 | } 89 | type NonEmptyString = string & NonEmptyStringBrand 90 | const eq = pipe( 91 | E.string, 92 | E.WithRefine.refine((s): s is NonEmptyString => s.length > 0, 'NonEmptyString') 93 | ) 94 | const a: NonEmptyString = 'a' as any 95 | const b: NonEmptyString = 'b' as any 96 | assert.deepStrictEqual(eq.equals(a, a), true) 97 | assert.deepStrictEqual(eq.equals(a, b), false) 98 | }) 99 | }) 100 | -------------------------------------------------------------------------------- /test/FreeSemigroup.ts: -------------------------------------------------------------------------------- 1 | import * as FS from '../src/FreeSemigroup' 2 | 3 | describe.concurrent('FreeSemigroup', () => { 4 | it('fold', () => { 5 | const sum: (input: FS.FreeSemigroup) => string = FS.fold( 6 | (value) => value, 7 | (left, right) => sum(left) + sum(right) 8 | ) 9 | 10 | expect(sum(FS.of('1'))).toBe('1') 11 | expect(sum(FS.concat(FS.of('1'), FS.of('2')))).toBe('12') 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /test/JsonSchema.test.ts: -------------------------------------------------------------------------------- 1 | import Ajv from 'ajv' 2 | import * as assert from 'assert' 3 | import * as C from 'fp-ts/lib/Const' 4 | import { pipe } from 'fp-ts/lib/pipeable' 5 | 6 | import * as J from './JsonSchema' 7 | 8 | const ajv = new Ajv() 9 | 10 | describe.concurrent('JsonSchema', () => { 11 | it('literal', () => { 12 | const validate = ajv.compile(J.literal('a').compile()) 13 | assert.strictEqual(validate('a'), true) 14 | assert.strictEqual(validate(1), false) 15 | }) 16 | 17 | it('string', () => { 18 | const validate = ajv.compile(J.string.compile()) 19 | assert.strictEqual(validate('a'), true) 20 | assert.strictEqual(validate(1), false) 21 | }) 22 | 23 | it('boolean', () => { 24 | const validate = ajv.compile(J.boolean.compile()) 25 | assert.strictEqual(validate(true), true) 26 | assert.strictEqual(validate(1), false) 27 | }) 28 | 29 | it('UnknownArray', () => { 30 | const validate = ajv.compile(J.UnknownArray.compile()) 31 | assert.strictEqual(validate([]), true) 32 | assert.strictEqual(validate([1, 2, 3]), true) 33 | assert.strictEqual(validate(1), false) 34 | assert.strictEqual(validate([1, undefined, 3]), true) 35 | }) 36 | 37 | it('UnknownRecord', () => { 38 | const validate = ajv.compile(J.UnknownRecord.compile()) 39 | assert.strictEqual(validate({}), true) 40 | assert.strictEqual(validate({ a: 'a', b: 1 }), true) 41 | assert.strictEqual(validate(1), false) 42 | assert.strictEqual(validate({ a: 'a', b: 1, c: undefined }), true) 43 | }) 44 | 45 | it('struct', () => { 46 | const schema = J.struct({ a: J.string, b: J.number }).compile() 47 | const validate = ajv.compile(schema) 48 | assert.strictEqual(validate({ a: 'a', b: 1 }), true) 49 | assert.strictEqual(validate({ a: 'a' }), false) 50 | assert.strictEqual(validate({ a: 'a', b: 'b' }), false) 51 | }) 52 | 53 | it('partial', () => { 54 | const validate = ajv.compile(J.partial({ a: J.string, b: J.number }).compile()) 55 | assert.strictEqual(validate({ a: 'a', b: 1 }), true) 56 | assert.strictEqual(validate({ a: 'a' }), true) 57 | assert.strictEqual(validate({ a: 'a', b: undefined }), true) 58 | assert.strictEqual(validate({ a: 'a', b: 'b' }), false) 59 | }) 60 | 61 | it('record', () => { 62 | const validate = ajv.compile(J.record(J.string).compile()) 63 | assert.strictEqual(validate({ a: 'a', b: 'b' }), true) 64 | assert.strictEqual(validate({ a: 'a', b: 1 }), false) 65 | }) 66 | 67 | it('array', () => { 68 | const validate = ajv.compile(J.array(J.number).compile()) 69 | assert.strictEqual(validate([]), true) 70 | assert.strictEqual(validate([1, 2, 3]), true) 71 | assert.strictEqual(validate([1, 'a', 3]), false) 72 | }) 73 | 74 | it('tuple', () => { 75 | const validate = ajv.compile(J.tuple(J.string, J.number).compile()) 76 | assert.strictEqual(validate(['a', 1]), true) 77 | assert.strictEqual(validate(['a', 1, true]), false) 78 | assert.strictEqual(validate(['a']), false) 79 | }) 80 | 81 | describe.concurrent('intersection', () => { 82 | it('should handle non primitive values', () => { 83 | const validate = ajv.compile(pipe(J.struct({ a: J.string }), J.intersect(J.struct({ b: J.number }))).compile()) 84 | assert.strictEqual(validate({ a: 'a', b: 1 }), true) 85 | assert.strictEqual(validate({ a: 'a' }), false) 86 | }) 87 | 88 | interface IntBrand { 89 | readonly Int: unique symbol 90 | } 91 | type Int = number & IntBrand 92 | 93 | const Int: J.JsonSchema = { 94 | compile: () => 95 | C.make({ 96 | type: 'integer' 97 | }) 98 | } 99 | const Positive: J.JsonSchema = { 100 | compile: () => 101 | C.make({ 102 | type: 'number', 103 | minimum: 0 104 | }) 105 | } 106 | 107 | it('should handle primitives', () => { 108 | const validate = ajv.compile(pipe(Int, J.intersect(Positive)).compile()) 109 | assert.strictEqual(validate(1), true) 110 | assert.strictEqual(validate(-1), false) 111 | }) 112 | }) 113 | 114 | it('sum', () => { 115 | const sum = J.sum('_tag') 116 | 117 | const A = J.struct({ _tag: J.literal('A'), a: J.string }) 118 | const B = J.struct({ _tag: J.literal('B'), b: J.number }) 119 | const validate = ajv.compile(sum({ A, B }).compile()) 120 | assert.strictEqual(validate({ _tag: 'A', a: 'a' }), true) 121 | assert.strictEqual(validate({ _tag: 'B', b: 1 }), true) 122 | assert.strictEqual(validate(undefined), false) 123 | assert.strictEqual(validate({}), false) 124 | }) 125 | 126 | it('union', () => { 127 | const validate = ajv.compile(J.union(J.string, J.number).compile()) 128 | assert.strictEqual(validate('a'), true) 129 | assert.strictEqual(validate(1), true) 130 | assert.strictEqual(validate(true), false) 131 | }) 132 | 133 | describe.concurrent('lazy', () => { 134 | it('should support recursive json schemas', () => { 135 | interface A { 136 | readonly a: number 137 | readonly b?: A 138 | } 139 | 140 | const schema: J.JsonSchema = J.lazy('A', () => 141 | pipe(J.struct({ a: J.number }), J.intersect(J.partial({ b: schema }))) 142 | ) 143 | 144 | const jsonSchema = schema.compile() 145 | const validate = ajv.compile(jsonSchema) 146 | assert.strictEqual(validate({}), false) 147 | assert.strictEqual(validate({ a: 1 }), true) 148 | assert.strictEqual(validate({ a: 1, b: null }), false) 149 | assert.strictEqual(validate({ a: 1, b: { a: 2 } }), true) 150 | }) 151 | 152 | it('should support mutually recursive json schemas', () => { 153 | interface A { 154 | readonly b?: B 155 | } 156 | interface B { 157 | readonly a?: A 158 | } 159 | const A: J.JsonSchema = J.lazy('A', () => J.partial({ b: B })) 160 | const B: J.JsonSchema = J.lazy('B', () => J.partial({ a: A })) 161 | const jsonSchema = A.compile() 162 | const validateA = ajv.compile(jsonSchema) 163 | assert.strictEqual(validateA({}), true) 164 | assert.strictEqual(validateA({ b: {} }), true) 165 | assert.strictEqual(validateA({ b: { a: {} } }), true) 166 | 167 | const validateB = ajv.compile(B.compile()) 168 | assert.strictEqual(validateB({}), true) 169 | assert.strictEqual(validateB({ a: {} }), true) 170 | assert.strictEqual(validateB({ a: { b: {} } }), true) 171 | }) 172 | }) 173 | }) 174 | -------------------------------------------------------------------------------- /test/JsonSchema.ts: -------------------------------------------------------------------------------- 1 | import * as C from 'fp-ts/lib/Const' 2 | import { identity } from 'fp-ts/lib/function' 3 | import { pipe } from 'fp-ts/lib/pipeable' 4 | import * as R from 'fp-ts/lib/ReadonlyRecord' 5 | import { JSONSchema7 } from 'json-schema' 6 | 7 | import * as S from '../src/Schemable' 8 | 9 | // ------------------------------------------------------------------------------------- 10 | // model 11 | // ------------------------------------------------------------------------------------- 12 | 13 | export interface JsonSchema { 14 | readonly compile: (definitions?: Record) => C.Const 15 | } 16 | 17 | // ------------------------------------------------------------------------------------- 18 | // constructors 19 | // ------------------------------------------------------------------------------------- 20 | 21 | export function literal], L extends S.Literal = S.Literal>( 22 | ...values: A 23 | ): JsonSchema { 24 | return { 25 | compile: () => C.make({ enum: [...values] }) 26 | } 27 | } 28 | 29 | // ------------------------------------------------------------------------------------- 30 | // primitives 31 | // ------------------------------------------------------------------------------------- 32 | 33 | export const string: JsonSchema = { 34 | compile: () => C.make({ type: 'string' }) 35 | } 36 | 37 | export const number: JsonSchema = { 38 | compile: () => C.make({ type: 'number' }) 39 | } 40 | 41 | export const boolean: JsonSchema = { 42 | compile: () => C.make({ type: 'boolean' }) 43 | } 44 | 45 | // tslint:disable-next-line: readonly-array 46 | export const UnknownArray: JsonSchema> = { 47 | compile: () => C.make({ type: 'array' }) 48 | } 49 | 50 | export const UnknownRecord: JsonSchema> = { 51 | compile: () => C.make({ type: 'object' }) 52 | } 53 | 54 | // ------------------------------------------------------------------------------------- 55 | // combinators 56 | // ------------------------------------------------------------------------------------- 57 | 58 | const nullJsonSchema: JsonSchema = { 59 | compile: () => C.make({ enum: [null] }) 60 | } 61 | 62 | export function nullable(or: JsonSchema): JsonSchema { 63 | return union(nullJsonSchema, or) 64 | } 65 | 66 | export function struct(properties: { [K in keyof A]: JsonSchema }): JsonSchema { 67 | return { 68 | compile: (lazy) => 69 | C.make({ 70 | type: 'object', 71 | properties: pipe( 72 | properties, 73 | R.map, JSONSchema7>((p) => p.compile(lazy)) 74 | ), 75 | required: Object.keys(properties) 76 | }) 77 | } 78 | } 79 | 80 | export function partial(properties: { [K in keyof A]: JsonSchema }): JsonSchema> { 81 | return { 82 | compile: (lazy) => 83 | C.make({ 84 | type: 'object', 85 | properties: pipe( 86 | properties, 87 | R.map, JSONSchema7>((p) => p.compile(lazy)) 88 | ) 89 | }) 90 | } 91 | } 92 | 93 | export function record(codomain: JsonSchema): JsonSchema> { 94 | return { 95 | compile: (lazy) => 96 | C.make({ 97 | type: 'object', 98 | additionalProperties: codomain.compile(lazy) 99 | }) 100 | } 101 | } 102 | 103 | // tslint:disable-next-line: readonly-array 104 | export function array(items: JsonSchema): JsonSchema> { 105 | return { 106 | compile: (lazy) => 107 | C.make({ 108 | type: 'array', 109 | items: items.compile(lazy) 110 | }) 111 | } 112 | } 113 | 114 | export function tuple>( 115 | ...components: { [K in keyof A]: JsonSchema } 116 | ): JsonSchema { 117 | const len = components.length 118 | return { 119 | compile: (lazy) => 120 | C.make({ 121 | type: 'array', 122 | items: len > 0 ? components.map((c) => c.compile(lazy)) : undefined, 123 | minItems: len, 124 | maxItems: len 125 | }) 126 | } 127 | } 128 | 129 | export const intersect = 130 | (right: JsonSchema) => 131 | (left: JsonSchema): JsonSchema => ({ 132 | compile: (lazy) => C.make({ allOf: [left.compile(lazy), right.compile(lazy)] }) 133 | }) 134 | 135 | export function sum( 136 | _tag: T 137 | ): (members: { [K in keyof A]: JsonSchema> }) => JsonSchema { 138 | return (members: Record>) => { 139 | return { 140 | compile: (lazy) => C.make({ anyOf: Object.keys(members).map((k) => members[k].compile(lazy)) }) 141 | } 142 | } 143 | } 144 | 145 | export function lazy(id: string, f: () => JsonSchema): JsonSchema { 146 | const $ref = `#/definitions/${id}` 147 | return { 148 | compile: (definitions) => { 149 | if (definitions !== undefined) { 150 | if (Object.prototype.hasOwnProperty.call(definitions, id)) { 151 | return C.make({ $ref }) 152 | } 153 | definitions[id] = undefined 154 | return (definitions[id] = f().compile(definitions)) 155 | } else { 156 | definitions = { [id]: undefined } 157 | definitions[id] = f().compile(definitions) 158 | return C.make({ 159 | definitions, 160 | $ref 161 | }) 162 | } 163 | } 164 | } 165 | } 166 | 167 | export const readonly: (arb: JsonSchema) => JsonSchema> = identity 168 | 169 | export function union]>( 170 | ...members: { [K in keyof A]: JsonSchema } 171 | ): JsonSchema { 172 | return { 173 | compile: (lazy) => C.make({ anyOf: members.map((m) => m.compile(lazy)) }) 174 | } 175 | } 176 | 177 | // ------------------------------------------------------------------------------------- 178 | // instances 179 | // ------------------------------------------------------------------------------------- 180 | 181 | export const URI = 'io-ts/JsonSchema' 182 | 183 | export type URI = typeof URI 184 | 185 | declare module 'fp-ts/lib/HKT' { 186 | interface URItoKind { 187 | readonly [URI]: JsonSchema 188 | } 189 | } 190 | 191 | export const Schemable: S.Schemable1 & S.WithUnknownContainers1 & S.WithUnion1 = { 192 | URI, 193 | literal, 194 | string, 195 | number, 196 | boolean, 197 | UnknownArray, 198 | UnknownRecord, 199 | nullable, 200 | type: struct, 201 | struct, 202 | partial, 203 | record, 204 | array, 205 | tuple: tuple as S.Schemable1['tuple'], 206 | intersect, 207 | sum, 208 | lazy, 209 | readonly, 210 | union: union as S.WithUnion1['union'] 211 | } 212 | -------------------------------------------------------------------------------- /test/Schema.ts: -------------------------------------------------------------------------------- 1 | import * as fc from 'fast-check' 2 | import { isRight } from 'fp-ts/lib/Either' 3 | import { pipe } from 'fp-ts/lib/pipeable' 4 | 5 | import * as D from '../src/Decoder' 6 | import * as Eq from '../src/Eq' 7 | import * as G from '../src/Guard' 8 | import { interpreter, make, Schema } from '../src/Schema' 9 | import * as A from './Arbitrary' 10 | 11 | function check(schema: Schema): void { 12 | const arb = interpreter(A.Schemable)(schema) 13 | const decoder = interpreter(D.Schemable)(schema) 14 | const guard = interpreter(G.Schemable)(schema) 15 | const eq = interpreter(Eq.Schemable)(schema) 16 | // decoders, guards and eqs should be aligned 17 | fc.assert(fc.property(arb, (a) => isRight(decoder.decode(a)) && guard.is(a) && eq.equals(a, a))) 18 | } 19 | describe.concurrent('Schema', () => { 20 | it('string', () => { 21 | check(make((S) => S.string)) 22 | }) 23 | 24 | it('number', () => { 25 | check(make((S) => S.number)) 26 | }) 27 | 28 | it('boolean', () => { 29 | check(make((S) => S.boolean)) 30 | }) 31 | 32 | it('literal', () => { 33 | check(make((S) => S.literal('a'))) 34 | check(make((S) => S.literal('a', 1))) 35 | check(make((S) => S.literal('a', null))) 36 | }) 37 | 38 | it('nullable', () => { 39 | check(make((S) => S.nullable(S.string))) 40 | }) 41 | 42 | it('struct', () => { 43 | check( 44 | make((S) => 45 | S.struct({ 46 | name: S.string, 47 | age: S.number 48 | }) 49 | ) 50 | ) 51 | }) 52 | 53 | it('partial', () => { 54 | check( 55 | make((S) => 56 | S.partial({ 57 | name: S.string, 58 | age: S.number 59 | }) 60 | ) 61 | ) 62 | }) 63 | 64 | it('record', () => { 65 | check(make((S) => S.record(S.string))) 66 | }) 67 | 68 | it('array', () => { 69 | check(make((S) => S.array(S.string))) 70 | }) 71 | 72 | it('tuple', () => { 73 | check(make((S) => S.tuple())) 74 | check(make((S) => S.tuple(S.string))) 75 | check(make((S) => S.tuple(S.string, S.number))) 76 | }) 77 | 78 | it('intersect', () => { 79 | check(make((S) => pipe(S.struct({ a: S.string }), S.intersect(S.struct({ b: S.number }))))) 80 | }) 81 | 82 | it('sum', () => { 83 | const A = make((S) => S.struct({ _tag: S.literal('A'), a: S.string })) 84 | const B = make((S) => S.struct({ _tag: S.literal('B'), b: S.number })) 85 | check(make((S) => S.sum('_tag')({ A: A(S), B: B(S) }))) 86 | }) 87 | 88 | it('lazy', () => { 89 | interface A { 90 | a: string 91 | b?: A 92 | c?: number 93 | } 94 | 95 | const schema: Schema = make((S) => 96 | S.lazy('A', () => pipe(S.struct({ a: S.string }), S.intersect(S.partial({ b: schema(S), c: S.number })))) 97 | ) 98 | check(schema) 99 | }) 100 | }) 101 | -------------------------------------------------------------------------------- /test/Schemable.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | 3 | import * as S from '../src/Schemable' 4 | 5 | describe.concurrent('DecoderT', () => { 6 | describe.concurrent('intersect', () => { 7 | it('should concat strings', () => { 8 | assert.deepStrictEqual(S.intersect_('a', 'b'), 'b') 9 | }) 10 | 11 | it('should concat numbers', () => { 12 | assert.deepStrictEqual(S.intersect_(1, 2), 2) 13 | }) 14 | 15 | it('should concat booleans', () => { 16 | assert.deepStrictEqual(S.intersect_(true, false), false) 17 | }) 18 | 19 | it('should concat nulls', () => { 20 | assert.deepStrictEqual(S.intersect_(null, null), null) 21 | }) 22 | 23 | it('should concat undefineds', () => { 24 | assert.deepStrictEqual(S.intersect_(undefined, undefined), undefined) 25 | }) 26 | 27 | it('should concat objects', () => { 28 | assert.deepStrictEqual(S.intersect_({ a: 1 }, { b: 2 }), { a: 1, b: 2 }) 29 | }) 30 | 31 | it('should concat a string with an object', () => { 32 | assert.deepStrictEqual(S.intersect_('a', { a: 1 }), { 0: 'a', a: 1 }) 33 | }) 34 | 35 | it('should concat a number with an object', () => { 36 | assert.deepStrictEqual(S.intersect_(1, { a: 1 }), { a: 1 }) 37 | }) 38 | 39 | it('should concat a boolean with an object', () => { 40 | assert.deepStrictEqual(S.intersect_(true, { a: 1 }), { a: 1 }) 41 | }) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /test/Type.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import * as fc from 'fast-check' 3 | import { isLeft, isRight } from 'fp-ts/lib/Either' 4 | import { HKT, Kind, Kind2, URIS, URIS2 } from 'fp-ts/lib/HKT' 5 | import { pipe } from 'fp-ts/lib/pipeable' 6 | 7 | import * as t from '../src' 8 | import * as D from '../src/Decoder' 9 | import * as G from '../src/Guard' 10 | import { 11 | memoize, 12 | Schemable, 13 | Schemable1, 14 | Schemable2C, 15 | WithUnion, 16 | WithUnion1, 17 | WithUnion2C, 18 | WithUnknownContainers, 19 | WithUnknownContainers1, 20 | WithUnknownContainers2C 21 | } from '../src/Schemable' 22 | import * as _ from '../src/Type' 23 | import * as A from './Arbitrary' 24 | 25 | interface Schema { 26 | (S: Schemable & WithUnknownContainers & WithUnion): HKT 27 | } 28 | 29 | function make(f: Schema): Schema { 30 | return memoize(f) 31 | } 32 | 33 | const interpreter: { 34 | (S: Schemable2C & WithUnknownContainers2C & WithUnion2C): ( 35 | schema: Schema 36 | ) => Kind2 37 | (S: Schemable1 & WithUnknownContainers1 & WithUnion1): (schema: Schema) => Kind 38 | } = (S: any) => (schema: any) => schema(S) 39 | 40 | function check(schema: Schema, type: t.Type): void { 41 | const arb = interpreter(A.Schemable)(schema) 42 | const decoder = interpreter({ ...D.Schemable, ...D.WithUnknownContainers, ...D.WithUnion })(schema) 43 | const guard = interpreter({ ...G.Schemable, ...G.WithUnknownContainers, ...G.WithUnion })(schema) 44 | const itype = interpreter({ ..._.Schemable, ..._.WithUnknownContainers, ..._.WithUnion })(schema) 45 | // decoder and type should be aligned 46 | fc.assert(fc.property(arb, (a) => isRight(decoder.decode(a)) === isRight(type.decode(a)))) 47 | // interpreted type and type should be aligned 48 | fc.assert(fc.property(arb, (a) => isRight(itype.decode(a)) === isRight(type.decode(a)))) 49 | // guard and `Type`'s `is` should be aligned 50 | fc.assert(fc.property(arb, (a) => guard.is(a) === type.is(a))) 51 | } 52 | 53 | describe.concurrent('Type', () => { 54 | it('string', () => { 55 | check( 56 | make((S) => S.string), 57 | t.string 58 | ) 59 | }) 60 | 61 | describe.concurrent('number', () => { 62 | it('number', () => { 63 | check( 64 | make((S) => S.number), 65 | t.number 66 | ) 67 | }) 68 | 69 | it('should exclude NaN', () => { 70 | assert.deepStrictEqual(isLeft(_.number.decode(NaN)), true) 71 | }) 72 | }) 73 | 74 | it('boolean', () => { 75 | check( 76 | make((S) => S.boolean), 77 | t.boolean 78 | ) 79 | }) 80 | 81 | it('UnknownArray', () => { 82 | check( 83 | make((S) => S.UnknownArray), 84 | t.UnknownArray 85 | ) 86 | }) 87 | 88 | it('UnknownRecord', () => { 89 | check( 90 | make((S) => S.UnknownRecord), 91 | t.UnknownRecord 92 | ) 93 | }) 94 | 95 | it('literal', () => { 96 | check( 97 | make((S) => S.literal('a', 'b')), 98 | t.keyof({ a: null, b: null }) 99 | ) 100 | }) 101 | 102 | it('nullable', () => { 103 | check( 104 | make((S) => S.nullable(S.string)), 105 | t.union([t.null, t.string]) 106 | ) 107 | }) 108 | 109 | it('struct', () => { 110 | check( 111 | make((S) => 112 | S.struct({ 113 | name: S.string, 114 | age: S.number 115 | }) 116 | ), 117 | t.type({ 118 | name: t.string, 119 | age: t.number 120 | }) 121 | ) 122 | }) 123 | 124 | it('partial', () => { 125 | check( 126 | make((S) => 127 | S.partial({ 128 | name: S.string, 129 | age: S.number 130 | }) 131 | ), 132 | t.partial({ 133 | name: t.string, 134 | age: t.number 135 | }) 136 | ) 137 | }) 138 | 139 | it('record', () => { 140 | check( 141 | make((S) => S.record(S.string)), 142 | t.record(t.string, t.string) 143 | ) 144 | }) 145 | 146 | it('array', () => { 147 | check( 148 | make((S) => S.array(S.string)), 149 | t.array(t.string) 150 | ) 151 | }) 152 | 153 | it('tuple', () => { 154 | check( 155 | make((S) => S.tuple(S.string)), 156 | t.tuple([t.string]) 157 | ) 158 | check( 159 | make((S) => S.tuple(S.string, S.number)), 160 | t.tuple([t.string, t.number]) 161 | ) 162 | }) 163 | 164 | it('intersect', () => { 165 | check( 166 | make((S) => pipe(S.struct({ a: S.string }), S.intersect(S.struct({ b: S.number })))), 167 | t.intersection([t.type({ a: t.string }), t.type({ b: t.number })]) 168 | ) 169 | }) 170 | 171 | it('sum', () => { 172 | check( 173 | make((S) => 174 | S.sum('_tag')({ 175 | A: S.struct({ _tag: S.literal('A'), a: S.string }), 176 | B: S.struct({ _tag: S.literal('B'), b: S.number }) 177 | }) 178 | ), 179 | t.union([t.type({ _tag: t.literal('A'), a: t.string }), t.type({ _tag: t.literal('B'), b: t.number })]) 180 | ) 181 | }) 182 | 183 | it('lazy', () => { 184 | interface A { 185 | a: string 186 | b?: A 187 | c?: number 188 | } 189 | 190 | const schema: Schema = make((S) => 191 | S.lazy('A', () => pipe(S.struct({ a: S.string }), S.intersect(S.partial({ b: schema(S), c: S.number })))) 192 | ) 193 | const type: t.Type = t.recursion('A', () => 194 | t.intersection([t.type({ a: t.string }), t.partial({ b: type, c: t.number })]) 195 | ) 196 | check(schema, type) 197 | }) 198 | 199 | it('union', () => { 200 | check( 201 | make((S) => S.union(S.string, S.number)), 202 | t.union([t.string, t.number]) 203 | ) 204 | }) 205 | 206 | it('refine', () => { 207 | interface NonEmptyStringBrand { 208 | readonly NonEmptyString: unique symbol 209 | } 210 | type NonEmptyString = string & NonEmptyStringBrand 211 | const type = pipe( 212 | _.string, 213 | _.refine((s): s is NonEmptyString => s.length > 0, 'NonEmptyString') 214 | ) 215 | assert.deepStrictEqual(isRight(type.decode('a')), true) 216 | assert.deepStrictEqual(isRight(type.decode('')), false) 217 | }) 218 | }) 219 | -------------------------------------------------------------------------------- /test/helpers.ts: -------------------------------------------------------------------------------- 1 | import { pipe } from 'fp-ts/lib/pipeable' 2 | 3 | import * as D from '../src/Decoder' 4 | import * as E from '../src/Encoder' 5 | import * as G from '../src/Guard' 6 | 7 | // ------------------------------------------------------------------------------------- 8 | // guards 9 | // ------------------------------------------------------------------------------------- 10 | 11 | export const guardUndefined: G.Guard = { 12 | is: (u): u is undefined => u === undefined 13 | } 14 | 15 | // ------------------------------------------------------------------------------------- 16 | // decoders 17 | // ------------------------------------------------------------------------------------- 18 | 19 | export const decoderUndefined: D.Decoder = D.fromGuard(guardUndefined, 'undefined') 20 | 21 | export const decoderNumberFromString: D.Decoder = { 22 | decode: (s) => { 23 | const n = parseFloat(s) 24 | return isNaN(n) ? D.failure(s, 'parsable to a number') : D.success(n) 25 | } 26 | } 27 | 28 | export const decoderNumberFromUnknownString: D.Decoder = pipe( 29 | D.string, 30 | D.compose(decoderNumberFromString) 31 | ) 32 | 33 | export interface PositiveBrand { 34 | readonly Positive: unique symbol 35 | } 36 | 37 | export type Positive = number & PositiveBrand 38 | 39 | export const decoderPositive: D.Decoder = pipe( 40 | D.number, 41 | D.refine((n): n is Positive => n > 0, 'Positive') 42 | ) 43 | 44 | export interface IntBrand { 45 | readonly Int: unique symbol 46 | } 47 | 48 | export type Int = number & IntBrand 49 | 50 | export const decoderInt: D.Decoder = pipe( 51 | D.number, 52 | D.refine((n): n is Int => Number.isInteger(n), 'Int') 53 | ) 54 | 55 | // ------------------------------------------------------------------------------------- 56 | // encoders 57 | // ------------------------------------------------------------------------------------- 58 | 59 | export const encoderNumberToString: E.Encoder = { 60 | encode: String 61 | } 62 | 63 | export const encoderBooleanToNumber: E.Encoder = { 64 | encode: (b) => (b ? 1 : 0) 65 | } 66 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": true 5 | }, 6 | "include": ["./**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.build-es6.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.build.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/es6", 5 | "module": "es6" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": false 5 | }, 6 | "include": ["./src"] 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/lib", 4 | "noEmit": true, 5 | "declaration": true, 6 | "esModuleInterop": true, 7 | "module": "commonjs", 8 | "noImplicitReturns": false, 9 | "noUnusedLocals": true, 10 | "noUnusedParameters": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "strict": true, 13 | "target": "es5", 14 | "moduleResolution": "node", 15 | "forceConsistentCasingInFileNames": true, 16 | "stripInternal": true, 17 | "lib": ["es2015", "ESNext.BigInt"], 18 | "skipLibCheck": true, 19 | "types": ["vitest/globals"] 20 | }, 21 | "include": ["./src", "./test", "./scripts", "./dtslint"] 22 | } 23 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from 'vite' 3 | 4 | export default defineConfig({ 5 | test: { 6 | include: ['./test/**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], 7 | exclude: [ 8 | './test/**/util.ts', 9 | './test/JsonSchema.ts', 10 | './test/helpers.ts', 11 | './test/Arbitrary.ts', 12 | './test/2.1.x/helpers.ts' 13 | ], 14 | globals: true, 15 | coverage: { 16 | provider: 'istanbul' 17 | } 18 | } 19 | }) 20 | --------------------------------------------------------------------------------