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.
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 an object against a number of cases. Loosely based on Scala's pattern matching.
Example
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
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
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
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 |
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:
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
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),
})
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({...})])
unions the passed-in type with null
and undefined
.
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])
StringsFromMixedArray.decode('not an array')
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 |
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')
IntFromString.decode('123.4')
IntFromString.decode('not a number')
IntFromString.decode(123)
Params
name | description |
---|
type | the target type |
decode | transform a string into the target type |
encode | transform the target type back into a string |
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.
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'},
})
const invalid = CloudResources.decode({
database: {username: 'user', password: 'pass'},
service: {dbConnectionString: 'user:wrongpassword'},
})
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. |
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')
AllCaps.decode('hello')
AllCaps.decode(123)
Validates that a value is an instance of a class using the instanceof
operator
Example
const DateType = instanceOf(Date)
DateType.is(new Date())
DateType.is('abc')