https://github.com/perfective/ts.common
Common types and functions for perfective development in TypeScript
https://github.com/perfective/ts.common
functional-programming javascript monad typescript
Last synced: 13 days ago
JSON representation
Common types and functions for perfective development in TypeScript
- Host: GitHub
- URL: https://github.com/perfective/ts.common
- Owner: perfective
- License: mit
- Created: 2020-02-01T22:49:40.000Z (about 5 years ago)
- Default Branch: main
- Last Pushed: 2025-03-09T17:07:49.000Z (about 2 months ago)
- Last Synced: 2025-04-12T04:08:22.461Z (13 days ago)
- Topics: functional-programming, javascript, monad, typescript
- Language: TypeScript
- Homepage:
- Size: 8.19 MB
- Stars: 12
- Watchers: 3
- Forks: 1
- Open Issues: 38
-
Metadata Files:
- Readme: README.adoc
- Changelog: CHANGELOG.adoc
- License: LICENSE
- Roadmap: ROADMAP.adoc
Awesome Lists containing this project
README
= Perfective Common for TypeScript
:mdn-js-globals: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects
:perfective-common: https://github.com/perfective/ts.common/tree/main== Installation
[source,bash]
----
npm install @perfective/common
----After the installation you can read the full compiled documentation in the `node_modules/@perfective/common/docs.html`.
== Key Features
The `@perfective/common` package facilitates writing highly readable functional code.
It focuses on providing functions to handle ECMAScript types
and to compose functions together easily.=== Maybe monad
The `link:{perfective-common}/src/maybe/index.adoc[@perfective/common/maybe]` package
provides a Maybe monad implementation.It allows you to write and compose functions that accept only present (defined and non-null) values.
It helps avoid additional complexity and noise when handling `null` and `undefined` values.For example, consider you have the `User` and `Name` types below and want to output a user’s full name.
[source,typescript]
----
interface User {
name?: Name;
}interface Name {
first: string;
last: string;
}
----If you write functions that have to handle `null` and `undefined` values,
then you would have to write something like this:[source,typescript]
----
function userNameOutput(user: User | null | undefined): string {
if (isPresent(user)) {
const name = fullName(user.name);
if (isPresent(name)) {
return name;
}
}
throw new Error('User name is unknown');
}function fullName(name: Name | null | undefined): string | null {
if (isPresent(name)) {
const trimmed = `${name.first} ${name.last}`.trim();
if (isNotEmpty(trimmed)) {
return trimmed;
}
}
return null;
}
----**Using the `link:{perfective-common}/src/maybe/index.adoc[Maybe]` monad,
you can write simpler and more readable functions**:[source,typescript]
----
import { panic } from '@perfective/common/error';
import { just, Maybe, maybe } from '@perfective/common/maybe';
import { isNotEmpty, trim } from '@perfective/common/string';function userNameOutput(user: User | null | undefined): string {
return maybe(user)
.pick('name') // <.>
.onto(fullName) // <.>
.or(panic('User name is unknown')); // <.>
}function fullName(name: Name): Maybe {
return just(`${name.first} ${name.last}`)
.to(trim) // <.>
.that(isNotEmpty); // <.>
}
----
<.> `link:{perfective-common}/src/maybe/index.adoc#maybepick[Maybe.pick()]`
provides a strictly typed "optional chaining" of the `Maybe.value` properties.
<.> `link:{perfective-common}/src/maybe/index.adoc#maybeonto[Maybe.onto()]`
(flat) maps a `Maybe.value` to another `Maybe`.
<.> `{perfective-common}/src/maybe/index.adoc#maybeor[Maybe.or()]`
extracts a `value` from the `Maybe` with a given fallback.
(or allows to throw an error).
<.> `link:{perfective-common}/src/maybe/index.adoc#maybethat[Maybe.that()]`
filters a value inside `Maybe`.
<.> `link:{perfective-common}/src/maybe/index.adoc#maybeto[Maybe.to()]` maps a value inside `Maybe` using a given callback.In addition to these methods,
the `link:{perfective-common}/src/maybe/index.adoc[Maybe]` type also provides:
`link:{perfective-common}/src/maybe/index.adoc#maybeinto[Maybe.into()]`,
`link:{perfective-common}/src/maybe/index.adoc#maybewhich[Maybe.which()]`,
`link:{perfective-common}/src/maybe/index.adoc#maybewhen[Maybe.when()]`,
`link:{perfective-common}/src/maybe/index.adoc#maybeotherwise[Maybe.otherwise()]`,
and `link:{perfective-common}/src/maybe/index.adoc#maybethrough[Maybe.through()]` methods.Read more about the `Maybe` monad and other
`link:{perfective-common}/src/maybe/index.adoc[@perfective/common/maybe]` functions in the
link:{perfective-common}/src/maybe/index.adoc[package documentation].=== Result monad
The `link:{perfective-common}/src/result/index.adoc[@perfective/common/result]` package
provides the `Result` monad implementation
(as a concrete case of the Either monad).
It allows developers to increase the reliability of their code by treating errors as valid part of a function output.A `Result` instance can be either a `Success` or a `Failure`.
If a `Result` is a `Success`, a computation proceeds to the next step.
In case of a `Failure`, all further computations are skipped until the recovery or exit from the computation.The `Result` monad is similar to the `Maybe` monad,
but unlike `Maybe`, a `Result` contains a reason for its `Failure`.The `Result` monad is also similar to the `Promise`
(as a `Promise` can either be "resolved" or "rejected").
But, unlike `Promise`, the `Result` chain is synchronous.For example, consider you have an HTTP endpoint to return user data stored in the database.
The purpose of the endpoint is to map a given (unsafe) user ID input to a `User`..Assume you have the following functions
[source,typescript]
----
export interface User {
// User data
}/** Returns `true` if the active user has admin access. */
declare function hasAdminAccess(): boolean;/** Builds an SQL query to load a user with a given `id`. */
declare function userByIdQuery(id: number): string;/** Sends a given `sql` to the database and returns a User. */
declare function userQueryResult(sql: string): Promise;/** Logs a given error */
declare function logError(error: Error): void;
----If you write regular imperative code you may have something like this:
[source,typescript]
----
/** @throws Error if a given id is invalid. */
function validUserId(id: unknown): number {
if (typeof id !== 'string') {
throw new TypeError('Input must be a string');
}
const userId = decimal(id);
if (userId === null) {
throw new Error('Failed to parse user id');
}
if (!Number.isInteger(userId) || userId <= 0) {
throw new Error('Invalid user id');
}
return userId;
}async function userResponseById(id: unknown): Promise {
try {
return userForQuery(
userByIdQuery(
validUserId(id), // <.>
),
);
}
catch (error: unknown) {
logError(error as Error);
throw error as Error;
}
}
----
<.> Note that `validUserId()` indicates that it throws an error using a JSDoc.
TypeScript compiler does not check that the code should be wrapped into the `try-catch` block.**Using the `Result` monad and functions from the `@perfective/common` subpackages you can write the same code as**:
[source,typescript]
----
import { isNotNull } from '@perfective/common';
import { typeError } from '@perfective/common/error';
import { naught } from '@perfective/common/function';
import { decimal, isNonNegativeInteger } from '@perfective/common/number';
import { rejected } from '@perfective/common/promise';
import { Result, success } from '@perfective/common/result';
import { isString } from '@perfective/common/string';function validUserId(id: unknown): Result {
return success(id)
.which(isString, typeError('Input must be a string')) // <.>
.to(decimal)
.which(isNotNull, 'Failed to parse user ID') // <.>
.that(isNonNegativeInteger, 'Invalid user ID'); // <.>
}async function userResponseById(id: unknown): Promise {
return success(id)
.when(hasAdminAccess, 'Access Denied') // <.>
.onto(validUserId) // <.>
.to(userByIdQuery)
.through(naught, logError) // <.>
.into(userForQuery, rejected); // <.>
}
----
<.> `link:{perfective-common}/src/result/index.adoc#resultwhich[Result.which()]`
applies a type guard and narrows the `Result.value` type.
<.> `decimal()` returns `number | null`, so another type guard is required.
<.> `link:{perfective-common}/src/result/index.adoc#resultthat[Result.that()]`
checks if the `Success.value` satisfies a given predicate.
<.> `link:{perfective-common}/src/result/index.adoc#resultwhen[Result.when()]`
checks an external condition.
<.> `link:{perfective-common}/src/result/index.adoc#resultonto[Result.onto()]`
allows a different `Result` object to be returned
(in this case, the `Result` of the `validUserId()` function).
<.> `link:{perfective-common}/src/result/index.adoc#resultthrough[Result.through()]`
runs a given procedure
(a no-op `naught()` function for the `Success`).
<.> `link:{perfective-common}/src/result/index.adoc#resultinto[Result.into()]`
allows the completion (folding) of the `Result` chain computation and switch to a different type.In addition to the methods used in the example above,
the `Result` monad also provides
`link:{perfective-common}/src/result/index.adoc#resultor[Result.or()]` and
`link:{perfective-common}/src/result/index.adoc#resultotherwise[Result.otherwise()]` methods.Read more about the `Result` monad and other
`link:{perfective-common}/src/result/index.adoc[@perfective/common/result]` functions in the
link:{perfective-common}/src/result/index.adoc[package documentation].=== Chained Exceptions
The ECMA `Error` class does not store a previous error.
This is inconvenient, as it requires either manually adding a previous error message to a new error.
Or worse, skip providing the previous error altogether.Chaining previous errors is helpful for debugging.
Especially in async environments, when most of the stack trace is full of useless function calls like `next()`
or on the frontend with packed code and renamed functions.The `link:{perfective-common}/src/error/index.adoc[@perfective/common/error]` package provides the `Exception` class
to make logging and debugging of production code easier.
It supports three features:* providing a previous error (allows to stack errors);
* using a message template with string tokens (allows to localize and format messages);
* storing additional context (simplifies logging and debugging)..Using the `Exception` class and its constructors.
[source,typescript]
----
import { caughtError, causedBy, chained, exception } from '@perfective/common/error';interface FetchRequest {
method: string;
url: string;
}interface User {}
function numberInput(input: string): number {
const id = Number(input);
if (Number.isNaN(id)) {
throw exception('Input {{value}} is not a number', { // <.>
value: input,
});
}
return id;
}function userRequest(id: string): FetchRequest {
try {
const userId = numberInput(id);
return {
method: 'GET',
url: `user/${userId}`,
};
}
catch (error: unknown) { // <.>
throw causedBy(caughtError(error), 'Invalid user id {{id}}', { // <.>
id,
});
}
}async function userResponse(request: FetchRequest): Promise {
return fetch(request.url, {
method: request.method,
});
}async function user(id: string): Promise {
return Promise.resolve(id)
.then(userRequest)
.then(userResponse)
.catch(chained('Failed to load user {{id}}', { // <.>
id,
}));
}
----
<.> Use the `exception()` function to instantiate an initial `Exception` without previous errors.
<.> Use the `caughtError()` function to wrap a possible non-`Error` value.
<.> When you use a `try-catch` block,
use the `causedBy()` function to create an `Exception` with a previous error.
<.> Use the `chained()` function to create a callback to chain an `Error`
(for example, in `Promise` or a `Result`).When you want to output a chained `Exception`,
you can use the `Exception.toString()` method.
For the example above, the output may look like this:[source,text]
----
Exception: Failed to load user `A`
- Exception: Invalid user id `A`
- Exception: Input `A` is not a number
----If you want to log an `Exception` for debugging purposes, use the `chainedStack()` function.
It will return a similar chain of messages as above,
but each message will also contain a stack trace for each error.Read more about the functions to handle the built-in JS errors and the `Exception` class in the
`link:{perfective-common}/src/error/index.adoc[@perfective/common/error]` package documentation.== Packages
Packages are organized and named around their primary type:
* `link:{perfective-common}/src/value/index.adoc[@perfective/common]`
— functions and types to handle types (e.g., `TypeGuard` interface), `null`, `undefined`, and `void` values.
+
* `link:{perfective-common}/src/array/index.adoc[@perfective/common/array]`
— functions and types for handling
link:{mdn-js-globals}/Array[arrays].
+
* `link:{perfective-common}/src/boolean/index.adoc[@perfective/common/boolean]`
— functions and types to handle
`link:{mdn-js-globals}/Boolean[boolean]` values.
+
* `link:{perfective-common}/src/date/index.adoc[@perfective/common/date]`
— functions and types to handle
`link:{mdn-js-globals}/Date[Date]` object.
+
* `link:{perfective-common}/src/error/index.adoc[@perfective/common/error]`
— functions and types to handle
`link:{mdn-js-globals}/Error[Error]`
and related classes.
+
* `link:{perfective-common}/src/function/index.adoc[@perfective/common/function]`
— functions and types for functional programming.
+
* `link:{perfective-common}/src/match/index.adoc[@perfective/common/match]`
— functions and types for a functional style `switch-case`.
+
* `link:{perfective-common}/src/maybe/index.adoc[@perfective/common/maybe]`
— a `Maybe` monad (https://en.wikipedia.org/wiki/Option_type[Option type]) implementation.
+
* `link:{perfective-common}/src/number/index.adoc[@perfective/common/number]`
— functions and types to handle
link:{mdn-js-globals}/Number[numbers].
+
* `link:{perfective-common}/src/object/index.adoc[@perfective/common/object]`
— functions and types for handling the
`link:{mdn-js-globals}/Object[Object]` class.
+
* `link:{perfective-common}/src/promise/index.adoc[@perfective/common/promise]`
— functions and types to handle the
`link:{mdn-js-globals}/Promise[Promise]` class.
+
* `link:{perfective-common}/src/result/index.adoc[@perfective/common/result]`
— a `Result` monad (https://en.wikipedia.org/wiki/Result_type[Result type]) implementation.
+
* `link:{perfective-common}/src/string/index.adoc[@perfective/common/string]`
— functions and types to handle
link:{mdn-js-globals}/String[strings].The packages have full unit test coverage.
[IMPORTANT]
====
The code provided by this project relies on strict https://www.typescriptlang.org[TypeScript] compiler checks.
Using these packages in regular JS projects may produce unexpected behavior and is undocumented.
For example,
a function that declares an argument as _required_ relies on strict TSC `null` checks
and may not additionally check the value for `null`.
====== Roadmap
The `link:{perfective-common}/ROADMAP.adoc[ROADMAP.adoc]` file describes
how built-in JavaScript objects and methods are covered by the `@perfective/common` package.