Codec
This module allows you to create a boundary on the outermost layer of your application, usually where you process back-end data or communicate with a third-party API. A codec consists of two parts - an encoder and a decoder, hence the name. Using a decoder you can validate your expectations regarding the structure and type of data you're receiving. An encoder, on the other hand, lets you make sure you're sending your application data in the correct format and can also act as a mapper from your custom domain objects to plain JSON values. In case your infrastructure supports JSON schema you can also generate one from your codecs so that you don't have to deal with the error handling.
import { Codec, GetType, string, number ... } from 'purify-ts/Codec'
interface<T extends Record<string, Codec<any>>>(properties: T): Codec<{[k in keyof T]: GetType<T[k]>}>Creates a codec for any JSON object.
Codec.interface({
username: string,
age: number,
email: optional(string),
followers: array(number)
})
Codec<{
username: string
age: number
email?: string
followers: Array<number>
}>
custom<T>(config: { decode: (value: unknown) => Either<string, T>, encode: (value: T) => any}): Codec<T>Creates a codec for any type, you can add your own deserialization/validation logic in the decode argument.
// ↓↓↓ It's important to specify the type argument
Codec.custom<string>({
decode: input => (typeof input === 'string' ? Right(input) : Left('fail')),
encode: input => input // strings have no serialization logic
})
GetTypeGetType<T extends Codec<any>> = T extends Codec<infer U> ? U : neverYou can use this to get a free type from any codec.
const User = Codec.interface({
username: string,
age: number
})
type User = GetType<typeof User>
// type User will equal {username: string; age: number}
FromTypeFromType<T> = {[P in keyof Required<T>]: Pick<T, P> extends Required<Pick<T, P>> ? T[P] : T[P] | undefined}Special type used when you want to do the opposite of `GetType` - define a Codec for an existing type. The problem is that due to technical limitations optional properties are hard to generate in TypeScript so Codec generates properties of type "T | undefined" instead, which is not compatible.
type A = { a?: number }
const A: Codec<A> = Codec.interface({ a: optional(number) })
type A = { a?: number }
const A: Codec<FromType<A>> = Codec.interface({ a: optional(number) })
// Type 'Codec<{ a: number | undefined; }>' is not assignable to type 'Codec<A>'
Success!
decode(input: unknown) => Either<string, T>Takes a JSON value (i.e. a plain JS object) and runs the decode function the codec was constructed with. All of purify's built-in codecs return a descriptive error message in case the decode fails.
encode(input: T) => unknownTakes a runtime value and turns it into a JSON value using the encode function the codec was constructed with. Most of purify's built-in codecs have no custom encode method and they just return the same value, but you could add custom serialization logic for your custom codecs.
unsafeDecode(input: unknown) => TThe same as the decode method, but throws an exception on failure. Please only use as an escape hatch.
schema() => JSONSchema6Codec.interface({
username: string,
age: number,
email: optional(optional(oneOf([oneOf[string]]))),
followers: array(number)
}).schema()
{
type: 'object',
properties: {
username: { type: 'string' },
age: { type: 'number' },
email: { type: 'string' }, // Schema is optimized
followers: { type: 'array', items: [ { type: 'number' } ] }
},
required: [ 'username', 'age', 'followers' ]
}
stringCodec<string>A codec for any string value. Most of the time you will use it to implement an interface codec (see the Codec.interface example above). Encoding a string acts like the identity function.
string.decode('purify-ts')
string.decode(3.14)
Right('purify-ts')
Left('Expected a string, but received a number with value 3.14')
numberCodec<number>A codec for any number value. This includes anything that has a typeof number - NaN, Infinity etc. Encoding a number acts like the identity function.
number.decode(4.20)
number.decode(null)
Right(4.20)
Left('Expected a number, but received null')
booleanCodec<boolean>A codec for a boolean value.
boolean.decode(true)
boolean.decode(0)
Right(true)
Left('Expected a boolean, but received a number with value 0')
nullTypeCodec<null>A codec for null only.
unknownCodec<unknown>A codec that can never fail, but of course you get no type information. Encoding an unknown acts like the identity function.
unknown.decode(0)
unknown.decode({someObject: true})
unknown.decode(false)
Right(0)
Right({someObject: true})
Right(false)
dateCodec<Date>A codec for a parsable date string, on successful decoding it resolves to a Date object. The validity of the date string during decoding is decided by the browser implementation of Date.parse. Encode runs toISOString on the passed in date object.
date.decode('2019-12-15T20:34:25.052Z')
date.encode(new Date(2019, 2, 13))
Right(new Date('2019-12-15T20:34:25.052Z'))
'2019-03-12T22:00:00.000Z'
oneOf<T extends Array<Codec<any>>>(codecs: T): Codec<GetType<T extends Array<infer U> ? U : never>>A codec combinator that receives a list of codecs and runs them one after another during decode and resolves to whichever returns Right or to Left if all fail. Keep in mind that encoding probably won't work correctly if you use a custom codec and it's not lawful (as in, decode(encode(X)) is not equal to X)
const nullable = <T>(codec: Codec<T>): Codec<T | null> =>
oneOf([codec, nullType])
oneOf([string, nullType]).decode('Well, hi!')
oneOf([string, nullType]).decode(null)
Codec<T | null>
Right('Well, hi!')
Right(null)
optional<T>(codec: Codec<T>): Codec<T | undefined>A codec for a value T or undefined. Mostly used for optional properties inside an object, hence the name.
optional(number).decode(undefined)
Codec.interface({ a: optional(number) }).decode({})
Right(undefined)
Right({})
nullable<T>(codec: Codec<T>): Codec<T | null>A codec for a value T or null. Keep in mind if you use `nullable` inside `Codec.interface` the property will still be required.
nullable(number).decode(null)
array<T>(codec: Codec<T>): Codec<Array<T>>A codec for an array.
array(number).decode([3.14, 2, 3])
array(oneOf([string, number])).decode(['x', 0, 'y', 1])
Right([3.14, 2, 3])
Right(['x', 0, 'y', 1])
record<K extends keyof any, V>(keyCodec: Codec<K>, valueCodec: Codec<V>): Codec<Record<K, V>>A codec for an object without specific properties, its restrictions are equivalent to the Record<K, V> type so you can only check for number and string keys.
record(string, boolean).decode({valid: true})
record(number, string).decode({0: 'user1', 1: 'user2'})
record(number, string).decode({valid: 'no'})
Right({valid: true}) // but the type is Either<string, Record<string, boolean>>
Right({0: 'user1', 1: 'user2'})
Left('Problem with key type of property "valid": Expected a number, but received a string with value "valid"')
tuple<TS extends [Codec<any>, ...Codec<any>[]]>(codecs: TS): Codec<{[i in keyof TS]: TS[i] extends Codec<infer U> ? U : never}>The same as the array decoder, but accepts a fixed amount of array elements and you can specify each element type, much like the tuple type.
tuple([number]).decode([0, 1])
tuple([number]).decode([''])
Left('Expected an array of length 1, but received an array with length of 2')
Left('Problem with value at index 0: Expected a number, but received a string with value ""')
map<K, V>(keyCodec: Codec<K>, valueCodec: Codec<V>): Codec<Map<K, V>>A codec for the built-in Map type.
map(string, number).decode([['a', 0], ['b', 1]])
map(string, number).encode(new Map(Object.entries({a: 0, b: 1}))
Right(Map(2) {"a" => 0, "b" => 1})
[['a', 0], ['b', 1]]
exactly<T extends (string | number | boolean)[]>(...expectedValues: T): Codec<T[number]>A codec that only succeeds decoding when the value is exactly what you've constructed the codec with.
exactly('').decode('non-empty string')
exactly('None', 'Read', 'Write')
Left('Expected "", but received a string with value "non-empty string"')
Codec<"None" | "Read" | "Write">
enumeration<T extends Record<string, string | number>>(e: T): Codec<T[keyof T]>A codec for a TypeScript enum.
enum Mode { Read, Write, ReadWrite }
enumeration(Mode).decode(0)
lazy<T>(getCodec: () => Codec<T>): Codec<T>A special codec used when dealing with recursive data structures, it allows a codec to be recursively defined by itself.
interface Comment {
content: string,
responses: Comment[]
}
const Comment: Codec<Comment> = Codec.interface({
content: string,
responses: lazy(() => array(Comment))
})
intersect<T, U>(t: Codec<T>, u: Codec<U>): Codec<T & U>Creates an intersection between two codecs. If the provided codecs are not for an object, the second decode result will be returned.
intersect(
Codec.interface({a: number}),
Codec.interface({b: string})
).decode({a: 5, b: ''})
maybe<T>(codec: Codec<T>): Codec<Maybe<T>>A codec for purify's Maybe type. Encode runs Maybe#toJSON, which effectively returns the value inside if it's a Just or undefined if it's Nothing.
maybe(number).decode(undefined)
maybe(number).decode(null)
maybe(number).decode(123)
maybe(number).encode(Just(0))
Right(Nothing) // Also works with missing properties inside an object
Right(Nothing)
Right(Just(123))
0
nonEmptyList<T>(codec: Codec<T>): Codec<NonEmptyList<T>>A codec for purify's NEL type.
nonEmptyList(number).decode([])
nonEmptyList(number).decode([0])
Left('Expected an array with one or more elements, but received an empty array')
Right(NonEmptyList([0]))
parseError(error: string): DecodeErrorTurns a string error message produced by a built-in purify codec into a meta object.
parseError(`
Problem with key type of property "a":
Expected a number, but received a string with value "a"
`)
{
type: 'property',
property: 'a',
error: {
type: 'failure',
expectedType: 'number',
receivedType: 'string',
receivedValue: 'a'
}
}
DecodeErrorDecodeError =
{ type: 'property'; property: string; error: DecodeError }
| { type: 'index'; index: number; error: DecodeError }
| { type: 'oneOf'; errors: DecodeError[] }
| {
type: 'failure'
expectedType?: ExpectedType
receivedType: ReceivedType
receivedValue?: unknown
}
| { type: 'custom'; message: string }An ADT representing all possible decode errors