Lightweight TypeScript-first encoding and decoding of complex objects.
A value of type Codec<A, O, I> (called "codec") is the runtime representation of the static type A.
A codec can:
- decode inputs of type
I, - encode values to type
O, - be used as a type predicate.
exportabstractclassCodec<A,O=A,I=unknown>{protectedconstructor(readonlyname: string){}abstractis(input: unknown): input is A;abstractencode(value: A): O;abstractdecode(input: I): Either<Error,A>;}As an example, here is a codec for integer encoded as a string:
// Represents integer `number`, the first type parameter.// If we encode a known number, it will turn into `string` (the second type parameter).// If we want to receive a number, the codec can accept `string` as input to parse (the third type parameter).// To decode `unknown` input do something like `string.pipe(numberAsString)`.classIntAsStringCodecextendsCodec<number,string,string>{constructor(){super(`IntAsString`);}// Similar to `instanceof`.is(input: unknown): input is number{returntypeofinput==="number";}decode(input: string,context: Context): Validation<number>{constsupposedlyInt=parseInt(input,10);// If an integerif(supposedlyInt.toString()===input){// Return value// Beware: do not return plain value, wrap it in `context.success`returncontext.success(supposedlyInt);}else{// If anything is wrong, signal failure by returning `context.failure`.// Whatever happens, **do not throw an error**.returncontext.failure(`Not an integer`);}}// Encode known value to string output.encode(value: number): string{returnvalue.toString();}}constintAsString=newIntAsStringCodec();In most cases though, creating codecs this way is an overkill. Codec combinators provided by the library are enough for 90% of use cases.
The Either type represents a value of one of two possible types (a disjoint union):
Leftmeaning success,Rightmeaning failure.
typeEither<TError,TValue>=|{readonly_tag: "Left";readonlyleft: TError;}|{readonly_tag: "Right";readonlyright: TValue;};You could check a result of validation using isValid or isError helpers:
import{string,refinement,validate,isError}from"codeco";constlongString=refinement(string,(s)=>s.length>=100);constvalidation=validate(longString,"short input");if(isError(validation)){console.log("Validation errorr",validation.left);}constvalid=validation.right;// Here goes proper long string| Description | TypeScript | codec |
|---|---|---|
| null | null | cs.null or cs.nullCodec |
| undefined | undefined | cs.undefined |
| void | void | cs.void |
| string | string | cs.string |
| number | number | cs.number |
| boolean | boolean | cs.boolean |
| BigInt | bigint | cs.bigint |
| unknown | unknown | cs.unknown |
| literal | 's' | cs.literal('s') |
| array of unknown | Array<unknown> | cs.unknownArray |
| dictionary of unknown | Record<string, unknown> | cs.unknownDictionary |
| array of type | Array<A> | cs.array(A) |
| any | any | cs.any |
| never | never | cs.never |
| dictionary | Record<string, A> | cs.dictionary(A) |
| record of type | Record<K, A> | cs.record(K, A) |
| partial | Partial<{ name: string }> | cs.partial({ name: cs.string }) |
| readonly | Readonly<A> | cs.readonly(A) |
| type alias | type T = { name: A } | cs.type({ name: A }) |
| tuple | [A, B] | cs.tuple([ A, B ]) |
| union | A | B | cs.union([ A, B ]) |
| intersection | A & B | cs.intersection([ A, B ]) |
| keyof | keyof M | cs.keyof(M) (only supports string keys) |
| recursive types | cs.recursive(name, definition) | |
| exact types | ✘ | cs.exact(type) (no unknown extra properties) |
| strict | ✘ | cs.strict({ name: A }) (an alias of cs.exact(cs.type({ name: A }))) |
| sparse | ✘ | cs.sparse({ name: A }) similar to cs.intersect(cs.type(), cs.partial() |
| replacement | ✘ | cs.replacement(A, altInput) |
| optional | A | undefined | cs.optional(A) |
In addition to structural encoding/decoding, we provide linear parsing functions in form of Parser Combinators available from 'codeco/linear':
import*asPfrom"codeco/linear";import{getOrThrow}from"codeco";constline=P.seq(P.literal("My name is "),P.match(/\w+/));constname=P.map(line,(parsed)=>parsed[1]);// `map` combinatorconstinput=newP.StringTape("My name is Marvin");// Prepare input for consumptionconstdecodedName=getOrThrow(P.parseAll(input));// Would throw if input does not conform to expected formatProvided combinators:
literal("string-value")- literal valuemap(combinator, mapFn)- map return value ofcombinatorto something else,mapFold(combinator, mapFn)- map return value ofcombinatorto something else asEither, so optionally indicating failure,match(regexp)- likeliteral, but matches a RegExp,seq(combinatorA, combinatorB, ...)- match combinators and return array of their results,join(combinators)- match combinators and their results as a single string,joinSeq(combinators)- shortcut forjoin(seq(combinatros)),option(combinator, value)- try matchingcombinator, returnvalueif the combinator does not match,choice(combinatorA, combinatorB, ...)- match any of the passed combinators,sepBy(combinator, separator, min = 1, max = Infinity)- match sequence of 1 or morecombinators separated byseparator, likeA,A + A,A + A + A, etc.many(combinator, min = 1, max = Infinity)- array of combinators of length[min, max),parseAll(combinator)- make sure all the input is consumed.