Perfective Common for TypeScript
Installation
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 @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.
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:
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;
}
When using the Maybe
monad,
you can write simpler and more readable functions:
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') // (1)
.onto(fullName) // (2)
.or(panic('User name is unknown')); // (3)
}
function fullName(name: Name): Maybe<string> {
return just(`${name.first} ${name.last}`)
.to(trim) // (4)
.that(isNotEmpty); // (5)
}
-
Maybe.pick()
provides a strictly-typed "optional chaining" of theMaybe.value
properties. -
Maybe.onto()
(flat) maps aMaybe.value
to anotherMaybe
. -
Maybe.or()
extracts avalue
from theMaybe
with a given fallback. (or allows to throw an error). -
Maybe.that()
filters a value insideMaybe
. -
Maybe.to()
maps a value insideMaybe
using a given callback.
In addition to these methods,
the Maybe
type also provides:
Maybe.into()
,
Maybe.which()
,
Maybe.when()
,
Maybe.otherwise()
,
and Maybe.through()
methods.
Result monad
The @perfective/common/result
package provides a Result
monad
(a concrete case of an Either monad).
It allows you to treat errors as part of a function result
and chain processing of such results.
For example, consider you are writing a function that loads a backend entity based on the user input.
interface EntityRequest {
entity: string;
id: number;
}
interface User {
id: number;
username: string;
}
/**
* @throws {Error} If a given input is empty or is not a number.
*/
declare function numberInput(input: string): number;
declare function request(entity: string): (id: number) => EntityRequest;
declare function user(request: EntityRequest): User;
Writing an imperative code, you would have:
function userOutput(input: string): User {
let id: number;
try {
id = numberInput(input);
}
catch {
id = 0;
}
const userRequest = request('user');
return user(userRequest(id));
}
Using the Result
your code would be:
import { Result, resultOf } from '@perfective/common/result';
function userOutput(id: string): Result<User> {
return resultOf(() => numberInput(id))
.otherwise(0)
.to(request('user'))
.to(user);
}
The Result
integrates
with the Promise
using the promisedResult()
and settledResult()
functions.
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 @perfective/common/error
package provides the Exception
class
to make logging and debugging of productions 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).
Exception
class and its constructors.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', { // (1)
value: input,
});
}
return id;
}
function userRequest(id: string): FetchRequest {
try {
const userId = numberInput(id);
return {
method: 'GET',
url: `user/${userId}`,
};
}
catch (error: unknown) { // (2)
throw causedBy(caughtError(error), 'Invalid user id {{id}}', { // (3)
id,
});
}
}
async function userResponse(request: FetchRequest): Promise<User> {
return fetch(request.url, {
method: request.method,
});
}
async function user(id: string): Promise<User> {
return Promise.resolve(id)
.then(userRequest)
.then(userResponse)
.catch(chained('Failed to load user {{id}}', { // (4)
id,
}));
}
-
Use the
exception()
function to instantiate an initialException
without previous errors. -
Use the
caughtError()
function to wrap a possible non-Error
value. -
When you use a
try-catch
block, use thecausedBy()
function to create anException
with a previous error. -
Use the
chained()
function to create a callback to chain anError
(for example, inPromise
or aResult
).
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:
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
@perfective/common/error
package docs.
Packages
Packages are organized and named around their primary type:
-
@perfective/common
— functions and types to handle types (e.g.,TypeGuard
interface),null
,undefined
, andvoid
values. -
@perfective/common/array
— functions and types for handling arrays. -
@perfective/common/boolean
— functions and types to handleboolean
values. -
@perfective/common/error
— functions and types to handleError
and related classes. -
@perfective/common/function
— functions and types for functional programming. -
@perfective/common/match
— functions and types for a functional styleswitch-case
. -
@perfective/common/maybe
— aMaybe
monad (Option type) implementation. -
@perfective/common/number
— functions and types to handle numbers. -
@perfective/common/object
— functions and types for handling theObject
class. -
@perfective/common/promise
— functions and types to handle thePromise
class. -
@perfective/common/result
— aResult
monad (Result type) implementation. -
@perfective/common/string
— functions and types to handle strings.
The packages have full unit test coverage.
Important
|
The code provided by this project relies on strict 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 |