io-ts-extra
v0.11.6
Published
Adds pattern matching, optional properties, and several other helpers and types, to io-ts.
Downloads
48,614
Maintainers
Readme
io-ts-extra
Adds pattern matching, optional properties, and several other helpers and types, to io-ts.
Features
- Pattern matching
- Optional properties
- Advanced refinement types
- Regex types
- Parser helpers
Contents
Motivation
Comparison with 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
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.
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
match
Match an object against a number of cases. Loosely based on Scala's pattern matching.
Example
// 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
// 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
// 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
// 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|
matcher
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
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
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);
Shorthand
The "shorthand" format for type specifications maps to io-ts types as follows:
codecFromShorthand
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|
|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|
Codecs/Combinators
sparseType
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
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({...})])
optional
unions the passed-in type with null
and undefined
.
mapper
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
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 |
parser
A helper for parsing strings into other types. A wrapper around mapper
where the from
type is t.string
.
Example
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|
strict
Like t.type
, but fails when any properties not specified in props
are defined.
Example
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.
narrow
Like io-ts's refinement type but:
- Not deprecated (see https://github.com/gcanti/io-ts/issues/373)
- Passes in
Context
to the predicate argument, so you can check parent key names etc. - Optionally allows returning another io-ts codec instead of a boolean for better error messages.
Example
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"
validationErrors
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,you can use this to keep error messages shorter.|
regexp
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
const AllCaps = regexp(/\b([A-Z]+)\b/)
AllCaps.decode('HELLO') // right([ 'HELLO', index: 0, input: 'HELLO' ])
AllCaps.decode('hello') // left(...)
AllCaps.decode(123) // left(...)
instanceOf
Validates that a value is an instance of a class using the instanceof
operator
Example
const DateType = instanceOf(Date)
DateType.is(new Date()) // right(Date(...))
DateType.is('abc') // left(...)