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 🙏

© 2026 – Pkg Stats / Ryan Hefner

be-good

v0.3.0

Published

Simple and flexible data decoding

Readme

Simple and Flexible Data Decoding

Work in progress. Full docs pending. API breakage not out of question.

What it is

This is a set of building blocks for type-safe JSON decoders, written in and for TypeScript. JSON decoders are well-known in languages like Elm or ReasonML, and their goal is, basically, to validate external data and guarantee it really is what the types say it is, so you can safely rely on your types.

It’s functional in the right places, it’s imperative-friendly, it’s flexible—it is, dare I say, pragmatic.

Installation

yarn add be-good

Or

npm i -S be-good

Usage

Basics: be

The basest building block is the be fabric:

import isString from 'lodash/isString'
import { be } from 'be-good'

const beString = be(isString)

be takes a user-defined type guard. E.g., lodash’s isString is typed as (value: any): value is string, so TypeScript infers the beString return type as string.

The functions that be returns (like beSting) are called decoders. A decoder is a function that takes an unknown value and returns a value of a proven type.

But, it is possible that a decoder cannot return a value of the necessary type, e.g., the input is invalid. In those cases, a decoder throws. Therefore, you might want to wrap all of your decoder invocations in try/catch, or...

Or: catching decorators

The second base function is or: a factory for catching decorators. Sounds complicated, but it’s actually quite simple:

import { be, or } from 'be-good'

const optional = or(undefined) // a catching decorator
const beString = be(isString) // + a decoder
const beOptionalString = optional(beString) // = a decorated decoder

beOptionalString('Catchers in the Rye') // 'Catchers in the Rye`
beOptionalString(-1) // undefined

To describe what happens above:

  • you apply or to a value (here, undefined) and get back a decorator
  • you apply the decorator to the decoder and get back a new decoder
  • if the new decoder is given a valid input value (here, a string), it returns that value
  • otherwise, it returns a fallback (undefined)

Obviously, the return type of beOptionalString here is not just string, but string | undefined. On the other hand, nothing stops you from using a fallback of the same type as the expected value:

const alwaysBeNumber = or(0)(be(isNumber))

And sure, you can create one-off decorators on the fly. On the other hand, you may want to keep some of them (like the optional above) reusable across your app.

Decoding objects

There’s a pretty low-level decoder called beObject that simply asserts that the value is indeed an object. It’s useful if you’re doing some non-standard stuff, like transforming your data instead of simply decoding—we’ll cover those scenarios later.

For the most scenarios, there’s a more convenient decoder: beObjectOf.

import { be, beObjectOf, or } from 'be-good'
import { isBoolean, isNumber, isString } from 'lodash'

const beBoolean = be(isBoolean)
const beNumber = be(isNumber)
const beString = be(isString)
const orNull = or(null)

type Mercenary = {
  name: string
  fee: number
  hasGun: boolean
  willTravel: boolean
}

const mercenaryDecoder = orNull(
  beObjectOf<Mercenary>({
    name: beString,
    fee: beNumber,
    hasGun: beBoolean,
    willTravel: beBoolean
  })
)

Never mind the silliness a mercenary without a gun that won’t travel (must be real good at sitting by the river waiting for those bodies), here’s how the decoder works.

mercenaryDecoder({ name: 'Al', fee: 100, hasGun: true, willTravel: true })
// input is an object, has all the fields, hence the decoder returns a Mercenary

mercenaryDecoder({
  name: 'Will',
  fee: 50_000_000,
  hasGun: true,
  willTravel: 'No, Will Smith'
})
// is object, right properties, wrong type, => null

mercenaryDecoder({
  name: 'Sara',
  hasGun: true,
  willTravel: true
})
// is object, missing fields, => null

mercenaryDecoder('I’m a mercenary, honest!')
// duh, not even an object => null

mercenaryDecoder({
  name: 'Spike',
  middleName: 'Danger',
  anotherMiddleName: 'Caution',
  surName: 'DeGuano',
  maidenName: 'Von Ulyanov',
  fee: 20,
  hasGun: false,
  willTravel: true
})
// extra properties don’t matter => Mercenary

A note on generics and type inference

Actually, you don’t have to write beObjectOf<Mercenary>. The decoder return type will be inferred from the property decoders you gave to beObjectOf: e.g. beObjectOf({ a: beString }) will have type (x: unknown) => { a : string }. And since TypeScript types are structural, it doesn’t matter how the type is called as long as the shape is right.

Then again, if you make a mistake in a property name or decoder you give to beObjectOf, TypeScript will fail—somewhere—and the error message might point to a place far from where the actual error is, and you’ll spend more time fixing it. Better specify the expected type right inside a decoder (like above), or maybe right outside of it, like this:

import { beOjbectOf, Decoder } from 'be-good'
// ...
const objDecoder: Decoder<Type> = optional(beObjectOf(/* ... */))

Fail early, I say.

Collections: beArrayOf & beDictOf

beArrayOf and beDictOf are similar to beObjectOf, but their parameters are a bit different. First, they take a single element decoder—meaning all the elements are supposed to be of the same type. Second, the fabric has some other options:

type BeCollectionOptions = {
  /** What to invalidate on errors */
  invalidate?: 'single' | 'all'
  /** Minimum count of (valid) collection elements */
  minSize?: number
}

Some examples:

const beNumber = be(isNumber)

beArrayOf(beNumber)([3, 25.4, false, -7])
// [3, 25.4, -7], because by default, `invalidate` option is 'singe',
// and that means simply omitting invalid elements

beArrayOf(beNumber, { invalidate: 'all' })([3, 25.4, false, -7])
// throws on the first bad element (use `or(...)`)

const orFallback = or('<fallback>') 
beArrayOf(orFallback(beNumber))([3, 25.4, false, -7])
// [3, 25.4, '<fallback>', -7], compare to the first example

beArrayOf(beNumber, { minSize: 4 })([3, 25.4, false, -7])
// throws: only 3 valid elements

/* beDictOf is about the same: */

beDictOf(beNumber)({ a: 3, b: 25.4, c: false, d: -7 })
// { a: 3, b: 25.4, d: -7 }
// etc...

Custom predicates and custom decoders

The type guard functions from lodash is handy, but what if you want to check for more?

const isEven = (n: unknown): n is number => isNumber(n) && n % 2 === 0;

const beEven = be(isEven) // unknown => number

If you dig opaque types, you can use them too (the enum trick taken from an article by Patrick Bacon).

  enum PriceBrand {}
  type Price = number & PriceBrand
  const isPrice = (n: unknown): n is Price => isNumber(n) && n > 0
  const bePrice = be(isPrice) // unknown => price

You can also write whole custom decoders, be it because you cannot validate fields separately, or because you need to ~~mess with~~ manipulate your data structure:

import { be, beObject, fail, or } from 'be-good'

type Range = {
  min: number;
  max: number;
}

type rangeDecoder = or(null)((input: unknown): Range => {
  // note that the input properties differ from the output ones
  const { start, end } = beObject(input)

  if (!isNumber(start) || !isNumber(end) || end > start) fail('Invalid range')

  return { min: start, max: end }
})

Note how the earlier examples mostly compose functions. As you see here, you don’t have to do it. Sure, here we still used a catching decorator™ (i.e. the result of or(null)), but you can also create variables, fail the decoder imperatively and do all the stuff you can normally can do in JavaScript—even though we do recommend keeping your decoders pure. And sure, you could write this particular decoder in a more functional fashion, but the point is you don’t have too. be-good is not hellbent on forcing a particular programming style.

Another important thing is using fail to... well, fail the decoder. Notice how, like beObject, fail is used inside a function wrapped in a catching decorator. You don’t want unchecked exceptions everywhere. And while on one hand you have to remember about the exceptions, on the other hand you’re not recommended to throw exceptions manually. If you want to fail your decoder, call fail. Right now it doesn’t do much, but it might in the future, so don’t break the abstraction.

Todos

  • [x] beDictOf
  • [ ] proper Readme
  • [ ] decoding sum types (discriminated unions)
  • [ ] more examples