io-ts-extra
Version:
Adds pattern matching, optional properties, and several other helpers and types, to io-ts.
387 lines (286 loc) • 14.8 kB
Markdown
# io-ts-extra
Adds pattern matching, optional properties, and several other helpers and types, to io-ts.
<!-- codegen:start {preset: badges} -->
[](https://github.com/mmkal/ts/actions?query=workflow%3A%22Node+CI%22)
[](https://codecov.io/gh/mmkal/ts/tree/main/packages/io-ts-extra)
[](https://npmjs.com/package/io-ts-extra)
<!-- codegen:end -->
## Features
* Pattern matching
* Optional properties
* Advanced refinement types
* Regex types
* Parser helpers
## Contents
<!-- codegen:start {preset: markdownTOC, minDepth: 2, maxDepth: 5} -->
- [Features](#features)
- [Contents](#contents)
- [Motivation](#motivation)
- [Comparison with io-ts](#comparison-with-io-ts)
- [Comparison with io-ts-types](#comparison-with-io-ts-types)
- [Documentation](#documentation)
- [Pattern matching](#pattern-matching)
- [match](#match)
- [matcher](#matcher)
- [Shorthand](#shorthand)
- [codecFromShorthand](#codecfromshorthand)
- [Codecs/Combinators](#codecscombinators)
- [sparseType](#sparsetype)
- [optional](#optional)
- [mapper](#mapper)
- [parser](#parser)
- [strict](#strict)
- [narrow](#narrow)
- [validationErrors](#validationerrors)
- [regexp](#regexp)
- [instanceOf](#instanceof)
<!-- codegen:end -->
## Motivation
### Comparison with [io-ts](https://github.com/gcanti/io-ts)
The maintainers of io-ts are (rightly) strict about keeping the API surface small and manageable, and the implementation clean. As a result, io-ts is a powerful but somewhat low-level framework.
This library implements some higher-level concepts for use in real-life applications with complex requirements - combinators, utilities, parsers, reporters etc.
### Comparison with [io-ts-types](https://github.com/gcanti/io-ts-types)
io-ts-types exists for similar reasons. This library will aim to be orthogonal to io-ts-types, and avoid re-inventing the wheel by exposing types that already exist there.
io-ts-extra will also aim to provide more high-level utilities and combinators than pre-defined codecs.
Philosophically, this library will skew slightly more towards pragmatism at the expense of type soundness - for example the stance on [t.refinement vs t.brand](https://github.com/gcanti/io-ts/issues/373).
This package is also less mature. It's currently in v0, so will have a different release cadence than io-ts-types.
## Documentation
### Pattern matching
<!-- codegen:start {preset: markdownFromJsdoc, source: src/match.ts, export: match} -->
#### [match](https://github.com/mmkal/ts/tree/4578555/packages/io-ts-extra/src/match.ts#L153)
Match an object against a number of cases. Loosely based on Scala's pattern matching.
##### Example
```typescript
// get a value which could be a string or a number:
const value = Math.random() < 0.5 ? 'foo' : Math.random() * 10
const stringified = match(value)
.case(String, s => `the message is ${s}`)
.case(7, () => 'exactly seven')
.case(Number, n => `the number is ${n}`)
.get()
```
Under the hood, io-ts is used for validation. The first argument can be a "shorthand" for a type, but you can also pass in io-ts codecs directly for more complex types:
##### Example
```typescript
// get a value which could be a string or a number:
const value = Math.random() < 0.5 ? 'foo' : 123
const stringified = match(value)
.case(t.number, n => `the number is ${n}`)
.case(t.string, s => `the message is ${s}`)
.get()
```
you can use a predicate function or `t.refinement` for the equivalent of scala's `case x: Int if x > 2`:
##### Example
```typescript
// value which could be a string, or a real number in [0, 10):
const value = Math.random() < 0.5 ? 'foo' : Math.random() * 10
const stringified = match(value)
.case(Number, n => n > 2, n => `big number: ${n}`)
.case(Number, n => `small number: ${n}`)
.default(x => `not a number: ${x}`)
.get()
```
##### Example
```typescript
// value which could be a string, or a real number in [0, 10):
const value = Math.random() < 0.5 ? 'foo' : Math.random() * 10
const stringified = match(value)
.case(t.refinement(t.number, n => n > 2), n => `big number: ${n}`)
.case(t.number, n => `small number: ${n}`)
.default(x => `not a number: ${x}`)
.get()
```
note: when using predicates or `t.refinement`, the type being refined is not considered exhaustively matched, so you'll usually need to add a non-refined option, or you can also use `.default` as a fallback case (the equivalent of `.case(t.any, ...)`)
##### Params
|name|description |
|----|--------------------------------|
|obj |the object to be pattern-matched|
<!-- codegen:end -->
<!-- codegen:start {preset: markdownFromJsdoc, source: src/match.ts, export: matcher} -->
#### [matcher](https://github.com/mmkal/ts/tree/4578555/packages/io-ts-extra/src/match.ts#L185)
Like @see match but no object is passed in when constructing the case statements. Instead `.get` is a function into which a value should be passed.
##### Example
```typescript
const Email = t.type({sender: t.string, subject: t.string, body: t.string})
const SMS = t.type({from: t.string, content: t.string})
const Message = t.union([Email, SMS])
type Message = typeof Message._A
const content = matcher<MessageType>()
.case(SMS, s => s.content)
.case(Email, e => e.subject + '\n\n' + e.body)
.get({from: '123', content: 'hello'})
expect(content).toEqual('hello')
```
The function returned by `.get` is stateless and has no `this` context, you can store it in a variable and pass it around:
##### Example
```typescript
const getContent = matcher<Message>()
.case(SMS, s => s.content)
.case(Email, e => e.subject + '\n\n' + e.body)
.get
const allMessages: Message[] = getAllMessages();
const contents = allMessages.map(getContent);
```
<!-- codegen:end -->
#### Shorthand
The "shorthand" format for type specifications maps to io-ts types as follows:
<!-- codegen:start {preset: markdownFromJsdoc, source: src/shorthand.ts, export: codecFromShorthand} -->
#### [codecFromShorthand](https://github.com/mmkal/ts/tree/4578555/packages/io-ts-extra/src/shorthand.ts#L79)
Gets an io-ts codec from a shorthand input:
|shorthand|io-ts type|
|-|-|
|`String`, `Number`, `Boolean`|`t.string`, `t.number`, `t.boolean`|
|Literal raw strings, numbers and booleans e.g. `7` or `'foo'`|`t.literal(7)`, `t.literal('foo')` etc.|
|Regexes e.g. `/^foo/`|see [regexp](#regexp)|
|`null` and `undefined`|`t.null` and `t.undefined`|
|No input (_not_ the same as explicitly passing `undefined`)|`t.unknown`|
|Objects e.g. `{ foo: String, bar: { baz: Number } }`|`t.type(...)` e.g. `t.type({foo: t.string, bar: t.type({ baz: t.number }) })`
|`Array`|`t.unknownArray`|
|`Object`|`t.object`|
|One-element arrays e.g. `[String]`|`t.array(...)` e.g. `t.array(t.string)`|
|Tuples with explicit length e.g. `[2, [String, Number]]`|`t.tuple` e.g. `t.tuple([t.string, t.number])`|
|io-ts codecs|unchanged|
|Unions, intersections, partials, tuples with more than 3 elements, and other complex types|not supported, except by passing in an io-ts codec|
<!-- codegen:end -->
### Codecs/Combinators
<!-- codegen:start {preset: markdownFromJsdoc, source: src/combinators.ts, export: sparseType} -->
#### [sparseType](https://github.com/mmkal/ts/tree/4578555/packages/io-ts-extra/src/combinators.ts#L40)
Can be used much like `t.type` from io-ts, but any property types wrapped with `optional` from this package need not be supplied. Roughly equivalent to using `t.intersection` with `t.type` and `t.partial`.
##### Example
```typescript
const Person = sparseType({
name: t.string,
age: optional(t.number),
})
// no error - `age` is optional
const bob: typeof Person._A = { name: 'bob' }
```
##### Params
|name |description |
|-----|----------------------------------------------|
|props|equivalent to the `props` passed into `t.type`|
##### Returns
a type with `props` field, so the result can be introspected similarly to a type built with
`t.type` or `t.partial` - which isn't the case if you manually use `t.intersection([t.type({...}), t.partial({...})])`
<!-- codegen:end -->
<!-- codegen:start {preset: markdownFromJsdoc, source: src/combinators.ts, export: optional} -->
#### [optional](https://github.com/mmkal/ts/tree/4578555/packages/io-ts-extra/src/combinators.ts#L14)
unions the passed-in type with `null` and `undefined`.
<!-- codegen:end -->
<!-- codegen:start {preset: markdownFromJsdoc, source: src/mapper.ts, export: mapper} -->
#### [mapper](https://github.com/mmkal/ts/tree/4578555/packages/io-ts-extra/src/mapper.ts#L33)
A helper for building "parser-decoder" types - that is, types that validate an input, transform it into another type, and then validate the target type.
##### Example
```typescript
const StringsFromMixedArray = mapper(
t.array(t.any),
t.array(t.string),
mixedArray => mixedArray.filter(value => typeof value === 'string')
)
StringsFromMixedArray.decode(['a', 1, 'b', 2]) // right(['a', 'b'])
StringsFromMixedArray.decode('not an array') // left(...)
```
##### Params
|name |description |
|-----|-----------------------------------------------|
|from |the expected type of input value |
|to |the expected type of the decoded value |
|map |transform (decode) a `from` type to a `to` type|
|unmap|transfrom a `to` type back to a `from` type |
<!-- codegen:end -->
<!-- codegen:start {preset: markdownFromJsdoc, source: src/mapper.ts, export: parser} -->
#### [parser](https://github.com/mmkal/ts/tree/4578555/packages/io-ts-extra/src/mapper.ts#L78)
A helper for parsing strings into other types. A wrapper around `mapper` where the `from` type is `t.string`.
##### Example
```typescript
const IntFromString = parser(t.Int, parseFloat)
IntFromString.decode('123') // right(123)
IntFromString.decode('123.4') // left(...)
IntFromString.decode('not a number') // left(...)
IntFromString.decode(123) // left(...)
```
##### Params
|name |description |
|------|--------------------------------------------|
|type |the target type |
|decode|transform a string into the target type |
|encode|transform the target type back into a string|
<!-- codegen:end -->
<!-- codegen:start {preset: markdownFromJsdoc, source: src/combinators.ts, export: strict} -->
#### [strict](https://github.com/mmkal/ts/tree/4578555/packages/io-ts-extra/src/combinators.ts#L159)
Like `t.type`, but fails when any properties not specified in `props` are defined.
##### Example
```typescript
const Person = strict({name: t.string, age: t.number})
expectRight(Person.decode({name: 'Alice', age: 30}))
expectLeft(Person.decode({name: 'Bob', age: 30, unexpectedProp: 'abc'}))
expectRight(Person.decode({name: 'Bob', age: 30, unexpectedProp: undefined}))
```
##### Params
|name |description |
|-----|-------------------------------------------------------|
|props|dictionary of properties, same as the input to `t.type`|
|name |optional type name |
note:
- additional properties explicitly set to `undefined` _are_ permitted.
- internally, `sparseType` is used, so optional properties are supported.
<!-- codegen:end -->
<!-- codegen:start {preset: markdownFromJsdoc, source: src/narrow.ts, export: narrow} -->
#### [narrow](https://github.com/mmkal/ts/tree/4578555/packages/io-ts-extra/src/narrow.ts#L35)
Like io-ts's refinement type but:
1. Not deprecated (see https://github.com/gcanti/io-ts/issues/373)
2. Passes in `Context` to the predicate argument, so you can check parent key names etc.
3. Optionally allows returning another io-ts codec instead of a boolean for better error messages.
##### Example
```typescript
const CloudResources = narrow(
t.type({
database: t.type({username: t.string, password: t.string}),
service: t.type({dbConnectionString: t.string}),
}),
({database}) => t.type({
service: t.type({dbConnectionString: t.literal(`${database.username}:${database.password}`)}),
})
)
const valid = CloudResources.decode({
database: {username: 'user', password: 'pass'},
service: {dbConnectionString: 'user:pass'},
})
// returns a `Right`
const invalid = CloudResources.decode({
database: {username: 'user', password: 'pass'},
service: {dbConnectionString: 'user:wrongpassword'},
})
// returns a `Left` - service.dbConnectionString expected "user:pass", but got "user:wrongpassword"
```
<!-- codegen:end -->
<!-- codegen:start {preset: markdownFromJsdoc, source: src/reporters.ts, export: validationErrors} -->
#### [validationErrors](https://github.com/mmkal/ts/tree/4578555/packages/io-ts-extra/src/reporters.ts#L11)
Similar to io-ts's PathReporter, but gives slightly less verbose output.
##### Params
|name |description |
|----------|------------------------------------------------------------------------------------------------------------------------------------|
|validation|Usually the result of calling `.decode` with an io-ts codec. |
|typeAlias |io-ts type names can be verbose. If the type you're using doesn't have a name,<br />you can use this to keep error messages shorter.|
<!-- codegen:end -->
<!-- codegen:start {preset: markdownFromJsdoc, source: src/combinators.ts, export: regexp} -->
#### [regexp](https://github.com/mmkal/ts/tree/4578555/packages/io-ts-extra/src/combinators.ts#L103)
A type which validates its input as a string, then decodes with `String.prototype.match`, succeeding with the RegExpMatchArray result if a match is found, and failing if no match is found.
##### Example
```typescript
const AllCaps = regexp(/\b([A-Z]+)\b/)
AllCaps.decode('HELLO') // right([ 'HELLO', index: 0, input: 'HELLO' ])
AllCaps.decode('hello') // left(...)
AllCaps.decode(123) // left(...)
```
<!-- codegen:end -->
<!-- codegen:start {preset: markdownFromJsdoc, source: src/combinators.ts, export: instanceOf} -->
#### [instanceOf](https://github.com/mmkal/ts/tree/4578555/packages/io-ts-extra/src/combinators.ts#L85)
Validates that a value is an instance of a class using the `instanceof` operator
##### Example
```typescript
const DateType = instanceOf(Date)
DateType.is(new Date()) // right(Date(...))
DateType.is('abc') // left(...)
```
<!-- codegen:end -->