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

decod

v6.1.6

Published

Decode unknown values into well-typed Typescript ones

Downloads

3,790

Readme

decod

Decode unknown values into well-typed Typescript ones.

What

This is heavily inspired by bs-json, a Bucklescript module to decode JSON values and will provide you type-safe with low overhead input validation and typing by using the recently added unknown type.

Why

Typescript is great since you can add lightweight static typing to your node.js or web application. If you follow strict guidelines you should be able to crush runtime exceptions down to 0.

A common problem is that you often work with external services exchanging JSON and you might be tempted to statically cast those arbitrary JSON values from the outer world from any to an interface definition of your choice. Great! But what happens when this input, for whatever reason, differs slightly from the expected payload?

  • The best case scenario would probably be a quick crash, but then it will be painful the read the error stacktrace to pinpoint exactly what went wrong.
  • But in the worst case, and I bet this might have already happened to you, you insert inconsistent data into your database.

How

Leveraging the power of Typescript and the unknown type, it is possible to validate and strongly type any uncertain value, at the same time!

Let say you call a third party REST API

Before

export interface IResult {
  id: string;
  user: IUser;
}

export interface IUser {
  firstName: string;
  comments: string[];
  email: string | null;
  age?: number;
  isCool: boolean;
}

const result: IResult = await get("https://cool.com/api");

Great, you typed your input reading the API doc, but what happens when the API slightly change, or if the developer made an error describing their APIs?

After

import * as decod from "decod";

const userDecoder = decod.props({
  firstName: decod.at("firstName", decod.string),
  comments: decod.at("comments", decod.array(decod.string)),
  email: decod.at("email", decod.nullable(decod.string)),
  age: decod.at("age", decod.optional(decod.number)),
  isCool: decod.at("isCool", decod.bool),
});

const resultDecoder = decod.props({
  id: decod.at("id", decod.string),
  user: decod.at("user", userDecoder),
});

export type TResult = ReturnType<typeof resultDecoder>;

const result = resultDecoder(await get("https://cool.com/api"));

API

Primitive decoders

The most basic blocks of decod are the primitive decoders. These just basically assert that the type of the unknown input you provide them is what you expect. If it is, you end up with your initial input value except now, as far as the typescript compiler is concerned, it's well-typed and not unknown anymore. If it fails though, it will throw a ScalarDecoderError (or a StrictDecoderError for decod.is) with nice information on the type (or value) you expected and the actual value you tried to decode.

  • decod.number
  • decod.string
  • decod.boolean
  • decod.null_
  • decod.undefined_
  • decod.date
  • decod.is

Among those, decod.is is kinda special in that it checks that the input value not only matches the type you expect but also its actual value. Not that in order to do that, it only accepts values of primitive types (string, number, boolean, null or undefined) otherwise it would need to perform deep equality in case of complex objects or arrays.

It's most often used in conjunction with decod.oneOf to decode string enums for example:

type Droid = "r2d2" | "c3po";
const droidDecoder = decod.oneOf(decod.is("r2d2"), decod.is("c3po"));

Be careful, although in this case droidDecoder will fail if its input is anything other than r2d2 or c3po, typescript won't consider the result to be as strictly typed as you might want. If you want the typescript compiler to infer that droidDecoder should have the type Decoder<Droid> instead of simply Decoder<string>, you have to either explicitly write that declaration or mark the arguments to decode.is with as const.

// Explicit typing
const droidDecoder: Decoder<Droid> = decod.oneOf(
  decod.is("r2d2"),
  decod.is("c3po"),
);

// `as const` declarations
const droidDecoder = decod.oneOf(
  decod.is("r2d2" as const),
  decod.is("c3po" as const),
);

Decoder combinators

Decoding primitive values is nice and all but it's pretty rare to want to decode some JSON that's just one scalar value. Thankfully, decod provides some nice combinators that allows you to build more complex decoders. We'll go into some more details about each of those.

decod.oneOf

We've already seen an example of decod.oneOf. It's behaviour is pretty straightforward, it just takes as arguments an arbitrary number of decoders (primitive or compound ones), trys them all in order and stops at the first one that succeeds. If none of them does, it throws a OneOfDecoderError. For example, primitive decoders are strict, meaning they don't allow null or undefined values. decod.oneOf lets us define nullable decoders from primitive ones. In fact, that is exactly how decod.nullable is implemented! There is no magic to it.

const nullable = (decoder: Decoder<T>) => decod.oneOf(decoder, decod.null_);
const nullableString = nullable(decod.string);

decod.nullable and decod.optional

Just like we saw, decod already provides for you decod.nullable that will transform any Decoder<T> into a Decoder<T | null>. It also provides decod.optional that transforms a Decoder<T> into a Decoder<T | null | undefined>.

decod.array

Another useful decoder transformer is decod.array that will transform a Decoder<T> into a Decoder<Array<T>>.

decod.attempt and decod.try_

Sometimes, you just want to try to decode something, but if it fails for some reason, have it recover with a default value. That's what decod.try_ and decod.attempt do (they really are just synonyms of each other). They accept any decoder for a type T alongside an optional default value of type T. If the decoder fails the input will decode to either undefined (if no default value is provided) or the provided default value instead of throwing.

const lenientStringDecoder = decod.attempt(decod.string);
const lenientStringDecoderWithDefault = decod.try_(decod.string, "");

decod.assoc

When you want to decode some JSON with dynamically generated keys, you might not know in advance which of those keys you're interested in. For those cases, decod.assoc takes two decoders, one for the keys (Decoder<K>) and one for the values (Decoder<V>) and returns a structure containing those key/value pairs Decoder<Array<{ key: K, value: V }>>.

const dynamicJSON = `{
  "key1": 42,
  "key2": 1337,
  "key128": 0
}`;

const kvDecoder = decod.assoc(decod.string, decod.number);
const kvs = kvDecoder(JSON.parse(dynamicJSON));
// => kvs will have value:
//    [
//      { key: 'key1', value: 42 },
//      { key: 'key2', value: 1337 },
//      { key: 'key128', value: 0 }
//    ]

decod.at

JSON is an inherently hierarchical data format. Most of the time, you'll want to decode a specific field into some well-typed value. And sometimes, that field will be arbitrarily nested in the hierarchy. That's precisely what decod.at will help you with. It can take various kind of arguments:

  • a string in case you want to access a top level field
  • a number for when you want to index a specific value from a JSON array
  • an Array<string | number> when you search for a deeply nested field
const someJSON = `{
  "movie": "Star Wars",
  "director": {
    "first_name": "George",
    "last_name": "Lucas"
  },
  "droids": [
    "r2d2",
    "c3po"
  ]
}`;

// Top level field
const movieDecoder = decod.at("movie", decod.string);

// Nested Field
const directorLastNameDecoder = decod.at(
  ["director", "last_name"],
  decod.string,
);

// Array index
const r2d2Decoder = decod.at(["droids", 0], decod.is("r2d2" as const));

const movie = movieDecoder(JSON.parse(someJSON));
const directorLastName = directorLastNameDecoder(JSON.parse(someJSON));
const r2d2 = r2d2Decoder(JSON.parse(someJSON));

decod.props

This is, hands down, the most useful combinator of all. That's why it is the one showcased in the overview at the top of this documentation. decod.props will let you declare the structure you want to decode into, associating each field with the decoder for that field.

Let's say you want to decode the JSON structure shown above into the following interface:

type Droid = "r2d2" | "c3po";

interface StarWars {
  movie: string;
  director: string;
  droids?: Array<Droid>;
}

This is how you would do it, using decod.props:

const droidDecoder = decod.oneOf(
  decod.is("r2d2" as const),
  decod.is("c3po" as const),
);

const directorDecoder = (input: unknown) => {
  const firstName = decod.at("first_name", decod.string);
  const lastName = decod.at("last_name", decod.string);

  return `${firstName} ${lastName}`;
};

const starWarsDecoder = decod.props({
  movie: decod.at("movie", decod.string),
  director: decod.at("director", directorDecoder),
  droids: decod.at("droid", decod.array(droidDecoder)),
});

See what we did here? We even created our own custom compound decoder in directorDecoder! Remember that a Decoder<T> is really just an alias for a function type (input: unknown) => T. As long as you respect that contract, you can use any of your own functions from unknown to any arbitrary T as decoders in your combinators.

Please note that this is just a toy example. In a real life application, you would want to catch any exception in your custom decoder to either recover from it or throw a more meaningfull error that will help you identify failures down the line.