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

@ailabs/ts-utils

v1.4.0

Published

Shared TypeScript utilities

Downloads

496

Readme

TypeScript Utility Modules

A collection of TypeScript utility classes and functions for safe, principled data manipulation. Class modules support both instance methods and static (curried) functions.

Prior Art

Motivation

This library is primarily oriented around safely consuming data from arbitrary sources within a TypeScript application. Existing libraries that solve this problem were found to be deficient in the following areas:

  • Extra book-keeping related to having to define both types and decoders: this library intends to be expressive enough to represent any expressable data shape, and allow types to be automatically derived.
  • Insufficient information related to failures: other libraries generate error objects that can only be string-ified for a simple explanation of the failure; this library provides error objects that produce highly detailed descriptions of failures, and structured data that can be inspected, or consumed by other tools.
  • Lack of granular composability: this library allows arbitrary parsing / validation logic to be granularly interwoven with decoders, and provides utilities to adapt existing code into decoder compositions (see: Result.attempt()).

Other modules / types included in this package exist primarly to support safely decoding arbitrary data, but have been augmented with typical features found in other programming environments, to make them nice to use.

Modules

Maybe

Represents a value that may or may not be present. While it provides equivalent functionality to the definition of Maybe found in other programming environments, it doesn't fully map to formal definition. Rather, the design goals are ease of use, and coherence with TypeScript's type checker.

Maybe.of('Hello').map(str => str.toUpperCase()).defaultTo('??') // ==> 'HELLO'
Maybe.of(null).map(str => str.toUpperCase()).defaultTo('??') // ==> '??'

Result

Represents an operation that could succeed or fail, and encapsulates both cases.

Decoder

A module of utility functions for safely consuming untyped data. Provides strong type-safety guarantees that data from APIs or other third-party sources is correct.

Also includes utlities to derive type definitions from decoders, to avoid redundancy and multiple sources of truth. Example:

// Original code

type User = {
  name: string;
  email: string;
  created: number;
  isActive: boolean;
};

Using decoders, this type can be written directly as a function that accepts a value, and returns a Result with a success value of User:

// Step 1

import { boolean, object, string } from '@ailabs/ts-utils/dist/decoder';

const user = object('User', {
  name: string,
  email: string,
  created: number,
  isActive: boolean
});

const valueFromAPI: any = {
  name: 'Will',
  email: '[email protected]',
  created: 1342828800,
  isActive: true
};

user(valueFromAPI) // ==> Result<..., { name: string, ... }>

The final step is to derive the original type from the decoder, so that it can be used elsewhere in the application, the same as a manually-defined type:

// Step 2

import { Decoded, ... } from '@ailabs/ts-utils/dist/decoder';

const user = object('User', {
  // ...
});

// The `User` type is now equivalent to the `User` type in the original example
type User = Decoded<typeof user>;

Decoder also includes utilites for mapping common type definitions, such as union types and enums:

inList: Converts a value to a member of a union type

Given the following union:

type Union = 'one' | 'two' | 'three';

It can be converted to an equivalent type + decoder as follows:

import { inList } from '@ailabs/ts-utils/dist/decoder';

type Union = typeof union[number];

const union = ['one', 'two', 'three'] as const;
const toUnion = inList(union);
toEnum: Converts a value to a member of an enum type
import { toEnum } from '@ailabs/ts-utils/dist/decoder';

enum Status {
  Pending = 'Pending',
  Approved = 'Approved',
  Cancelled = 'Cancelled',
  Declined = 'Declined'
}

const toStatus = toEnum('Status', Status);

toStatus('Pending') // ==> Result<..., Status>
and: Intersects decoders, analogous to TypeScript's & operator
import { and, string, object } from '@ailabs/ts-utils/dist/decoder';

const foo = object('Foo', { foo: string });
const bar = object('Bar', { bar: string });

const fooAndBar = and(foo, bar);
// ==> Decoder<..., { foo: string } & { bar: string }>;

Decoder Composition & Error Handling

The Decoder module strives to facilitate the representation of any valid JSON value, and to handle the challenges of varying or inconsistent formats. The following example demonstrates a Task type, with the following attributes:

  • An optional due field (a Date type)
  • An owner field, which is either a Person type, or a URL to a Person resource

(Note: The pipe() function imported from the Ramda library provides left-to-right function composition).

import { pipe } from 'ramda';
import Result from '@ailabs/ts-utils/dist/result';
import {
  DecodeError, Decoded, object, string, parse,
  number, boolean, nullable, oneOf, array
} from '@ailabs/ts-utils/dist/decoder';

/**
 * Utility function to convert a parseable string to a date, or
 * else fail with an error.
 */
const toDate = (val: string): Result<Error, Date> => (
  isNaN(Date.parse(val))
    ? Result.err(new Error('[Invalid date]'))
    : Result.ok(new Date(val))
);

/**
 * Utility function to validate that a string is a URL. The `URL`
 * constructor will throw an error if the parameter is not a valid URL, so
 * `Result.attempt()` is used trap the error and convert it to a failed
 * `Result` object.
 */
const isUrl = Result.attempt((val: string) => new URL(val) && val);

/* ... */

/**
 * Extract domain model type definitions from decoders
 */
type Person = Decoded<typeof person>;
type Task = Decoded<typeof task>;
type TaskList = Decoded<typeof tasks>;

/**
 * Define the decoder for the `Person` type—this could be inlined
 * inside of `task`, except that the type definition is needed for
 * `oneOf()`, which requires an explicit type parameter of the union
 * of all possible decoder types.
 */
const person = object('Person', {
  name: string,
  email: string,
  avatarUrl: pipe(string, parse(isUrl))
});

const task = object('Task', {
  id: number,
  title: string,
  completed: boolean,
  dueDate: nullable(pipe(string, parse(toDate))),
  owner: oneOf<string | Person, Error>([
    pipe(string, parse(isUrl)),
    person
  ])
});

const tasks = array(task);

const data: any = [{
  id: 1138,
  title: 'Implement to-do example',
  completed: true,
  owner: {
    name: 'Will',
    email: '[email protected]',
    avatarUrl: 'invalid.url'
  }
}];

const taskList: Result<DecodeError<Error>, TaskList> = tasks(data);

console.log(taskList.error()!.toString());

// ==> Decode Error: [TypeError [ERR_INVALID_URL]]: 'Invalid URL: invalid.url' \
//       in path: [0] > \
//       Decoder.object(Task).owner > \
//       Decoder.object(Person).avatarUrl

Decoders produce highly detailed errors, reporting the exact path of invalid data, as well as the value(s) that failed to decode.

The structural information that errors are generated from is also exposed on DecodeError for consumption by other tools.