npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@ts-std/codec

v0.3.0

Published

A convenient and highly type-safe typescript decoder library.

Downloads

23

Readme

@ts-std/codec

A convenient and highly type-safe typescript decoder library.

This library makes it simple to create decoders/validators for unknown input. Various decoders and combinators are exposed, that allow you to construct arbitrarily complex types.

import * as c from '@ts-std/codec'
import { Result, Ok, Err } from '@/ts-std/monads'

const Person = c.object('Person', {
  name: c.string,
  hobbies: c.array(c.object('Hobby', {
    name: c.string,
    years: c.optional(c.number)
  }))
})
type Person = c.TypeOf<typeof Person>

const ok: Result<Person> = Person.decode({
  name: 'Alice',
  hobbies: [{
    name: 'Hiking',
    years: 3,
  }, {
    name: 'Piano',
  }]
})
ok === Ok(...)

const err: Result<Person> = Person.decode({})
err === Err("expected object Person, got {}")

This library is most useful when decoding unknown things from the outside world, such as files, environment variables, or incoming http request bodies.

const password: string =
  Result.from_nillable(process.env.CONFIG_JSON, 'CONFIG_JSON is unset')
  .try_change(env => Result.attempt(() => JSON.parse(env)))
  .change_err(e => e.message)
  .try_change((json: unknown) => c.string.decode(json))
  .expect('invalid CONFIG_JSON')

Common Types

abstract class Decoder<T>

This abstract class defines the interface for all decoders.

abstract class Decoder<T> {
  abstract readonly name: string
  abstract decode(input: unknown): Result<T>

  // this has a default implementation
  guard(input: unknown): input is T
}

type TypeOf<D extends Decoder<unknown>> = D extends Decoder<infer T> ? T : never

Extracts the type of the decoder. Useful when you would like to construct a decoder first, and use the type it defines.

type A = c.TypeOf<typeof c.string> // === string
const NumberOrBoolean = c.union(c.number, c.boolean)
type NumberOrBoolean = c.TypeOf<typeof NumberOrBoolean> // === number | boolean
const = ''

Static Decoders

string: Decoder<string>

Decodes strings.

c.string.decode('a') === Ok('a')

boolean: Decoder<boolean>

Decodes booleans.

c.string.decode(true) === Ok(true)

number: Decoder<number>

Decodes numbers. Doesn't allow any form of NaN or Infinity.

c.string.decode(1.1) === Ok(1.1)

loose_number: Decoder<number>

Decodes numbers. Does allow any form of NaN or Infinity.

c.string.decode(NaN) === Ok(NaN)

int: Decoder<number>

Decodes numbers if they have no decimal component.

c.int.decode(-1) === Ok(-1)

uint: Decoder<number>

Decodes numbers if they have no decimal component and are positive.

c.int.decode(1) === Ok(1)

undefined_literal: Decoder<undefined>

Decodes undefined.

c.string.decode(undefined) === Ok(undefined)

null_literal: Decoder<null>

Decodes null.

c.string.decode(null) === Ok(null)

Decoder Combinators

wrap<T>(name: string, decoder_func: (input: unknown) => Result<T>): Decoder<T>

The most general combinator. Takes a function that converts from unknown to Result<T>.

const OnlyEven = c.wrap('OnlyEven', input => {
  return c.number.decode(input)
    .try_change(n => n % 2 === 0 ? Ok(n) : Err('number must be even'))
})

array<T>(decoder: Decoder<T>): Decoder<T[]>

Creates an array decoder from an internal decoder.

const NumberArray = c.array(c.number)

dictionary<T>(decoder: Decoder<T>): Decoder<Dict<T>>

Creates a decoder of { [key: string]: T } from an internal decoder.

const NumberDict = c.dict(c.number)

tuple<L extends unknown[]>(...decoders: DecoderTuple<L>): Decoder<L>

Creates a tuple decoder from some set of internal decoders.

const StrNumBool = c.tuple(c.string, c.number, c.boolean)
StrNumBool.decode(['a', 1, true]) === Ok(...)

object<O>(name: string, decoders: DecoderObject<O>): Decoder<O>

Creates a decoder specified by the shape of the incoming object. Doesn't allow extra keys.

const Person = c.object('Person', { name: c.string, height: c.number })
Person.decode({ name: 'Alice', height: 6 }) === Ok(...)
Person.decode({ name: 'Alice', height: 6, weight: 120 }) === Err("...")

loose_object<O>(name: string, decoders: DecoderObject<O>): Decoder<O>

Creates a decoder specified by the shape of the incoming object. Does allow extra keys, but both the output type and the output value won't include them

const Person = c.object('Person', { name: c.string, height: c.number })
Person.decode({ name: 'Alice', height: 6 }) === Ok(...)

const had_extra = Person.decode({ name: 'Alice', height: 6, weight: 120 }).expect("")
// won't compile
had_extra.weight

union(...decoders: DecoderTuple): Decoder<T | U | ...>

Creates a decoder for the union type of all input decoders.

const NumOrBoolOrStr = c.union(c.number, c.boolean, c.string)
// number | boolean | string
type NumOrBoolOrStr = c.TypeOf<typeof NumOrBoolOrStr>
const
NumOrBoolOrStr.decode(1) === Ok(1)
NumOrBoolOrStr.decode(true) === Ok(true)
NumOrBoolOrStr.decode('a') === Ok('a')

literal<V extends Primitives>(value: V): Decoder<V>

Creates a decoder for an exact value. Must be string | boolean | number | null | undefined.

const OnlyOne = c.literal(1)
// 1
type OnlyOne = c.TypeOf<typeof OnlyOne>
const ok = OnlyOne.decode(1)
const err = OnlyOne.decode(2)

literals<V extends Primitives>(...values: V[]): Decoder<V[0] | V[1] | ...>

Creates a decoder for the union of several exact values. Must all be string | boolean | number | null | undefined.

const OneOrAOrTru = c.literals(1, 'a', true)
// 1 | 'a' | true
type OnlyOne = c.TypeOf<typeof OnlyOne>
const ok = OnlyOne.decode(1)
const ok = OnlyOne.decode('a')
const ok = OnlyOne.decode(true)
const err = OnlyOne.decode(2)

optional<T>(decoder: Decoder<T>): Decoder<T | undefined>

Creates a decoder for the optional version of the input decoder.

c.optional(c.number)

nullable<T>(decoder: Decoder<T>): Decoder<T | null>

Creates a decoder for the nullable version of the input decoder.

c.nullable(c.number)

nillable<T>(decoder: Decoder<T>): Decoder<T | null | undefined>

Creates a decoder for the nillable version of the input decoder.

c.nillable(c.number)

maybe<T>(decoder: Decoder<T>): Decoder<Maybe<T>>

Creates a decoder that can adapt T | null | undefined to Maybe<T>. This is mostly useful when nesting this decoder within other structures.

import { Maybe, Some, None } from '@ts-std/monads'
const MaybeNumber = c.maybe(c.number)
MaybeNumber.decode(1) === Ok(Some(1))
MaybeNumber.decode(null) === Ok(None)
MaybeNumber.decode(undefined) === Ok(None)

MaybeNumber.decode('a') === Err(...)

const Person = c.object({
  name: c.string,
  height: c.number,
  weight: MaybeNumber,
})

const ok = Person.decode({
  name: 'Alice',
  height: 2,
  weight: null,
})
ok === Ok({
  name: 'Alice',
  height: 2,
  weight: None,
})

If you find yourself in a situation where you'd like to decode a simple value to a Maybe, instead of trying to flatten or extract the maybe from the result, just decode and use the ok_maybe method of Result, which converts Ok to Some and Err to None.

c.number
  .decode(process.env.CONFIG_NUMBER)
  .ok_maybe()
  .match({
    some: n => console.log('Yay got a valid number!'),
    none: () => console.error('Boo number was invalid or not present!'),
  })

Serializable Classes

All the decoders here are for "static" types, or things that simply describe their shape. What happens when you want a custom class to be decodable?

One way is to just have your class extend Decoder:

class A { constructor(readonly name: string, height: ) }

However, with the Codec interface and the cls combinator, you can easily produce a class that is easy to encode and decode using the normal constructor for your class.

class A implements c.Codec {
  constructor(readonly x: number, readonly y: string) {}
  static decode = c.tuple(c.number, c.string)
  encode() {
    return t(this.x, this.y)
  }

  static decoder: c.Decoder<A> = c.cls(A)
}

const original = new A(1, 2)

const json = JSON.stringify(original.encode())
const decoded =
  Result.attempt(() => JSON.parse(json))
  .try_change(json => A.decoder.decode(json))

decoded === Ok(original)

cls<T extends Codec>(cn: CodecConstructor<T>): Decoder<T>

Creates a decoder from a class that implements Codec.

interface Codec

interface Codec<L extends unknown[] = unknown[]> {
  // new (...args: L): T
  static decoder: Decoder<L>
  encode(): L
}

Adaptation/Conversion

Often we don't need input to be in exactly the form we expect, but can work with many different types. These adaptation helpers can create decoders that are lenient and try multiple ways of producing the same thing.

adapt(decoder: Decoder<T>, ...adaptors: AdaptorTuple<T>)

Produce an adapting decoder from a base decoder and some set of adaptors. Adaptors are functions that can convert to our goal of T through some other type U.

Adaptors can be both "safe", so never fail to convert from T to U; or they can be fallible, so they sometimes will fail and produce Result<T> instead.

When creating adaptors, we also have to provide U's base decoder, so we can attempt to go from unknown to U.

const LenientBool = c.adapt(
  c.boolean,
  // we can always get a boolean from a number
  c.adaptor(c.number, n => n === 0),
  // we can sometimes get a boolean from a string
  c.try_adaptor(c.string, s => {
    if (s === 'true') return Ok(true)
    if (s === 'false') return Ok(false)
    return Err("couldn't convert from string to boolean")
  }),
)

LenientBool.decode(true) === Ok(true)
LenientBool.decode(false) === Ok(false)
LenientBool.decode(1) === Ok(true)
LenientBool.decode(0) === Ok(false)
LenientBool.decode('true') === Ok(true)
LenientBool.decode('false') === Ok(false)

LenientBool.decode('whatup') === Err(...)

adaptor<U, T>(decoder: Decoder<U>, func: (input: U) => T): SafeAdaptor<U, T>

Creates an adaptor from U to T that never fails.

c.adaptor(c.number, n => n === 0)

try_adaptor<U, T>(decoder: Decoder<U>, func: (input: U) => Result<T>): FallibleAdaptor<U, T>

Creates an adaptor from U to T that sometimes fails.

c.try_adaptor(c.string, s => {
  if (s === 'true') return Ok(true)
  if (s === 'false') return Ok(false)
  return Err("couldn't convert from string to boolean")
})