npm.io
0.3.0 • Published 2d ago

@ts-fns/stdlib

Licence
MIT
Version
0.3.0
Deps
1
Size
1.1 MB
Vulns
0
Weekly
15

@ts-fns/stdlib

Core concepts

  • Functional Programming style API
  • Stand-alone functions
  • Fully type-safe
  • Immutability, with appropriate exceptions
  • Normalized return types to Error | T unions
  • pipe/compose work like ADT map/flatMap under the hood
  • Interoperable with other FP focused libraries!

Elixir style everything, for consistent, clean, and type-safe code

Usage

Very much a Work in progress

Still working on the standard alias

import * as Arr from '@ts-fns/stdlib/array';
import * as Func from '@ts-fns/stdlib/function';
import * as Guard from '@ts-fns/stdlib/guard';
import * as Iter from '@ts-fns/stdlib/iterator';
import * as Lens from '@ts-fns/stdlib/lens';
import * as Map from '@ts-fns/stdlib/map';
import * as Num from '@ts-fns/stdlib/number';
import * as Obj from '@ts-fns/stdlib/object';
import * as Ord from '@ts-fns/stdlib/order';
import * as Set from '@ts-fns/stdlib/set';
import * as Str from '@ts-fns/stdlib/string';
import * as Tuple from '@ts-fns/stdlib/tuple';

Details

Normalized return types to Error | T unions

Javascript has no normalized "Empty" value that gets returned from methods. -1? undefined? null?. In addition, while exceptions and try/catch blocks are the built-in mechanisms for Errors, sometimes those errors are represented as values. For example, Number.parseInt() returns NaN instead of throwing.

Errors-as-values in the form of unions are the most idiomatic way to continue to use javascript without introducing new paradigms such as Result. Control-flow logic doesn't change in most cases, you just change your condition parameters

/* vanilla js */
const user = users.find(u => u.id === id);
//    ^? User | undefined

if (user === undefined) {
  // handle "NotFound" case
}

/* @ts-fns/stdlib */
import * as Arr from '@ts-fns/stdlib/array';

const user = Arr.find(users, u => u.id === id);
//    ^? User | NotFoundError

if (user instanceof NotFoundError) {
  // handle "NotFound" case
}

While functions that throw get changed from try-catch blocks to other control-flow mechanics, this is probably for the better, as it aligns into a single paradigm flow

/* vanilla js */
let updatedUsers;
try {
  updatedUsers = users.with(5, updatedUser);
} catch(e) {
  // handle error
}

/* @ts-fns/stdlib */
import * as Arr from '@ts-fns/stdlib/array';

const updatedUsers = Arr.insert(users, 5, updatedUser)

if (updatedUsers instanceof RangeError) {
  // handle error
}

Errors are no longer opaque, and compliment Typescript's type system. Yielding more type-safety and less error prone code.

Two Type of Errors

This repo subscribes to Effect's philosophy that there are Expected and Unexpected Errors (read about it here: https://effect.website/docs/guide/effects/errors).

Just like Effect does not track Unexpected Errors, neither does @ts-fns/stdlib. In particular, this library only captures expected errors, returning them as values. Unexpected errors are thrown as normal.

Expected Errors

Some examples

  • Array.prototype.with() throws a RangeError if the index is out of bounds. Arr.update() returns RangeError as a value for the same reason.
  • Different value are used to represent "not found". Arr.indexOf() returns -1. Arr.find() returns undefined. Str.match() returns null. @ts-fns/stdlib normalizes these by returning NotFoundError as a value
  • Array function that would normally return when called on an empty array will return EmptyArrayError instead.

All functions that catch, or add, expected Errors will return that errors as a value, typed as part of that function's return type union.

Type narrowing to remove errors from return type

Arr.first() returns EmptyArrayError | T. The guard function Arr.isNotEmpty() validates at runtime that an array is not empty and narrow an array T[] to NonEmptyArray<T>. Arr.first() is typed to accept ReadonlyNonEmptyArray<T> and will return only T for that case. This allows you to remove errors from the return type by validating at runtime that the error condition cannot exist.

import * as Arr from '@ts-fns/stdlib/array';

const arr: number[] = [];

const first = Arr.first(arr);
//    ^? EmptyArrayError | number

if (Arr.isNotEmpty(arr)) {
  const first = Arr.first(arr);
  //    ^? number
}
Unexpected Errors

Str.startsWith() is a good example. Javascript's String.prototype.startsWith() throws an TypeError if the search value is a regex. Though defined, this error is considered unexpected because it would be illogical to use a regex. Javascript coerces other types to strings, Typescript defines string as the only valid argument type.

Any function that does this will have @throws in their JSDoc comment to indicate this possibility.

TODO

Go over:

  • .assert() to remove errors from the return types but with the risk of throwing
  • gen() function to utilize generator functions to write procedural code and aggregate error handle to the result
  • pipe/compose act like ADT .map()/.flatMap() under-the-hood, allowing you to string together functions that throw error, but defer those errors to the result

Keywords