@ts-fns/stdlib
v0.2.1
Published
A Functional Programming Standard Library for Typescript
Maintainers
Readme
@ts-fns/stdlib
Core concepts
- Functional Programming style API
- Stand-alone functions
- Fully type-safe
- Immutability, with appropriate exceptions
- Normalized return types to
Error | Tunions - pipe/compose work like ADT
map/flatMapunder 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 aRangeErrorif the index is out of bounds.Arr.update()returnsRangeErroras a value for the same reason.- Different value are used to represent "not found".
Arr.indexOf()returns-1.Arr.find()returnsundefined.Str.match()returnsnull.@ts-fns/stdlibnormalizes these by returningNotFoundErroras a value - Array function that would normally return when called on an empty array will return
EmptyArrayErrorinstead.
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 throwinggen()function to utilize generator functions to write procedural code and aggregate error handle to the resultpipe/composeact like ADT.map()/.flatMap()under-the-hood, allowing you to string together functions that throw error, but defer those errors to the result
