Common

The @perfective/common package provides functions to work with undefined and null values, utility types to work with undefined and null (similar to NonNullable<T>). It also provides functions to work with type and instance definitions.

Value

  • type Defined<T> = T extends undefined ? never : T — constructs a type by excluding undefined from T.

    • defined<T>(value: T | undefined): T — returns a given value, if it is defined. Throws a TypeError otherwise.

    • isDefined<T>(value: T | undefined): value is Defined<T> — a type guard that returns true, if a given value is not undefined.

  • type Undefined<T> = T extends undefined ? T : never — constructs a type by excluding defined types from T.

    • isUndefined<T>(value: T | undefined): value is undefined — a type guard that returns true if a given value is undefined.

  • type NotNull<T> = T extends null ? never : T — constructs a type by excluding null from T.

    • notNull<T>(value: T | null): T — returns a given value, if it is not null. Throws a TypeError otherwise.

    • isNotNull<T>(value: T | null): value is NotNull<T> — a type guard that returns true if a given value is not null.

  • type Null<T> = T extends null ? T : never — constructs a type by excluding non-null types from T.

    • isNull<T>(value: T | null): value is null — a type guard that returns true if a given value is null.

  • type Present<T> = T extends null | undefined ? never : T — constructs a type by excluding null and undefined from T.

    Same as NonNullable<T>, added for consistency with the terminology of the package. It is defined as a conditional type (not T & {}) to ensure support of TS compiler before v4.9.

    • present<T>(value: T | null | undefined): T — returns a given value, if it is defined and is not null. Throws a TypeError otherwise.

    • isPresent<T>(value: T | null | undefined): value is Present<T> — a type guard that returns true if a given value is defined and is not null.

  • type Absent<T> = T extends null | undefined ? T : never — constructs a type by excluding non-null and defined types from T.

    • isAbsent<T>(value: T | null | undefined): value is null | undefined — a type guard that return true if a given value is null or undefined.

isDefined and isUndefined use the same check against void 0 as the JS code generated by TypeScript for the optional chaining, while TS compiler keeps checks typeof value === 'undefined' and value === undefined as is.

Type and Instance

  • TypeGuard<T, V extends T> = (value: T) ⇒ value is V — a function type to check if the value is of a certain type.

  • Instance<T, U extends any[] = any[]> = abstract new(…​args: U) ⇒ T — definition for class type reference:

    • isInstanceOf<T, U extends any[] = any[], V = unknown>(type: Instance<T, U>): (value: T | V) ⇒ value is T — creates a type guard that returns true if a passed argument is an instance of a given type.

    • isNotInstanceOf<T, U extends any[] = any[], V = unknown>(type: Instance<T, U>): (value: T | V) ⇒ value is V — creates a type guard that returns true if a passed argument is not an instance of a given type.

The isInstanceOf/isNotInstanceOf functions generic parameters are not always correctly recognized by the compiler. It works as expected for the concrete classes, but requires explicit type parameter for the TypeScript classes.

import { isInstanceOf } from '@perfective/common';


class A { public readonly value: string = 'a'; }
class B extends A {}
class C { public readonly c: string = 'value'; }

let x: A | C = new B();

if (isInstanceOf(A)(x)) { (1)
    x.value; (2)
}

let xOrError: A | TypeError = new TypeError();
if (isInstanceOf<TypeError>(TypeError)(xOrError)) { (3)
}

let typeOrRangeError: TypeError | RangeError = new RangeError();
if (isInstanceOf<TypeError>(TypeError)(typeOrRangeError)) { (4)
}
1 isInstanceOf infers class A as the type parameter.
2 Compiler type guards x as an instance of A.
3 Explicitly providing the built-in type. Otherwise, compiler considers xOrError as A | TypeError. The function works correctly.
4 For the union of two built-in type, even explicit type parameter does not work. The function works correctly.

ECMA and TypeScript types

JavaScript provides the typeof operator that returns 'object' for arrays and null. This results in additional checks needed using this operator. To reduce this complexity, the typeOf<T> function is provided with the conditional TypeOf<T> type.

  • Types

    • EcmaType — is one of undefined, boolean, number, bigint, string, symbol, function, or object.

    • TsType — is one of EcmaType or null, array, and unknown.

    • TypeOf<T> — a conditional type that returns TsType dynamically.

  • Unit functions:

    • ecmaType(type: EcmaType): EcmaType — Type guards an EcmaType value in compile time (unlike the as operator which allows to type cast any string as EcmaType). Throws a TypeError when the value in runtime is not an EcmaType.

    • tsType(type: TsType): TsType — Type guards a TsType value in compile time (unlike the as operator which allows to type cast any string as TsType). Throws a TypeError when the value in runtime is not a TsType.

    • typeOf<T>(value: T | null | undefined): TypeOf<T> & TsType. — returns the name of a TypeScript type of a given value.

  • Type guards:

    • isEcmaType(input: string): input is EcmaType — returns true and narrows the variable type, if the given input string is an EcmaType.

    • isTsType(input: string): input is TsType — returns true and narrows the variable type, if the given input string is a TsType.

  • Predicates:

    • isTypeOf<T>(type: TypeOf): (value: T | null | undefined) ⇒ boolean — creates a predicate that returns true, if a passed argument is of a given TypeScript type.

    • isNotTypeOf<T>(type: TypeOf): (value: T | null | undefined) ⇒ boolean — creates a predicate that returns true, if a passed argument is not of a given TypeScript type.

Handling void

In TypeScript, void type is treated differently from undefined and null, and linters provide different rules to restrict its usage only to correct cases.

Unfortunately, different packages (like AWS SDK) may use void as a synonym to null | undefined. This approach creates conflicts in the code linted with the @perfective/eslint-config, so a special type and function are introduced to "type cast" any void value into null | undefined:

  • Voidable<T> = T | void — a value that is either of type T or is void.

  • voidable<T>(value: T | void): T | null | undefined — casts a given Voidable value into an optional nullable type.

Array

The @perfective/common/array package provides functions for the standard JS Array class:

Constructors

  • array<T>(…​elements: T[]): T[] — creates an array from given elements.

  • elements<T>(value: Iterable<T> | ArrayLike<T>): T[] — creates an array from elements of a given Iterable or an ArrayLike value.

  • copy<T>(array: T[]): T[] — creates a shallow copy of a given array.

  • concatenated<T>(arrays: T[][]): T[] — concatenates given arrays in order they are given.

  • concatenated<T>(initial: T[], …​arrays: T[][]): T[] — concatenates given arrays in order they are given.

  • intersection<T>(array1: T[], array2: T[]): T[] — returns an array of unique elements that are included in both given arrays.

  • replicated<T>(value: T, count: number): T[] — creates an array with a given value replicated a given count of times.

  • replicated<T>(count: number): Unary<T, T[]> — creates a callback that replicates the input value a given count of times.

  • reversed<T>(array: T[]): T[] — creates a shallow copy of a given array with elements in reversed order.

  • sorted<T>(array: T[], order?: Compare<T>): T[] — returns a shallow copy of a given array sorted with a given order callback.

  • sorted<T>(order?: Compare<T>): Unary<T[], T[]> — creates a callback to return a shallow copy of the input array sorted with a given order callback.

  • unique<T>(array: T[]): T[] — returns an array with only the first occurrence of each value in a given array.

  • wrapped<T>(value: T | T[]): T[] — returns an array that consists only of a given value. If a given value is an array, returns the original value.

Type guards

  • isArray<T, V = unknown>(value: T[] | V): value is T[] — returns true if a given value is an array. Otherwise, returns false.

  • isNotArray<T, V = unknown>(value: T[] | V): value is V — returns true if a given value is not an array. Otherwise, returns false.

Predicates

  • isEmpty<T>(value: T[]): boolean — returns true if a given array is empty. Otherwise, returns false.

  • isNotEmpty<T>(value: T[]): boolean — returns true if a given array is not empty. Otherwise, returns false.

  • includes<T>(search: T, from?: number): Predicate<T[]> — creates a callback that returns true if a given value is included in the input array (optionally, starting from a given index).

  • includedIn<T>(array: T[], from?: number): Predicate<T> — creates a callback that returns true if the input value is present in a given array (optionally, checking from a given index).

  • every<T>(condition: Predicate<T>): Predicate<T[]> — creates a callback that returns true if all elements of the input array satisfy a given condition.

  • some<T>(condition: Predicate<T>): Predicate<T[]> — creates a callback that returns true if the input array contains at least one element that satisfies a given condition.

Iterators

  • entries<T>(array: T[]): IterableIterator<[number, T]> — returns a new array iterator that contains the key/value pairs for each index of a given array.

  • keys<T>(array: T[]): IterableIterator<number> — returns a new array iterator that contains the keys of each index in the array.

  • values<T>(array: T[]): IterableIterator<T> — returns a new array iterator that iterates the value of each item in a given array.

Filter

Filter<T, S extends T> — a callback that can be passed into the Array.prototype.filter() method.

  • filter<T>(condition: Predicate<T>): Unary<T[], T[]> — creates a callback that returns an array with element the input array that satisfy a given condition.

  • by<T, K extends keyof T>(property: K, condition: Predicate<T[K]>): Filter<T, T> — creates a filter callback that returns true if a given property of the input value satisfies a given condition.

  • isFirstOccurrence<T>(value: T, index: number, array: T[]): boolean — returns true, if a given value has its first occurrence at given index in a given array. Otherwise, returns false.

  • isLastOccurrence<T>(value: T, index: number, array: T[]): boolean — returns true, if a given value has its last occurrence at given index in a given array. Otherwise, returns false.

Map

Map<T, U> — a callback that can be passed into the Array.prototype.map() method.

  • map<T, V>(callback: Unary<T, V>): Unary<T[], V[]> — creates a callback that returns an array of results of a given callback applied to each element of the input array.

Reduce

Reduce<T, V> — a callback that can passed into the Array.prototype.reduce() and Array.prototype.reduceRight() methods.

  • reduce<T, V>(reducer: Reducer<T, V>, initial: V): Unary<T[], V> — creates a callback that reduces the input array with a given reducer callback using a given initial value.

  • reduceTo<T>(reducer: Reducer<T, T>): Unary<T[], T> — creates a callback that reduces the input array with a given reducer callback without an initial value.

  • reduceRight<T, V>(reducer: Reducer<T, V>, initial: V): Unary<T[], V> — creates a callback that reduces the input array with a given reducer callback using a given initial value starting from the end of the array.

  • reduceRightTo<T>(reducer: Reducer<T, T>): Unary<T[], T> — creates a callback that reduces the input array with a given reducer callback without an initial value starting from te end of the array.

  • join<T>(separator: string = ','): Unary<T[], string> — creates a callback that returns a string concatenated from elements of the input array with a given separator.

Sort

Compare<T> = (a: T, b: T) ⇒ number — a function that defines the sort order and can be passed into the Array.prototype.sort() method.

  • sort<T>(order?: Compare<T>): Unary<T[], T[]> — creates a callback that returns the input array sorted in-place using a given order function.

Element

type Element<A> = A extends readonly (infer T)[] ? T : undefined — infers the element type of a given array A.

  • head<T>(array: T[]): T | undefined — returns the first element of a given array.

  • tail<T>(array: T[]): T[] — returns a sub-array of a given array, without the first element.

  • end<T>(array: T[]): T | undefined — returns the last element of a given array.

  • init<T>(array: T[]): T[] — returns a sub-array of a given array, without the last element.

  • element<T>(index: number): Unary<T[], T | undefined> — creates a callback that returns an element at a given index in the array input.

  • find<T>(condition: Predicate<T>): Unary<T[], T | undefined> — creates a callback that returns the first element of the input array that satisfies a given condition; or returns undefined if no elements satisfy the condition.

  • pop<T>(array: T[]): T | undefined — returns the last element of a given array and removes it from the array. Returns undefined if the array is empty.

  • push<T>(…​elements: T[]): Unary<T[], number> — creates a callback that adds given elements to the end of the input array and returns its new length.

  • pushInto<T>(array: T[]): (…​elements: T[]) ⇒ number — creates a callback that adds the input elements to the end of a given array and returns its new length.

  • shift<T>(array: T[]): T | undefined — removes the first element of a given array and it.

  • unshift<T>(…​elements: T[]): Unary<T[], number> — creates a callback that adds given elements to the beginning of the input array and returns its new length.

  • findIndex<T>(condition: Predicate<T>): Unary<T[], number | -1> — creates a callback that returns the index of the first element of the input array that satisfies a given condition; or returns -1 if no elements satisfy the condition.

  • indexOf<T>(search: T, from?: number): Unary<T[], number | -1> — creates a callback that returns the first index of a given value in the input array (optionally, starting from a given index); or returns -1 if the value is not present.

  • lastIndexOf<T>(search: T, from?: number): Unary<T[], number | -1> — creates a callback that returns the last index of a given value in the input array; or returns -1 if the value is not present.

  • first<T>(count: number = 1): Unary<T[], T[]> — creates a callback that returns an array of the first count of the input array elements.

  • last<T>(count: number = 1): Unary<T[], T[]> — creates a callback that returns an array of the last count of the input array elements.

  • append<T>(element: T): Unary<T[], T[]> — creates a callback that returns a shallow copy of the input array with a given element added as the last element.

  • prepend<T>(element: T): Unary<T[], T[]> — creates a callback that returns a shallow copy of the input array with a given element added as the first element.

  • insert<T>(index: number, element: T): Unary<T[], T[]> — creates a callback that returns a shallow copy of the input array with a given element inserted at a given index.

  • insertInto<T>(array: T[], index: number): Unary<T, T[]> — creates a callback that returns a shallow copy of a given array with the input element inserted at a given index.

  • replace<T>(index: number, element: T): Unary<T[], T[]> — creates a callback that returns a shallow copy of the input array with a given element replacing the input array element at a given index.

  • remove<T>(index: number): Unary<T[], T[]> — creates a callback that returns a shallow copy of the input array without an element at a given index.

  • concat<T>(…​items: ConcatArray<T>[]): Unary<T[], T[]> — creates a callback that merges given items to the input array.

  • slice<T>(start?: number, end?: number): Unary<T[], T[]> — creates a callback that returns an array of elements between given start and end (exclusive) indices of the input array.

  • copyWithin<T>(target: number, start: number = 0, end?: number): Unary<T[], T[]> — creates a callback that shallow copies elements within a given start to end range starting from the target index.

  • fill<T>(value: T, start?: number, end?: number): Unary<T[], T[]> — creates a callback that changes all elements of the input array within a given start to end range to a given value.

  • reverse<T>(array: T[]): T[] — reverses a given array (in-place).

  • splice<T>(start: number, deleteCount?: number): Unary<T[], T[]> — creates a callback that removes (in-place) count number of elements of the input array from a given start index and returns the array.

  • spliceWith<T>(start: number, deleteCount: number, …​elements: T[]): Unary<T[], T[]> — creates a callback that replaces (in-place) count number of elements of the input array from a given start index with given elements and returns the array.

  • forEach<T>(procedure: UnaryVoid<T>): UnaryVoid<T[]> — creates a callback that executes a given procedure on every element of the input array.

Boolean

The @perfective/common/boolean package provides types and functions to work with the boolean type.

Proposition

Proposition<T> is a boolean value, or a nullary function that returns a boolean value.

  • isTrue(proposition: Proposition): boolean — returns true if a given proposition is true or returns true. Otherwise, returns false.

  • isFalse(proposition: Proposition): boolean — returns true if a given proposition is false or returns false. Otherwise, returns false.

  • negated(proposition: Proposition): boolean — returns true if a given proposition is false or returns false. Otherwise, returns false.

Predicate

Predicate<T> is a unary function that returns a boolean value.

  • isBoolean<T>(value: T | boolean): value is boolean — returns true if a given value is boolean.

  • isNotBoolean<T>(value: T | boolean): value is T — returns true if a given value is not boolean.

  • isTruthy<T>(value: T): boolean — returns true when the value is neither undefined, null, false, NaN, 0, -0, 0n (a BigInt zero), "" (an empty string), or the document.all builtin.

  • isFalsy<T>(value: T): boolean — returns true when the value is undefined, null, false, NaN, 0, -0, 0n (a BigInt zero), "" (an empty string), or the document.all builtin.

  • is<T>(input: T): Predicate<T> — creates a Predicate that is true if its argument strictly equals a given input.

  • isNot<T>(input: T): Predicate<T> — creates a Predicate that is true if its argument does not equal a given input.

  • not<T>(predicate: Predicate<T>): Predicate<T> — creates a Predicate that inverses the result of a given Predicate.

  • all<T>(…​predicates: Predicate<T>[]): Predicate<T> — creates a Predicate that is true when all given predicates are true (logical AND).

  • either<T>(…​predicates: Predicate<T>[]): Predicate<T> — creates a Predicate that is true when at least one of given predicates is true (logical OR).

  • neither<T>(…​predicates: Predicate<T>[]): Predicate<T> — creates a Predicate that is true when none of given predicates is true.

  • atLeast<T>(minimum: number, …​predicates: Predicate<T>[]): Predicate<T> — creates a Predicate that is true when at least a given minimum number of given predicates is true.

  • atMost<T>(maximum: number, …​predicates: Predicate<T>[]): Predicate<T> — creates a Predicate that is true when no more than a given maximum number of given predicates is true.

  • exactly<T>(count: number, …​predicates: Predicate<T>[]): Predicate<T> — creates a Predicate that is true when exactly a given count number of given predicates is true.

Date

The @perfective/common/date package provides functions to work with the Date type.

There are a couple important differences in how @perfective handles the Date values.

  • @perfective treats Date objects as immutable, so setter methods are not supported.

  • @perfective functions return null instead of the Invalid Date instances.

    Using null allows to combine the results of the functions using Maybe:

    import { date, Timestamp, timestamp } from '@perfective/common/date`;
    import { Maybe, just } from '@perfective/common/maybe';
    
    function parseTime(input: string): Maybe<number> { (1)
        const time = Date.parse(input);
        if (Number.isNaN(time)) {
            return nothing();
        }
        return just(time);
    }
    
    function maybeTime(input: string): Maybe<Timestamp> { (2)
        return just(input).to(date).to(timestamp);
    }
    1 Date.parse(input) returns a number or NaN, so additional checks are required.
    2 date() function returns Date | null, so it’s easily chained into Maybe.

Date

  • Constructors:

    • date(date: Date): Date | null; — creates a copy of a given date. If the given date is Invalid Date, returns null.

    • date(timestamp: Timestamp): Date | null — creates a Date for a given Timestamp. If the given Timestamp is invalid, returns null.

    • date(input: string): Date | null — parses a Date from a given input string. If the given input cannot be parsed, returns null.

    • now(): Date — creates a Date for the current moment. Uses Date.now(), so it can be mocked in the tests (for example, in Jest).

    • epoch(): Date — creates a Date for the Unix epoch (i.e., January 1, 1970, 00:00:00 UTC).

  • Predicates:

    • isValid(date: Date): boolean — returns true if a given Date is a valid (not an Invalid Date).

    • isInvalid(date: Date): boolean — returns true if a given Date is an Invalid Date.

Timestamp

  • Timestamp = number — an alias type for number and is specified in milliseconds since the Unix epoch.

  • timestamp(date: Date): Timestamp | null — returns a Timestamp for a given date. If the given Date is Invalid Date, returns null.

  • timestamp(input: string): Timestamp | null — parses a given date/time input string as a Timestamp. If the given input is invalid, returns null.

    Use instead of the Date.parse() function, as it’s easy to compose using maybe().

Error

The @perfective/common/error package helps organize exceptions and error handling. It defines an Exception, based on the JS Error class, that supports localizable error messages and error chaining; and provides functions to handle error stack output.

Exception

Exception type definition.
class Exception extends Error {
    public readonly name: string = 'Exception';
    public message: string; (1)

    public readonly template: string; (2)
    public readonly tokens: ExceptionTokens; (3)
    public readonly context: ExceptionContext; (4)
    public readonly previous: Error | null; (5)

    public toString(): string; (6)
}
1 Inherited from Error, message is generated from the template and tokens.
2 Message template with tokens wrapped in {{ }}, e.g. User ID {{id}} or User {{user_id}}.
3 Token/Value map to be used in the error message.
4 Additional context information. Context is not rendered by default, but may be useful in applications.
5 Provide a previous error, if exists.
6 Human-readable output of all the errors.

Constructors

  • exception(message: string, tokens: ExceptionTokens = {}, context: ExceptionContext = {}): Exception — creates an Exception.

  • chained(message: string, tokens: ExceptionTokens = {}, context: ExceptionContext = {}): (previous: Error) ⇒ Exception — creates a function to wrap a previous Error into an Exception.

  • causedBy(previous: Error, message: string, tokens: ExceptionTokens = {}, context: ExceptionContext = {}): Exception — creates an Exception with a previous Error.

  • caughtError(value: unknown): Error | Exception — wraps a non-Error value into an Exception. The Exception.message starts with Caught and contains the caught value coerced to a string.

    If given an Error, returns it as is.

Custom exceptions

  • invalidArgumentException(argument: string, expected: string, actual: string): Exception — creates an exception with the message about an invalid argument.

Usage

import { causedBy, chainStack, exception, rethrows, throws } from '@perfective/common/error';
import { decimal } from '@perfective/common/number';

function positive(input: number): string {
    if (input <= 0) {
        return throws(exception('Invalid input {{input}}', { (1)
            input: decimal(input);
        }));
    }
    return decimal(10);
}

function field(name: string, value: number): string {
    try {
        return `${name} = ${positive(value)}`;
    }
    catch (error) {
        return throws(causedBy(error, 'Failed to output field {{field}}', { (2)
            field: name,
        }));
    }
}

function record(): Record<string, string> {
    try {
        return {
            username: field('username', -42),
        }
    }
    catch (error) {
        return rethrows(error, 'Failed to build a record'); (3)
    }
}

try {
    record();
}
catch (error) {
    console.error(error.toString()); (4)
    // Outputs:
    //  Exception: Failed to build a record
    //      - Exception: Failed to output field `username`
    //      - Exception: Invalid input `-42`

    console.error(chainStack(error)); (5)
    // Outputs:
    //  Exception: Failed to build a record
    //      at Object.rethrows (...)
    //      at Object.record (...)
    //      ...
    //  Caused by: Exception: Failed to output field `username`
    //      at Object.causedBy (...)
    //      at Object.throws (...)
    //      at Object.field (...)
    //      ...
    //  Caused by: Exception: Invalid input `-42`
    //      at Object.exception (...)
    //      at Object.throws (...)
    //      at Object.positive (...)
    //      ...
}
1 throws can be used with any Error type, but Exception supports passing context information separately, which can be convenient for localization or logging purposes.
2 causedBy is a function to create an Exception with a present Exception.previous property.
3 rethrows is a shortcut for the throws(causedBy()) call. It supports message context as well.
4 error.toString() is explicitly re-defined on Exception to output all error messages.
5 chainStack() allows to output all errors with their stack information (platform dependent).

Panic

type Panic = (cause?: unknown) ⇒ never — a function that throws an Error.

  • panic(message: string, tokens?: ExceptionTokens, context?: ExceptionContext): Panic — creates a function that throws an Exception with a given message template with tokens and additional context data. If the cause is defined, sets the cause as a previous error.

    Useful working with Promise.catch() and RxJS catchError().

  • panic<E extends Error>(error: Value<E>): Panic — creates a function that throws a given Error. Ignores the cause, even when if is defined.

    Use panic to create a callback for lazy evaluation.
    import { panic } from '@perfective/common/error';
    import { maybe } from '@perfective/common/maybe';
    
    export function example(input: string | null | undefined): string {
        return maybe(input)
            .or(panic('Input is not present')); (1)
    }
    1 Must use panic(), as the fallback in Maybe.or() is called only when the input is not present. Using throws() will result in throwing an exception every time a function is called.
  • throws(message: string, tokens?: ExceptionTokens, context?: ExceptionContext): never — throws an Exception with a given message template with tokens and additional context data.

  • throws<E extends Error>(error: Value<E>): never — throws a given Error. If given a callback, throws an Error returned by the callback.

    Can be used to throw an exception from a one-line arrow function.

  • rethrows(previous: Error, message: string, tokens: ExceptionTokens = {}, context: ExceptionContext = {}): never — throws an Exception with a given message caused by a previous Error. Exception message may contain given tokens and additional context data.

    Similar to throws, but requires to provide a previous error.

Recovery

type Recovery<T> = (error: Error) ⇒ T — a function that handles a given Error and returns a recovery value.

Failure

The default JS Error class does not have toJSON method and is serialized as an empty object by JSON.stringify. This creates a problem for any attempt to transfer error information. Type Failure solved this problem by providing a record type to "serialize" Error and Exception. It omits stack information, but keeps the list of previous errors.

  • Failure

    • failure<E extends Error>(error: E): Failure — convert and Error or an Exception into a Failure record.

Standard built-in JS Error types

  • Error:

    • error(message: string): Error — instantiates a new Error.

    • isError<T>(value: Error | T): value is Error — returns true when the value is an instance of Error.

    • isNotError<T>(value: Error | T): value is T — returns true when the value is not an instance of Error.

  • EvalError:

    • evalError(message: string): EvalError — instantiates a new EvalError.

    • isEvalError<T>(value: EvalError | T): value is EvalError — returns true when the value is an instance of EvalError.

    • isNotEvalError<T>(value: EvalError | T): value is T — returns true when the value is not an instance of EvalError.

  • RangeError:

    • rangeError(message: string): RangeError — instantiates a new RangeError.

    • isRangeError<T>(value: RangeError | T): value is RangeError — returns true when the value is an instance of RangeError.

    • isNotRangeError<T>(value: RangeError | T): value is T — returns true when the value is not an instance of RangeError.

  • ReferenceError:

    • referenceError(message: string): ReferenceError — instantiates a new ReferenceError.

    • isReferenceError<T>(value: ReferenceError | T): value is ReferenceError — returns true when the value is an instance of ReferenceError.

    • isNotReferenceError<T>(value: ReferenceError | T): value is T — returns true when the value is not an instance of ReferenceError.

  • SyntaxError:

    • syntaxError(message: string): SyntaxError — instantiates a new SyntaxError.

    • isSyntaxError<T>(value: SyntaxError | T): value is SyntaxError — returns true when the value is an instance of SyntaxError.

    • isNotSyntaxError<T>(value: SyntaxError | T): value is T — returns true when the value is not an instance of SyntaxError.

  • TypeError:

    • typeError(message: string): TypeError — instantiates a new TypeError.

    • isTypeError<T>(value: TypeError | T): value is TypeError — returns true when the value is an instance of TypeError.

    • isNotTypeError<T>(value: TypeError | T): value is T — returns true when the value is not an instance of TypeError.

  • InternalError is non-standard and won’t be supported.

  • URIError will be supported in the @perfective/common/url package.

Roadmap

Function

The @perfective/common/function package provides types and functions for the functional programming style. As functions with more than three arguments are considered a code smell, this package only declares the Nullary, Unary, Binary, and Ternary types. The functions of higher arity is unlikely to be added to the package.

Type Guards

  • isFunction<T>(value: Function | T): value is Function — returns true if a given value is a Function. Otherwise, returns false.

  • isNotFunction<T>(value: Function | T): value is T — returns true if a given value is not a Function. Otherwise, returns false.

Nullary functions

  • Nullary<T> — a function without arguments:

    • isNullary<F extends Function>(f: F): boolean — returns true if a given function f has length 0 (excluding a variadic argument). Otherwise, returns false.

    • constant<T>(value: T): Nullary<T> — creates a nullary function that returns a given value.

  • Void — a procedure without arguments:

    • naught(): void — an empty function to be passed as a callback when a no-op behavior is required.

  • Value<T> = T | Nullary<T> — a value itself or a nullary function that returns a value. (e.g. for lazy evaluation).

    • valueOf<T>(value: Value<T>): T — When given a nullary function, evaluates the function and returns the result. Otherwise, returns the given value.

Unary functions

  • Unary<X, V> — a function with one argument:

    • isUnary<F extends Function>(f: F): boolean — returns true if a given function f has length 1 (excluding a variadic argument).

    • same<T>(value: T): T — the identity function. Returns a given value.

  • UnaryVoid<T> = (value: T) ⇒ void — a procedure with one argument.

Binary functions

  • Binary<X, Y, V> — a function with two arguments.

    • isBinary<F extends Function>(f: F): boolean — Returns true if a given function f has length 2 (excluding a variadic argument). Otherwise, returns false.

Ternary functions

  • Ternary<X, Y, Z, V> — a function with three arguments.

    • isTernary<F extends Function>(f: F): boolean — returns true if a given function f has length 3 (excluding a variadic argument). Otherwise, returns false.

Length

Length type defines a kind of objects that have "length" (arrays, strings, etc).

interface Length {
    length: number;
}
  • Functions

    • length<L extends Length>(value: L): number — returns the length property of a given value.

  • Predicates

    • hasLength<L extends Length>(length: number): Predicate<L> — returns a predicate that checks if the input value has a given length.

    • isNotEmpty<L extends Length>(value: L): boolean — returns true if a given value has non-positive length.

    • isEmpty<L extends Length>(value: L): boolean — returns true if a given value has length greater than 0.

  • Reducers

    These functions can be used as a callback for Array.prototype.reduce.

    • toShortest<T extends Length>(shortest: T, value: T): T — returns the shortest of two given values.

    • toLongest<T extends Length>(longest: T, array: T): T — returns the longest of two given values.

Arguments

  • BiMap<T1, U1, T2, U2> = [Unary<T1, U1>, Unary<T2, U2>] — a pair of callbacks to map both arguments of a given function at the same time.

  • BiFold<T1, T2, U> = [Unary<T1, U>, Unary<T2, U>] — a pair of unary callbacks to map both arguments T1 and T2 of a function into the same type U.

Match

The @perfective/common/match package provides functions and types for pattern matching.

  • match<T>(value: T | Nullary<T>): Match<T>

    • Match.cases<U>(…​cases: Case<T, U>[]): Maybe<U>

    • Match.cases<U>(cases: Case<T, U>[]): Maybe<U> — applies given value to the Case.condition predicate until the first match, returns the result of applying the value to the Case.statement function; when a match is not found, returns Nothing.

  • when<T>(condition: T | Predicate<T>): When<T>Case<T, U> builder;

    • to<U>(value: U | Unary<T, U>): Case<T, U>

  • fromEntries<T, U>(entries: CaseEntry<T, U>[]): Case<T, U>[] — creates cases from an array of tuples [Predicate<T>, Unary<T, U>].

import { match, when, fromEntries } from '@perfective/common/match';
import { Maybe } from '@perfective/common/maybe';
import { isGreaterThan } from '@perfective/common/number';

function square(x: number): number {
    return x * x;
}

function squares(root: number): Maybe<number> {
    return match(root).cases(
        when(1.41).to(2),
        when(1.73).to(3),
        when(2.23).to(5),
        when(isGreaterThan(3)).to(square), (1)
    );
}

function squares(root: number): Maybe<string> {
    return match(root).cases(fromEntries([
        [1.41, '2']
        [1.73, '3'],
        [2.23, '5'],
        [true, 'unknown'], (2)
    ]));
}
1 when.to() passes the original match value to a callback.
2 fromEntries function allows passing constant values as conditions and as result statements.

Maybe monad

@perfective/common/maybe package provides an Option type implementation. It is inspired by the Haskell Maybe monad and satisfies the monad laws.

Maybe type simplifies handling of the absent (null/undefined) values and provides methods that are called only when the value is present. It allows the creation of chained calls similar to Promise.then() and RxJS pipe().

Handling null and undefined values

In JavaScript, two types represent an "absence" of value: undefined and null. So the dichotomies like Just | Nothing or Some | None do not fit precisely and require additional logic to cover all the cases: T | null | undefined. For example, when you create a new Maybe<T> with a null value, it has to maintain the value as null and should not change it to undefined.

Maybe<T> maintains the original value.
import { maybe, nil, nothing } from '@perfective/common/maybe';
import { isGreaterThan } from '@perfective/common/number';

maybe(3.14).value === 3.14;

maybe(undefined).value === undefined;
nothing().value === undefined; (1)
maybe(3.14)
    .that(isGreaterThan(4))
    .value === undefined; (2)

maybe(null).value === null;
nil().value === null; (3)
maybe(null)
    .that(isGreaterThan(4))
    .value === null; (4)
1 The nothing() function is the default unit function for Nothing<T> and returns a memoized new Nothing(undefined).
2 By default, Maybe uses undefined for the absent value.
3 The nil() function is the secondary unit function for Nothing<T> and returns a memoized new Nothing(null).
4 If the original value is null, null will be returned as an absent value after all transformations.

It is not always desired to have both undefined and null excluded. For example, consider you have a field in an API that is null when there is no data, but the field is not returned if a user does not have access to it. In this case, you may prefer to fall back from null to a default value but to throw an error for undefined.

To cover these cases, @perfective/common/maybe provides the Maybe.into() method: it allows to apply a function that handles either T, null, or undefined value.

The original absent value (null or undefined) is maintained through transformations.
import { isNull, isUndefined } from '@perfective/common';
import { panic } from '@perfective/common/error';
import { just } from '@perfective/common/maybe';

interface ApiResponse {
    username?: string | null;
}

function username(response: ApiResponse) {
    return just(response)
        .pick('username')
        .into((username: string | null | undefined) => { (1)
            if (isUndefined(username)) {
                return panic('Unknown username');
            }
            if (isNull(username)) {
                return 'anonymous';
            }
            return username;
        });
}
1 As Maybe preserves the original type, the result of the pick() can be either nothing() or nil(). So Maybe.into() will have a correct value on input.

Preserving the monad type

This package strictly preserves the type of the monad. For example, if you have a Just type and apply a function that returns a present value, then the result will also be of type Just.

Difference between Maybe, Just and Nothing contexts.
function maybeDecimal(value: number | null | undefined): Maybe<string> {
    return maybe(value).to(v => v.toString(10)); (1)
}

function justDecimal(value: number): Just<string> {
    return maybe(value).to(v => v.toString(10)); (2)
}

function nothingDecimal(value: null | undefined): Nothing<string> {
    return maybe<number>(value).to(a => a.toString(10)); (3)
}
1 The argument of the maybeDecimal function is number | null | undefined. So the maybe() function returns Maybe<number> (which is either Just<number> or Nothing<number>). The result of the function may also be Just or Nothing, because we can not be sure that the to() method will be called, even the v ⇒ v.toString(10) returns a string for any number input.
2 The argument of the justDecimal is always a number. The maybe() function returns Just<number>, because the value is always present. maybe has a custom overload signature, and compiler also knows, that maybe returns Just<number>.

As the v ⇒ v.toString(10) result is always a string, compiler also knows that the result of the whole chain remains present. And the return type can be set as Just<string>.

3 Similarly, when the value can only be null or undefined, the maybe() function returns Nothing<number> in compile time and in runtime. And the return type of the whole chain can be set to Nothing<string>.

Using onto() and to() methods

Both Maybe.onto() and Maybe.to() methods apply a given function only when the value is present. But onto requires the function to return the next Maybe instance, while to will wrap the returned value into Maybe.

When writing functions that use Maybe chaining, the best practice is to return the packed value (as Maybe, Just, or Nothing). This allows a consumer of the function to decide how they want to unpack it or to keep it as Maybe for the next chain.

When you have a function of non-Maybe types, then you have to use Maybe.to.

For example, consider you are writing a function to parse a date.
function isValidDate(date: Date): boolean {
    return date.toString() !== 'Invalid Date'; (1)
}

function parsedDate(input: string): Maybe<Date> { (2)
    const date = new Date(input);
    if (isValidDate(date)) {
        return just(date);
    }
    return nothing();
}

interface BlogPost {
    createdAt: string;
}

function dbDate(input: BlogPost): Date { (3)
    return just(input)
        .pick('createdAt')
        .onto(parsedDate)
        .or(panic('Invalid "Created At" Date'));
}

function jsonDate(input: BlogPost): string|null { (4)
    return just(input)
        .pick('createAt')
        .onto(parsedDate)
        .or(null);
}

function formattedCreatedAt(input: BlogPost): string { (5)
    return just(input)
        .pick('createdAt')
        .onto(parsedDate)
        .or('Unknown date');
}
1 The new Date() constructor creates a Date object even for invalid inputs.
2 We postpone the decision of how to handle an invalid value. By returning Maybe<Date> (instead of Date|null or throwing an error) we allow consumers of the function to make a decision that is most appropriate to their situation.
3 When we record value to the database, it has to be valid. So we must throw an error when the date is invalid.
4 When we return an API response, a null for invalid dates is ok.
5 When we try to format a date in the UI, we may prefer a readable fallback.

Using the into() method with the maybeFrom() function

The Maybe.into() method allows reducing a Maybe instance into a different type. It applies the argument function for present and absent values. In combination with the maybeFrom() function, it allows to apply functions with custom handling of absent values and return a new Maybe instance.

import { isAbsent } from '@perfective/common';
import { just, maybe, maybeFrom } from '@perfective/common/maybe';

function decimal(value: number | null | undefined): string {
    if (isAbsent(value)) {
        return '0'; (1)
    }
    return value.toString(10);
}

maybe(null).onto(x => maybe(decimal(x))) != just(decimal(null)); (2)
maybe(null).to(decimal) != just(decimal(null)); (3)

maybe(null).into(x => maybe(decimal(x)) == just(decimal(null)) (4)
maybe(null).into(maybeFrom(decimal)) == just(decimal(null)) (5)
1 The decimal() function returns a default value for the absent values instead of returning another absent value (or throwing an error).
2 As a result, when decimal() is applied through the Maybe.onto() method, it breaks the left-identity monad law.
3 Applying decimal() through Maybe.to() gives the same incorrect result.
4 Using the Maybe.into() method allows working around this issue because Maybe.into() is called for all Maybe values (not only present values).
5 Use the maybeFrom() function as a shortcut.

Since v0.9.0, Maybe.into(maybeFrom) replaced the Maybe.lift(map) method.

Reference

Types

  • Maybe<T> — an abstract class, represents either Just<T> or Nothing<T>.

  • Just<T> — represents a defined non-null value of type T.

  • Nothing<T> — represents an undefined or null value.

Functions

  • maybe<T>(value: T | null | undefined): Maybe<T> — creates an instance of Just when the value is present, or returns a memoized instance of Nothing with either null or undefined value.

  • maybeFrom<T, U>(map: Unary<T | null | undefined, U | null | undefined>): Unary<T | null | undefined, Maybe<U>>

  • just<T>(value: Present<T>): Just<T> — creates an instance of Just with a given defined non-null value. A unit (return) function for the Maybe monad.

  • justFrom<T, U>(map: Unary<T | null | undefined, Present<U>>): Unary<T | null | undefined, Just<U>> — creates a function that applies a given map to a value and returns the result wrapped into a Just.

  • nothing<T>(): Nothing<Present<T>> — returns a memoized instance of Nothing with an undefined value.

  • nil<T>(): Nothing<Present<T>> — returns a memoized instance of Nothing with a null value.

Type Guards

  • isMaybe<T, U>(value: Maybe<T> | U): value is Maybe<T> — returns true if a given value is a Maybe.

    • isNotMaybe<T, U>(value: Maybe<T> | U): value is U — returns true if a given value is not a Maybe.

  • isJust<T, U>(value: Just<T> | U): value is Just<T> — returns true if a given value is a Just.

    • isNotJust<T, U>(value: Just<T> | U): value is U — returns true if a given value is not a Just.

  • isNothing<T, U>(value: Nothing<T> | U): value is Nothing<T> — returns true if a given value is a Nothing + isNotNothing<T, U>(value: Nothing<T> | U): value is U — returns true if a given value is not a Nothing.

Maybe.onto()

  • Maybe.onto<U>(flatMap: (value: T) ⇒ Maybe<Present<U>>): Maybe<Present<U>>

    • for a Just, applies a given flatMap callback to the Just.value and returns the result;

    • for a Nothing, ignore the flatMap callback and returns the same Nothing.

This method is similar to the mergeMap/switchMap operator in rxjs and the flatMap method in java.util.Optional.

Maybe.to()

  • Maybe.to<U>(map: (value: T) ⇒ U | null | undefined): Maybe<U>

    • for a Just, applies a given map callback to the Just.value and returns the result wrapped into a Maybe.

    • for a Nothing, ignores the map callback and returns the same Nothing.

Using Maybe.to() chaining
import { Maybe, maybe } from '@perfective/common/maybe';
import { lowercase } from '@perfective/common/string';

interface Name {
    first: string;
    last: string;
}

interface User {
    name?: Name;
}

function nameOutput(name: Name): string { (1)
    return `${name.first} ${name.last}`;
}

function usernameOutput(user?: User): Maybe<string> {
    return maybe(user)
        .pick('name')
        .to(nameOutput)
        .to(lowercase);
}
1 The to method wraps the result into maybe.

This method is similar to the map operator in rxjs and the map method in java.util.Optional.

Maybe.into()

  • Maybe.into<U>(reduce: (value: T | null | undefined) ⇒ U): U — applies a given reduce callback to the Maybe.value and returns the result. The purpose of Maybe.into() is to terminate the Maybe and switch to a different type.

Unlike Maybe.onto() and Maybe.to(), the Maybe.into() method is called even if the Maybe.value is absent.

Unlike Maybe.or() and Maybe.otherwise(), the Maybe.into() method is called even if the Maybe.value is present.

Using Maybe.into()
import { Maybe, maybe } from '@perfective/common/maybe';
import { isPresent } from '@perfective/common';

function usernameRequest(userId: number | null | undefined): Promise<string> {
    if (isPresent(userId)) {
        return Promise.resolve({ userId });
    }
    return Promise.reject("UserId is missing");
}

function username(userId: Maybe<number>): Promise<string> {
    return userId.into(usernameRequest) // === usernameRequest(userId.value)
        .then(response => response.username) (1)
        .catch(() => "Unknown");
}
1 While passing the Maybe.value directly into the function is possible, the Maybe.into() method allows to switch the chain to a different monadic type and continue the chain with that new type.

Maybe.pick()

  • Maybe.pick<K extends keyof T>(property: Value<K>): Maybe<Present<T[K]>>

    • for a Just, returns the value of a given property of Just.value wrapped into a Maybe;

    • for a Nothing, ignores the property and returns the same Nothing.

Only properties that are defined on the value type are allowed.

It is similar to the optional chaining introduced in TypeScript 3.7 but does not generate excessive JS code for each null and undefined check in the chain.

Using Maybe.pick() for optional chaining
import { panic } from '@perfective/common/error';
import { maybe } from '@perfective/common/maybe';

interface Name {
    first?: string;
    last?: string;
}

interface User {
    id: number;
    name?: Name;
}

function firstName(user?: User): string {
    return maybe(user).pick('name').pick('first').or(panic('Unknown first name')); (1)
}

function userId(user: User): number {
    return just(user).pick('id').value; (2)
}
1 maybe(user).pick('email') will not compile, as, in this example, the User type does not have an email property.
2 When the value is Just<T>, and you pick a required property, the result is Just<U> (where U is the type of that property). Hence, starting a maybe-chain with Just is strongly recommended if the value is already present.

This method is similar to the pluck operator in rxjs.

Maybe.that()

  • Maybe.that(filter: Predicate<T>): Maybe<T>

    • for a Just, if the value matches a given filter predicate, returns the same Just, otherwise returns Nothing.

    • for a Nothing, ignores the filter and returns itself.

Using Maybe.that() to filter out a value
import { isNot } from '@perfective/common/function';
import { Maybe, just } from '@perfective/common/maybe';

function quotient(dividend: number, divisor: number): Maybe<number> {
    return just(divisor)
        .that(isNot(0)) (1)
        .to(divisor => dividend / divisor);
}
1 Returns Nothing, so to() will not be running its function.

This method is similar to the filter operator in rxjs and the filter method in java.util.Optional.

Maybe.which()

  • Maybe.which<U extends T>(filter: TypeGuard<T, U>): Maybe<U>

    • for a Just, if the value matches a given filter type guard, returns the same Just with a narrowed-down (differentiated) type.

    • for a Nothing, ignores the filter and returns itself.

Maybe.which() is a filter method that requires passing a

It narrows down the result type based on the type guard.

Using Maybe.which() to filter out values with absent properties.
import { Maybe, just } from '@perfective/common/maybe';
import { hasDefinedProperty } from '@perfective/common/object';

interface Name {
    first: string;
    last: string;
}

interface Username {
    first?: string;
    middle?: string;
    last?: string;
}

function nameOutput(name: Name): string {
    return `${name.first} ${name.last}`;
}

function usernameOutput(user: User): Maybe<string> {
    return just(user)
        .which(hasDefinedProperty('first', 'last')) (1)
        .to(nameOutput); (2)
}
1 A broader hasPresentProperty('first', 'last') can also be used. to guarantee that these properties' values are not null too. But it is not required by the TS compiler strictNullCheck, as these properties are optional, not nullable.
2 Name type requires both first and last properties to be defined and not null, so without the which filter (with TS strictNullChecks enabled), this code will not compile.

Maybe.when()

  • Maybe.when(condition: Proposition): Maybe<T>

    • for a Just, if a given condition is true, returns the same Just, otherwise returns Nothing.

    • for a Nothing, ignores the condition and returns itself.

Maybe.when() should be used for better readability instead of passing a nullary function into the Maybe.that().

Using Maybe.when() to filter out values based on a global condition.
import { just } from '@perfective/common/maybe';

function tokenLogOutput(token: string, isLog: boolean): Maybe<string> {
    return just(token)
        .when(isLog) (1)
        .to(token => '***');
}
1 You can use when(() ⇒ isLog) if you only want to run the computation when the value is present.

Maybe.otherwise()

  • Maybe.otherwise(fallback: Value<T | null | undefined>): Maybe<T>

    • for a Just, ignores a given fallback value and returns itself.

    • for a Nothing, returns a given fallback wrapped into a Maybe.

Maybe.otherwise(fallback) method allows passing a fallback value or throwing an error if the value is absent.

Using Maybe.otherwise() to continue the chain after the fallback.
import { panic } from '@perfective/common/error';
import { isNot } from '@perfective/common/function';
import { maybe } from '@perfective/common/maybe';

function range(min?: number, max?: number): number {
    return maybe(min)
        .otherwise(max) (1)
        .that(isNot(0))
        .otherwise(panic('Invalid range'));
}
1 otherwise wraps the fallback value into the next Maybe.

Maybe.or()

  • Maybe.or(fallback: Value<T | null | undefined>): T | null | undefined

    • for a Just, ignores a given fallback value and returns the Just.value.

    • for a Nothing, returns the given fallback value.

The Maybe.or(fallback) method allows getting the present monad value and providing a fallback value or throwing an error when the value is missing.

Using Maybe.or()
import { panic } from '@perfective/common/error';
import { maybe } from '@perfective/common/maybe';

interface Name {
    first: string;
    last: string;
}

interface User {
    name?: Name;
}

function nameOutput(name?: Name): string {
    return maybe(name)
        .to(name => `${name.first} ${name.last}`)
        .or('Unknown name'); (1)
}

function userOutput(user?: User): string {
    return maybe(user)
        .pick('name')
        .to(nameOutput)
        .or(panic('Undefined user')); (2)
}
1 The fallback value type can be present or absent. It allows returning only undefined or null if the value is absent.
2 Using panic or any other function that throws an error when called allows guaranteeing a present value is returned.

This method is similar to the orElse, orElseGet, and orElseThrow methods in java.util.Optional.

Maybe.through()

  • Maybe.through(procedure: (value: T) ⇒ void): Maybe<T>

    • for a Just, runs a given procedure with the Just.value as an argument, the returns the original Just.

    • for a Nothing, ignores the procedure and returns itself.

The Maybe.through() does not check if the given procedure mutates the present value.

import { maybe } from '@perfective/common/maybe';

function logError(error?: Error): Error|undefined {
    return maybe(error)
        .through(console.error);
}

This method is similar to the tap operator in rxjs and ifPresent method in java.util.Optional.

Lifting functions

Each method has a corresponding lifting function to be used in the Array.prototype.map (or any other mapping method or operator).

import { Maybe, just, nil, nothing, or } from '@perfective/common/maybe';

const numbers: Maybe<number>[] = [
    just(2.71),
    just(3.14),
    nothing<number>(),
    nil<number>(),
];

numbers.map(or(0)) === [2.71, 3.14, 0, 0];

Type classes

Monad

The Maybe<T> type is a monad that provides:

  • the Maybe.onto() method as a bind operator (>>=);

  • the just() constructor as a unit (return) function.

It satisfies the three monad laws for defined non-null T:

  1. unit is a left identity for bind:

    let x: T;
    let f: (value: T) => Maybe<U>;
    
    just(x).onto(f) === f(x);
  2. unit is a right identity for bind:

    let ma: Maybe<T>;
    
    ma.onto(just) === ma;
  3. bind is associative:

    let ma: Maybe<T>;
    let f: (value: T) => Maybe<U>;
    let g: (value: U) => Maybe<V>;
    
    ma.onto(a => f(a).onto(g)) === ma.onto(f).onto(g)

If you have a flatMap function with custom handling for null or undefined values, you may break the left-identity and the associativity monad laws.

Custom handling of null with Maybe<T>.onto() breaking the left-identity law.
import { isNull } from '@perfective/common';
import { Just, just, nil } from '@perfective/common/maybe';

function decimal(value: number | null): Just<string> { (1)
    if (isNull(value)) {
        return just('0');
    }
    return just(value.toString(10));
}

just(3.14).onto(decimal) == decimal(3.14); (2)
nil().onto(decimal) != decimal(null); (3)
1 Maybe<T>.onto() expects the function of type Unary<number, Maybe<string>>, but the decimal function is of type Unary<number | null, Maybe<string>>, so the argument type does not match.
2 Applying decimal to a present number behaves as expected.
3 When the value is absent, onto does not execute decimal at all, so the result is not the same as applying decimal directly.

If you have to use custom handling of null/undefined, you should use the Maybe.into() method that passed null and undefined as into the callback.

Custom handling of null and undefined
import { isAbsent } from '@perfective/common';
import { Just, just, nothing, nil } from '@perfective/common/maybe';

function decimal(value: number | null | undefined): Just<string> {
    if (isAbsent(value)) {
        return just('0');
    }
    return just(value.toString(10));
}

just(3.14).onto(decimal) == decimal(3.14); // === just('3.14')
just(3.14).into(decimal) == decimal(3.14); // === just('3.14')

nothing().onto(decimal) == nothing(); // != decimal(undefined);
nothing().into(decimal) == decimal(undefined); // === just('0')

nil().onto(decimal) == nil(); // != decimal(null);
nil().into(decimal) == decimal(null); // === just('0')

For the (legacy) functions (written prior to using Maybe) that handle/return null/undefined, you should use Maybe.map() or Maybe.lift() methods.

Functor

The Maybe<T> type is a functor that provides:

  • the Maybe.to() method as a fmap operator.

It satisfies functor laws for defined non-null T:

  1. Maybe.to() preserves identity morphisms:

    let id = (value: T) => value;
    let value: T;
    
    maybe(value).to(id) === maybe(id(value));
  2. Maybe.to() preserves composition of morphisms:

    let f: (value: U) => V;
    let g: (value: T) => U;
    let value: T;
    
    maybe(value).to(v => f(g(v))) === maybe(value).to(g).to(f);

Number

The @perfective/common/number package declares types and functions to work with real numbers, including the JavaScript Number class.

Number

  • Nominal types:

    • PositiveNumber — a number that is greater than 0.

    • NonNegativeNumber – a number that is greater than or equal to 0.

    • NegativeNumber — a number that is less than 0.

    • NonPositiveNumber — a number that is less than or equal to 0.

    • Sign — indicator of the sign of the number. -1 for negative numbers. 1 for positive numbers.

  • Type guards:

    • isNumber<T>(value: number | T): value is number — returns true if a given value is a number and not a NaN.

    • isNotNumber<T>(value: number | T): value is T — returns true if a given value is not a number or is a NaN.

    • isNonNegativeNumber(value: number): value is NonNegativeNumber — returns true if a given value is greater than or equal to 0. Returns false if the value is less than 0 or is NaN.

  • Assertions:

    • assertIsNotNaN(value: number): asserts value is number

      assertIsNotNaN(value: number, expected: string): asserts value is number

      assertIsNotNaN(argument: string, value: number): asserts value is number

      assertIsNotNaN(argument: string, value: number, expected: string): asserts value is number — asserts that the given number value is not NaN. Throws Exception if the given number value is NaN.

    • assertIsNonNegativeNumber(value: number): asserts value is NonNegativeNumber

      assertIsNonNegativeNumber(argument: string, value: number): asserts value is NonNegativeNumber — asserts that a given value is a number and is greater than or equal to 0.

      Throws an Exception if the value is less than 0 or NaN.

  • Functions:

    • negative(value: number): number — returns the negated value of a given number. If the given number is 0, returns 0.

    • sign(value: number): Sign | null — returns 1 if given a positive number, -1 if given a negative number. Returns null if given 0 or -0. Throws Exception if the given value is NaN.

  • Number methods:

    • exponential(fraction: Digits): Unary<number, string> — creates a function that returns the input number in exponential notation rounded to a given number of fraction digits.

    • fixed(fraction: Digits): Unary<number, string> — creates a function that returns the input number in fixed-point notation with a given number of fraction digits.

    • precision(precision: Precision): Unary<number, string> — creates a function that returns the input number in fixed-point or exponential notation rounded to a given precision.

      In JavaScript, a Number only keeps 17 decimal places of precision, while Digit is an integer from 0 to 20, and Precision is an integer from 1 to 21. So passing digits or precision over 15 requires careful consideration and thorough testing.

  • Algebraic functions:

    • cubeRoot(value: number): number — returns the cube root of a given number. Throws Exception if the given value is NaN.

    • l2norm(…​values: number[]): number

      l2norm(values: number[]): number

      — returns the L2 norm (Euclidean norm) of a list of numbers. Throws Exception if any of the given values is NaN.

    • power(base: number, exponent: number): number

      power([base, exponent]: [number, number]): number

      power(base: number): (exponent: number) ⇒ number

      — returns the result of raising a base to a given exponent. Returns -1 or 1 if the base is -1 or 1 and exponent is Infinity (overrides the default Math.pow() behavior to match IEEE 754). Throws Exception if the base or exponent is NaN.

    • powerOf(exponent: number): (base: number) ⇒ number — returns a function that raises a given base to the specified exponent. Throws Exception if the exponent is NaN.

    • squareRoot(value: PositiveNumber): number — returns the square root of a given non-negative number. Throws Exception if the given value is NaN or a negative number.

  • Arithmetic functions:

    • sum(augend: number, addend: number): number — returns the result of addition of the given numbers.

    • difference(minuend: number, subtrahend: number): number — returns the result of subtraction of a given subtrahend from a given minuend.

    • product(multiplier: number, multiplicand: number): number — returns the result of multiplication of given numbers.

    • quotient(dividend: number, divisor: number): number — returns the result of division of a given dividend by a given divisor.

    • remainder(dividend: number, divisor: number): number. — returns the remainder of division of a given dividend by a given divisor.

    • absolute(value: number): number — returns the absolute value of a given number.

      Throws Exception if the given value is NaN.

  • Exponential functions:

    • exp(value: number): NonNegativeNumber — returns Euler’s number e raised to the power of the given number. Throws Exception if the given value is NaN.

    • expm1(value: number): number — returns Euler’s number e raised to the power of the given number minus 1. Throws Exception if the given value is NaN.

  • Logarithmic functions:

    • log(value: number): number — returns the natural logarithm (base e) of a given non-negative number. Throws Exception if the given value is NaN or less than zero.

    • log10(value: number): number — returns the common (base 10) logarithm of a given non-negative number. Throws Exception if the given value is NaN or less than zero.

    • log1p(value: number): number — returns the natural logarithm (base e) of 1 plus a given number. Throws Exception if the given value is NaN or less than -1.

    • log2(value: number): number — returns the binary (base 2) logarithm of a given non-negative number. Throws Exception if the given value is NaN or less than zero.

  • Rounding functions:

    • round(value: number): number — rounds a floating-point number to the nearest integer. Returns Infinity if the given value is Infinity. Returns -Infinity if the given value is -Infinity. Throws Exception if the given value is NaN.

    • roundedUp(value: number): number — returns the smallest integer greater than or equal to a given number. Returns Infinity if the given value is Infinity. Returns -Infinity if the given value is -Infinity. Throws Exception if the given value is NaN.

    • roundedDown(value: number): number — returns the largest integer less than or equal to a given number. Returns Infinity if the given value is Infinity. Returns -Infinity if the given value is -Infinity. Throws Exception if the given value is NaN.

    • roundedToFloat32(value: number): number — returns the nearest 32-bit single precision float representation of a given number. Throws Exception if the given value is NaN.

    • truncated(value: number): number — returns the integer part of a floating-point number. Throws Exception if the given value is NaN.

  • Trigonometry types and functions:

    • Radians is a nominal type for a radians value (number).

    • arccos(cosine: number): Radians | null — returns the inverse cosine [0, π] of the given cosine value [-1, 1].

      Throws Exception if the given cosine is less than -1, greater than 1, or is NaN.

    • arccosh(value: number): NonNegativeNumber | null — returns the inverse hyperbolic cosine [0, +∞) of a given number from [1, +∞).

      Throws Exception if the given value is less than 1 or is NaN.

    • arcsin(sine: number): Radians | null — returns the inverse sine [-π/2, π/2] of the given sine value [-1, 1].

      Throws Exception if the given sine is less than -1, greater than 1 or is NaN.

    • arcsinh(value: number): number — returns the inverse hyperbolic sine (-∞, +∞) of a given number from (-∞, +∞). Throws Exception if the given value is NaN.

    • arctan(value: number): Radians — returns the inverse tangent [-π/2, π/2] of a given value from (-∞, +∞).

      Throws Exception if the given value is NaN.

    • arctan2(y: number, x: number): Radians

      arctan2([y, x]: [number, number]): Radians — returns the angle in radians [-π, π] between the positive x-axis and the ray from (0, 0) to the point (x, y).

      Throws Exception if either y or x is NaN.

    • arctanh(value: number): number — returns the inverse hyperbolic tangent (-∞, +∞) of a given number from (-1, 1).

      Throws Exception if the given value is less than or equal to -1, greater than or equal to 1, or is NaN.

    • cos(angle: Radians): number — returns the cosine [-1, 1] of a given angle in radians.

      Throws Exception if the given angle is NaN or Infinity.

    • cosh(value: number): number — returns the hyperbolic cosine [1, +∞) of a given number.

      Throws Exception if the given value is NaN.

    • sin(angle: Radians): number — returns the sine [-1, 1] of a given angle in radians.

      Throws Exception if the given angle is NaN or Infinity.

    • sinh(value: number): number — returns the hyperbolic sine (-∞, +∞) of a given number.

      Throws Exception if the given value is NaN.

    • tan(angle: number): number — returns the tangent (-∞, +∞) of a given angle in radians.

      Throws Exception if the given angle is NaN or Infinity.

    • tanh(value: number): number — returns the hyperbolic tangent (-1, 1) of a given number.

      Returns 1 if the given value is Infinity.

      Returns -1 if the given value is -Infinity.

      Throws Exception if the given value is NaN.

  • Set functions:

    • maximum(values: readonly number[]): number | null — returns the largest of given numbers (ignores NaN). If the given values array is empty or contains only NaN, returns null. Use this function instead of Math.max, as it returns Infinity or NaN for edge cases.

    • minimum(values: readonly number[]): number | null — returns the smallest of given numbers (ignores NaN). If the given values array is empty or contains only NaN, returns null. Use this function instead of Math.min, as it returns Infinity or NaN for edge cases.

Integer

  • Nominal types (aliases of number):

    • Integer — a positive natural number, zero, and negative integer number.

    • SafeInteger — integers from -(2^53 - 1) to 2^53 - 1, inclusive.

    • PositiveInteger — an integer that is greater than or equal to 0.

    • NonNegativeInteger — an integer that is greater than 0.

    • NonPositiveInteger — an integer that is less than or equal to 0.

    • NegativeInteger. — an integer that is less than 0.

  • Predicates:

    • isInteger(value: number): value is Integer — returns true if a given number is an integer.

    • isSafeInteger(value: number): value is SafeInteger — returns true if a given number is from -(2^53 - 1) to 2^53 - 1, inclusive.

    • isNonNegativeInteger(value: number): value is NonNegativeInteger — returns true if a given number is an integer and is greater than or equal to 0.

    • isPositiveInteger(value: number): value is PositiveInteger — returns true if a given number is an integer and is greater than 0.

    • isNonPositiveInteger(value: number): value is NonPositiveInteger — returns true if a given number is an integer and is less than or equal to 0.

    • isNegativeInteger(value: number): value is NegativeInteger — returns true if a given number is an integer and is less than 0.

Natural

  • Nominal types (aliases of number):

    • Natural — a non-negative integer, according to the ISO 80000-2.

  • Type Guards:

    • isNatural<T>(value: number | T): value is Natural. — returns true if a given number is a non-negative integer.

Infinity

  • Nominal types:

    • Infinity — either a PositiveInfinity or NegativeInfinity.

    • PositiveInfinity — an alias for a number signifying Number.POSITIVE_INFINITY.

    • NegativeInfinity — an alias for a number signifying Number.NEGATIVE_INFINITY.

    • FiniteNumber — a number that is not Infinity, nor NaN.

  • Type guards:

    • isInfinity(value: number): value is Infinity — returns true if the given value is an Infinity.

    • isFinite(value: number): value is FiniteNumber — returns true if the given value is not Infinity and is not NaN.

  • Assertions:

    • assertIsFinite(value: number): asserts value is FiniteNumber

      assertIsFinite(name: string, value: number): asserts value is FiniteNumber

      — asserts that the given value is a finite number (not NaN and not Infinity).

      Throws Exception if the given value is NaN or positive/negative Infinity.

Base (Radix)

The parseFloat(), parseInt(), and Number.prototype.toString() functions are combined into polymorphic shortcuts for readability and avoiding NaN.

  • decimal(value: number): string — returns a string representing a specified number in decimal notation (base 10).

  • decimal(value: string): number | null — returns a number parsed from a given string in decimal notation (base 10). If the string cannot be parsed, returns null.

  • binary(value: Integer): string — returns a string representing a specified integer in binary notation (base 2).

  • binary(value: string): Integer | null — Returns an integer number parsed from a given string in binary notation (base 2). If the string cannot be parsed, returns null.

  • octal(value: Integer): string — returns a string representing a specified integer in octal notation (base 8).

  • octal(value: string): Integer | null — returns an integer number parsed from a given string in octal notation (base 8). If the string cannot be parsed, returns null.

  • hexadecimal(value: Integer): string — returns a string representing a specified integer in hexadecimal notation (base 16).

  • hexadecimal(value: string): Integer | null — returns an integer number parsed from a given string in hexadecimal notation (base 16). If the string cannot be parsed, returns null.

When any of these functions is passed as a parameter to a generic function or method, TypeScript does not recognize the polymorphic (string): number signature. It requires to pass the number as a type parameter explicitly.

import { just } from '@perfective/common/maybe';
import { decimal } from '@perfective/common/number';

just('3.14').to(decimal) === just(3.14); (1)
just(3.14).to<string>(decimal) === just('3.14'); (2)
1 The (number): string signature is recognized, and to() method does not require a type parameter.
2 The (string): number signature is not picked by the compiler and to() method requires a type parameter to compile.

Order

  • Predicates:

    • isEqualTo(value: number): Predicate<number> — creates a function that returns true if the input number is equal to a given value.

    • isNotEqualTo(value: number): Predicate<number> — creates a function that returns true if the input number is not equal to a given value.

    • isGreaterThan(value: number): Predicate<number> — creates a function that returns true if the input number is greater than a given value.

    • isGreaterThanOrEqualTo(value: number): Predicate<number> — creates a function that returns true if the input number is greater than or equal to a given value.

    • isLessThan(value: number): Predicate<number> — creates a function that returns true if the input number is less than a given value.

    • isLessThanOrEqualTo(value: number): Predicate<number> — creates a function that returns true if the input number is less than or equal to a given value.

  • Sorting:

    • ascending(a: number, b: number): number — returns a negative number if the first argument is less than the second argument.

      Can be used as a callback for the Array.prototype.sort() method to sort numbers in ascending order.

    • descending(a: number, b: number): number — Returns a positive number if the first argument is greater than the second argument.

      Can be used as a callback for the Array.prototype.sort() method to sort numbers in descending order.

Interval

  • Type:

  • Constructors:

    • interval(min: number, max: number): Interval | null — creates an Interval from given min and max numbers. If min is greater than max, returns null.

    • intervalFromPair(pair: readonly [number, number]): Interval | null — creates an Interval from a given pair of numbers, where the first number is min and the second is max. If min is greater than max, returns null.

    • intervalFromValues(values: number[]): Interval | null — creates an Interval from the minimum and maximum numbers in a given array of numbers. If the given array is empty, returns null.

    • intervalFromNullable(min: number | null, max: number | null): Interval | null — creates an Interval from the given min and max numbers, which can be null. If the min is null, the interval will have a minimum of -∞. If the max is null, the interval will have a maximum of +∞.

  • Predicates:

    • isInInterval(interval: Interval): Predicate<number> — creates a predicate that checks returns true if the input number is greater than or equal to the given interval minimum, or is less than or equal the given interval maximum.

    • isInOpenInterval(interval: Interval): Predicate<number> — creates a predicate that checks returns true if the input number is greater than the given interval minimum, or is less than the given interval maximum.

    • isInLeftOpenInterval(interval: Interval): Predicate<number> — creates a predicate that checks returns true if the input number is greater than the given interval minimum, or is less than or equal the given interval maximum.

    • isInRightOpenInterval(interval: Interval): Predicate<number> — creates a predicate that checks returns true if the input number is greater than or equal to the given interval minimum, or is less than the given interval maximum.

Bitmasks

  • Types:

    • Flags<T extends number = number> — an enum object with a list of available bitmask flags.

    • Flag<T extends Flags> — a bitmask flag defined in a given Flags enum.

    • Bitmask<T extends Flags | number = number> — a bitmask consisting of one or more Flags.

  • Constructor:

    • bitmask<T extends Flags | number = number>(flags: Bitmask<T>[]): Bitmask — creates a bitmask with all given flags raised.

  • Predicates:

    • isFlagOn<T extends Flags | number>(bitmask: Bitmask<T>, flag: Bitmask<T>): boolean — returns true if a given flag is raised on a bitmask.

    • hasFlagOn<T extends Flags | number>(flag: Bitmask<T>): Unary<Bitmask<T>, boolean> — creates a function that returns true if a given flag is raised in the input bitmask.

  • Functions:

    • raisedFlags<T extends number>(type: object, bitmask: Bitmask<T>): Member<T>[] — returns flags that are raised on the given bitmask.

Object

The @perfective/common/object package provides functions to work with the standard JS Object class.

  • Types:

    • ObjectWithDefined<T, K extends keyof T> — an object of type T with a defined value of property K.

    • ObjectWithUndefined<T, K extends keyof T> — an object of type T with an undefined value of property K.

    • ObjectWithNotNull<T, K extends keyof T> — an object of type T with a non-null value of property K.

    • ObjectWithNull<T, K extends keyof T> — an object of type T with a null value of property K.

    • ObjectWithPresent<T, K extends keyof T> — an object of type T with a present value of property K.

    • ObjectWithAbsent<T, K extends keyof T> — an object of type T with an absent value of property K.

    • RecursivePartial<T> — a generic type that recursively marks all properties of a given type T as optional.

    • Entry<K = string, V = unknown> — a key-value pair (array).

  • Constructors:

    • recordFromArray(array: string[]): Record<string, number> — creates an object from a given array with the array values as keys and their indexes as values.

    • recordFromEntries(entries: Entry[]): Record<string, unknown> — creates an object from a given array of entries. An inverse for Object.entries().

    • pick<T, K extends keyof T>(record: NonNullable<T>, …​properties: readonly K[]): Pick<T, K> — creates a copy of a given record only with given properties.

    • recordWithPicked<T, K extends keyof T>(…​properties: readonly K[]): Unary<NonNullable<T>, Pick<T, K>> — creates a function to pick() given properties from its argument.

    • omit<T, K extends keyof T>(record: NonNullable<T>, …​properties: readonly K[]): Omit<T, K> — creates a copy of a given record without given properties.

    • recordWithOmitted<T, K extends keyof T>(…​property: readonly K[]): Unary<NonNullable<T>, Omit<T, K>> — creates a function to omit() given properties from its argument.

    • filter<T, K extends keyof T>(record: NonNullable<T>, condition: Predicate<T[K]>): Partial<T> — creates a copy of a given record only with properties that meet a given condition.

    • recordFiltered<T, K extends keyof T = keyof T>(condition: Predicate<T[K]>): Unary<NonNullable<T>, Partial<T>> — creates a function to filter() properties that satisfy a given condition from its argument.

    • assigned<T, V = Partial<T>>(value: T, …​overrides: (V | Partial<T>)[]): T & V — creates a shallow copy of the given value with the given overrides.

    • function recordWithAssigned<T, V = Partial<T>>(…​overrides: (V | Partial<T>)[]): (value: T) ⇒ T & V — creates a function to assign given overrides to its argument.

  • Type guards:

    • hasDefinedProperty<T, K extends keyof T>(property: K, …​properties: readonly K[]): (value: T) ⇒ value is ObjectWithDefined<T, K> — returns a type guard that returns true if its argument has a defined property and all given properties.

    • hasUndefinedProperty<T, K extends keyof T>(property: K, …​properties: readonly K[]): (value: T) ⇒ value is ObjectWithUndefined<T, K> — returns a type guard that returns true if its argument has an undefined property and all given properties.

    • hasNotNullProperty<T, K extends keyof T>(property: K, …​properties: readonly K[]): (value: T) ⇒ value is ObjectWithNotNull<T, K> — returns a type guard that returns true if its argument has a non-null property and all given properties.

    • hasNullProperty<T, K extends keyof T>(property: K, …​properties: readonly K[]): (value: T) ⇒ value is ObjectWithNull<T, K> — returns a type guard that returns true if its argument has a null property and all given properties.

    • hasPresentProperty<T, K extends keyof T>(property: K, …​properties: readonly K[]): (value: T) ⇒ value is ObjectWithPresent<T, K> — returns a type guard that returns true if its argument has a present property and all given properties.

    • hasAbsentProperty<T, K extends keyof T>(property: K, …​properties: readonly K[]): (value: T) ⇒ value is ObjectWithAbsent<T, K> — returns a type guard that returns true if its argument has an absent property and all given properties.

  • Predicates:

    • isObject<T>(value: T | null): boolean — returns true when the value is not null and is not a primitive.

    • isRecord<T>(value: T): boolean — returns true when the value is an object created from the Object class (not an Array, Date, etc.).

    • isEmpty<T>(value: T): boolean — returns true when the value is falsy, an empty array or a Record without properties.

    • hasMethod(value: unknown, method: string): boolean — returns true when a given value implements a given method.

    • hasNoMethod(value: unknown, method: string): boolean — returns true when a given value implements a given method.

  • Reducers:

    • toRecordFromEntries(record: Record<string, unknown>, value: Entry): Record<string, unknown> — a reducer to build a record from entries.

  • Property functions:

    • property<T, K extends keyof T>(property: K): Unary<T, T[K]> — creates a function that for a given value returns the value of a given property.

    • property<T, K extends keyof T>(property: K, condition: Predicate<T[K]>): Predicate<T> — creates a predicate that for a given value returns true if a given property satisfies a given condition.

    • by<T, K extends keyof T>(property: K, order: Compare<T[K]>): Compare<T> — returns a function to compare two objects by their property with a given order callback.

Input

One of the challenges in API development is declaring type of requests (inputs). On the client side these types need to be as strict as possible (e.g., all fields that are required must be marked as required). On the server side the same type need to be treated as completely unknown, unvalidated data. If the type is written for the client side, compiler will not be able to enforce any checks on the server side. At the same time, server side cannot just use the plain unknown type, as any access to properties will be prohibited (with strict compiler settings).

To resolve this issue, the Input<T> type is introduced. It recursively adds unknown, null, and undefined to the type of every field or value. That allows to enforce validation of input data, while declaring the original type T can be declared in strict mode for the client side.

Validation functions and additional input types cover default JSON types: object, array, string, number, boolean, and null.

  • Types:

    • Input<T>

    • InputArray<T>

    • InputObject<T>

    • InputPrimitive<T>

  • Unit function:

    • input<T>(input: unknown): Input<T> — type cast to Input<T>.

  • Basic validation functions:

    • stringInput(input: Input<string>): string | undefined

    • numberInput(input: Input<number>): number | undefined

    • booleanInput(input: Input<boolean>): boolean | undefined

    • arrayInput<T>(input: Input<T[]>): Input<T>[] | undefined — checks that the input is an array and returns it as an array of unvalidated elements.

    • objectInput<T>(input: Input<T>): InputObject<T> | undefined — checks that the input is a non-null, non-array object, and returns it as an object with unvalidated properties.

    • nullInput(input: Input<null>): null | undefined.

Use Maybe chain to validate inputs
import { panic } from '@perfective/common/error';
import { maybe } from '@perfective/common/maybe';
import { isNatural, Natural } from '@perfective/common/number';
import { Input, InputObject, numberInput, objectInput } from '@perfective/common/object';

interface ExampleParams {
    id: number;
}

interface Example {
    params: ExampleParams;
}

function userId(request: Input<Example>): Natural {
    return maybe(request) (1)
        .to<InputObject<Example>>(objectInput) (2)
        .pick('params')
        .to<InputObject<ExampleParams>>(objectInput)
        .pick('id')
        .to(numberInput) (3)
        .otherwise(panic('User ID is not defined'))
        .that(isNatural) (4)
        .or(panic('User ID is invalid'));
}
1 request may be undefined.
2 At the moment type transformations are not inferred correctly, so explicit type need to provided for objectInput.
3 Last validation of the input structure.
4 Final validation of the input, specific for the function.

A custom validation monad may be added later to allow "collecting" all validation errors and warnings.

Enum

  • Types:

    • Enum<T extends number | string> — An Object with string keys and string or number values as generated by the TypeScript for an enum definition.

    • Member<T extends number | string> — key of an enum. — Defines a type of the keys of an Enum.

  • Functions:

    • members<T extends number | string, E extends Enum<T>>(value: E): Member<T>[] — returns a list of an enum keys.

Promise

The @perfective/common/promise package provides functions to work with the Promise class.

Reference

Types

  • Resolvable<T> = T | PromiseLike<T> — a value that can be passed into Promise.resolve.

  • Resolve<T> = (value: Resolvable<T>) ⇒ void — a function that can called to resolve a Promise value.

  • Reject<E extends Error = Error> = (reason?: E) ⇒ void — a function called to reject a Promise with an optional reason.

    This type is stricter than the default type of the reject callback, as it requires an Error as a reason.

  • Executor<T, E extends Error = Error> = (resolve: Resolve<T>, reject: Reject<E>) ⇒ void — a callback passed into the Promise constructor.

  • OnFulfilled<T, U = T> = (value: T) ⇒ Resolvable<U> — a onFulfilled callback passed into the Promise.then() method.

  • OnRejected<T = never> = (reason: unknown) ⇒ Resolvable<T> — a onRejected passed into the Promise.then() or Promise.catch() methods.

  • Callback<T, E extends Error = Error> = (error: E | null | undefined, value: T) ⇒ void — an error-first callback.

Constructors

  • promise<T, E extends Error = Error>(executor: Executor<T, E>): Promise<T> — creates a new Promise with a given executor callback.

  • fulfilled<T>(value: Resolvable<T>): Promise<Awaited<T>> — creates a fulfilled Promise (a shortcut for the Promise.resolve() function).

    Using Promise.resolve() directly causes the @typescript-eslint/unbound-method linting error and a TS compiler error: TS2322: Type 'unknown' is not assignable to type 'T'.

  • rejected<T = never>(reason: Error): Promise<Awaited<T>> — creates a rejected Promise (a shortcut for the Promise.rejected() function).

    Using Promise.rejected() directly causes the @typescript-eslint/unbound-method linting error.

  • settled<T>(): BiFold<Resolvable<T>, Error, Promise<Awaited<T>>> — creates a BiFold pair of callbacks to wrap a value into a Promise.

  • settlement<T, E extends Error = Error>(resolve: Resolve<T>, reject: Reject<E>): Callback<T, E> — create a Callback that uses given resolve and reject functions from an executor to settle a Promise. Use settlement to promisify functions written in the error-first callback style.

Result

The @perfective/common/result package provides a Result type implementation.

The Result type represents a result of a function that can be a Success or a Failure (an Error). It provides a mechanism for "checked exceptions" and allows to avoid try-catch blocks and unchecked JavaScript errors.

Using Result with Promise

You can use Result<T> to handle synchronous parts of Promise chains.

import { error, throws } from '@perfective/common/error';
import { Unary } from '@perfective/common/function';
import { failure, promisedResult, Result, settledResult, success } from '@perfective/common/result';
import { isEmpty } from '@perfective/common/string';

interface Request {
    method: string;
    url: string;
}

function validInput(id: string): string {
    if (isEmpty(id)) {
        return throws(error('Input id is empty'));
    }
    return id;
}

function apiRequest(method: string): Unary<string, Request> {
    return (id: string): Request => ({
        method,
        url: `/entity/${id}`,
    });
}

async function apiResponse(request: Request): string {
    if (request.method === 'HEAD') {
        return '501 Not Implemented';
    }
    return '200 OK';
}

async function entityById(inputId: Promise<string>): Promise<string> {
    return inputId
        .then(validInput)
        .then(apiRequest('HEAD')) (1)
        .then(apiResponse);
}

async function entityById(inputId: Promise<string>): Promise<string> {
    return promisedResult(inputId) (2)
        .then(headApiRequest) (3)
        .then(settledResult) (4)
        .then(apiResponse);
}

async function headApiRequest(inputId: Result<string>): Result<Request> {
    return inputId.onto(validId).to(apiRequest('HEAD'));
}

function validId(id: string): Result<string> {
    if (isEmpty(id)) {
        return failure(error('Input id is empty'));
    }
    return success(id);
}
1 Even if the inputId is a Promise, and the apiResponse is an asynchronous function, the validInput and apiRequest functions are synchronous.
2 Use promisedResult to wrap Promise resolved value or rejection into a Result.
3 The result is transformed using a synchronous headApiRequest function.
4 Use settledResult to unpack a Result into a settled Promise. In this case, if the value is a Success, a fulfilled Promise is returned. But if the value is a Failure, then a Promise is rejected.

Reference

Types

  • BiMapResult<T, U> = BiMap<T, U, Error, Error> — a pair of callbacks for the Result.to() bifunctor.

  • BiFoldResult<T, U> = Bifold<T, Error, U> — a pair of callbacks for the Result.into() method argument.

Functions

  • success<T>(value: T): Success<T> — creates a Success object from a given value. It is a unit function for the Result monad.

    An Error type can also be used as a Success value.

    For example, when an API returns an error code and this error code is parsed and matched to a specific Error subtype.

  • successFrom<T, U>(map: (value: T) ⇒ U): Unary<T, Success<U>> — creates a function to transform a value with a given map callback and return the result as a Success.

  • failure<T>(error: Error): Failure<T> — creates a Failure object from a given error.

    Throws a TypeError, if the given error value is not an Error object.

  • failureFrom<T>(map: (value: T) ⇒ Error): Unary<T, Failure<T>> — creates a function to transform a value into an Error with a given map callback and return the result as a Failure.

  • rejection<T = never>(reason: unknown): Failure<T> — creates a Failure from an unknown reason. Pass rejection into Promise.catch() or Promise.then() as an onRejected callback to wrap a reason into a Failure.

  • result<T>(value: T | Error): Result<T> — creates a Failure if a given value is an Error, otherwise creates a Success.

    TypeScript compiler does not exclude an Error type from the T automatically.

    If you pass a value of type T | Error to the result(), the return type will be Success<T | Error>.

    You have to cast the type parameter manually (e.g., result<string>(…​)) to get the return type as Result<T>.

  • resultOf<T>(callback: Nullary<T>): Result<T> — calls a given callback in a try-catch block. If the callback throws an error, catches the error and returns it as a Failure. Otherwise, returns the result of the callback as a Success.

    Use the resultOf to wrap up unsafe functions into a Result.

  • resultFrom<T, U>(map: (value: T) ⇒ U): Unary<T, Result<U>> — creates a function to try transforming a value with a given map callback and return it as a Success. If the map callback throws, returns a Failure.

  • promisedResult<T>(promise: Promise<T>): Promise<Result<T>> — wraps a Promise value into a Result.

  • settledResult<T>(result: Result<T>): Promise<T> — creates a settled Promise from a given Result.

  • successWith<T, U>(map: Unary<T, U>): BiMapResult<T, U> — creates a BiMapResult pair with a given map callback as the first element and an identity function as the second element.

  • failureWith<T>(map: Unary<Error, Error>): BiMapResult<T, T> — creates a BiMapResult pair with a given map callback as the second element and an identity function as the first element.

Type guards

  • isResult<T, U>(value: Result<T> | U): value is Result<T> — returns true if a given value is a Result.

    • isNotResult<T, U>(value: Result<T> | U): value is U — returns true if a given value is not a Result.

  • isSuccess<T, U>(value: Success<T> | U): value is Success<T> — returns true if a given value is a Success.

    • isNotSuccess<T, U>(value: Success<T> | U): value is U — returns true if a given value is not a Success.

  • isFailure<T, U>(value: Failure<T> | U): value is Failure<T> — returns true if a given value is a Failure.

    • isNotFailure<T, U>(value: Failure<T> | U): value is U — returns true if a given value is not a Failure.

Result.onto()

  • Result.onto<U>(flatMap: (value: T) ⇒ Result<U>): Result<U>:

    • for a Success, applies a given flatMap callback to the Success.value and returns the result;

    • for a Failure, ignores the flatMap callback and returns the same Failure.

  • Lifts:

    • onto<T, U>(value: Unary<T, Result<U>>): Unary<Result<T>, Result<U>> — creates a function to apply a given value callback to the Result.onto() method and return the result of the value.

    • onto<T, U>(value: Unary<T, Failure<U>>): Unary<Result<T>, Failure<U>> — creates a function to apply a given value callback to the Result.onto() method and return the result of the value (a Failure).

import { error, typeError } from '@perfective/common/error';
import { Unary } from '@perfective/common/function';
import { Result, failure, success } from '@perfective/common/result';
import { isEmpty } from '@perfective/common/string';

interface Request {
    method: string;
    url: string;
}

interface Response {
    status: string;
}

function validInput(id: string): Result<string> {
    if (isEmpty(id)) {
        return failure(typeError('Input id is empty'));
    }
    return success(id);
}

function apiRequest(method: string): Unary<string, Result<Request>> {
    return (id: string): Result<Request> => success({
        method,
        url: `/entity/${id}`,
    });
}

function apiResponse(request: Request): Result<Response> {
    if (request.method === 'HEAD') {
        return failure(error('Not implemented'));
    }
    return success({
        status: '200 OK',
    });
}

validInput('abc')
    .onto(apiRequest('GET'))
    .onto(apiResponse)
    .value == { status: '200 OK' }; (1)

validInput('abc')
    .onto(apiRequest('HEAD'))
    .onto(apiResponse)
    .value == error('Not implemented'); (2)

validInput('')
    .onto(apiRequest('HEAD'))
    .onto(apiResponse)
    .value == typeError('Input id is empty'); (3)
1 When we have a valid id and "send" a GET request, then the whole chain succeeds.
2 When we have a valid id but "send" a HEAD request, the apiResponse fails with an Error.
3 When we have an invalid id, neither the apiRequest nor apiResponse callbacks are called.

So even as a HEAD request, it would fail with the earliest error in the chain (from validInput).

Result.to()

  • Result.to<U>(map: Unary<T, U>): Result<U>:

    • For a Success, applies a given map callback to the Success.value and returns the result;

    • For a Failure, ignores the map callback and returns the same Failure.

  • Result.to<U>(mapValue: Unary<T, U>, mapError: Unary<Error, Error>): Result<U>

    • For a Success, applies a given mapValue callback to the Success.value and returns the result as a Success;

    • For a Failure, applies a given mapError callback to the Failure.value and returns the result as s Failure.

      This method can be used to track occurred failures occurred in a previous step by chaining them together using the mapError.

  • Result.to<U>(maps: BiMapResult<T, U>): Result<U>:

    • For a Success, applies the first callback of a given maps pair to the Success.value and returns its result wrapped as a Success.

    • For a Failure, applied the second callback of a given maps pair to the Failure.value and returns its result wrapped as a Failure.

      This method allows to use a pair of mapValue and mapError functions created dynamically.

      You can also use it with the failureWith function to only transform a Failure.value.

  • Lifts:

    • to<T, U>(value: Unary<T, U>, error?: Unary<Error, Error>): Unary<Result<T>, Result<U>> — creates a function to apply given value and error callbacks to the Result.to() method and return the result.

    • to<T, U>(maps: BiMapResult<T, U>): Unary<Result<T>, Result<U>> — creates a function to apply a given maps callbacks pair to the Result.to() method and return the result.

import { error, typeError } from '@perfective/common/error';
import { Unary } from '@perfective/common/function';
import { Result, failure, success } from '@perfective/common/result';
import { isEmpty } from '@perfective/common/string';

interface Request {
    method: string;
    url: string;
}

interface Response {
    status: string;
    url: string;
}

function validInput(id: string): Result<string> {
    if (isEmpty(id)) {
        return failure(typeError('Input id is empty'));
    }
    return success(id);
}

function apiRequest(method: string): Unary<string, Request> {
    return (id: string): Request => ({
        method,
        url: `/entity/${id}`,
    });
}

function apiResponse(request: Request): Response {
    return {
        status: '200 OK',
        url: request.url,
    };
}

validInput('abc')
    .to(apiRequest('GET'))
    .to(apiResponse) (1)
    .value == { status: '200 OK' }; (2)

validInput('')
    .to(apiRequest('GET'))
    .to(apiResponse)
    .value == typeError('Input id is empty'); (3)
1 Both apiRequest and apiResponse transform a given value into a new one. Result.to wraps them into the next Success.
2 When we have a valid id, then the whole chain succeeds.
3 When we have an invalid id, neither apiRequest nor apiResponse callbacks are called. So the result is the TypeError returned by the validInput.
Using Result.to with the failureWith() function to only transform a Failure.value.
import { chained, typeError } from '@perfective/common/error';
import { failure, failureWith, Result, success } from '@perfective/common/result';
import { isEmpty } from '@perfective/common/string';

function validInput(id: string): Result<string> {
    if (isEmpty(id)) {
        return failure(typeError('Input id is empty'));
    }
    return success(id);
}

function entityByIdRequest(id: string): Result<Request> {
    return validInput(id)
        .to(failureWith(chained('Entity ID {{id}} is invalid' { (1)
            id,
        })))
        .to(apiRequest('GET'));
}
1 You can also combine both callbacks into one Result.to(apiRequest(…​), chained(…​)) call.
The following calls are equivalent
Result.to([mapValue, mapError]) === Result.to(mapValue, mapError);
Result.to([mapValue, mapError]) === Result.to(successWith(mapValue)).to(failureWith(mapError));
Result.to(successWith(mapValue)) === Result.to(mapValue);
Result.to(successWith(mapValue)) === Result.to(mapValue, same);
Result.to(failureWith(mapError)) === Result.to(same, mapError);

Result.into()

  • Result.into<U>(fold: BiFoldResult<T, U>): U:

    • For a Success, applies the first callback of the fold pair to the Success.value and returns the result.

    • For a Failure, applies the second callback of the fold pair to the Failure.value and returns the result.

      Result.into(fold) allows to pass a pair of reduce callbacks dynamically as one argument.

  • Result.into<U>(reduceValue: Unary<T, U>, reduceError: Unary<Error, U>): U:

    • For a Success applies a given reduceValue to the Success.value,

    • For a Failure applies a given reduceError to the Failure.value (Error).

    Result.into(reduceValue, reduceError) separates handling of the Success.value and Failure.value. It is especially useful when the Success.value is an Error. As in this case, the Result.into(reduce) call may not be able to distinguish between a Success.value Error and a Failure.value Error.

  • Lifts:

    • into<T, U>(value: Unary<T, U>, error: Unary<Error, U>): Unary<Result<T>, U> — creates a function to apply given value and error callbacks to the Result.into() method and return the result.

    • into<T, U>(fold: BiFoldResult<T, U>): Unary<Result<T>, U> — creates a function to apply a given fold callbacks pair to the Result.into() method and return the result.

Using the Result.into() method to switch to a Promise.
import { typeError } from '@perfective/common/error';
import { Unary } from '@perfective/common/function';
import { rejected } from '@perfective/common/promise';
import { failure, Result, success } from '@perfective/common/result';
import { isEmpty } from '@perfective/common/string';

interface Request {
    method: string;
    url: string;
}

function validInput(id: string): Result<string> {
    if (isEmpty(id)) {
        return failure(typeError('Input id is empty'));
    }
    return success(id);
}

function apiRequest(method: string): Unary<string, Request> {
    return (id: string): Request => ({
        method,
        url: `/entity/${id}`,
    });
}

async function apiResponse(request: Request): Promise<string> {
    if (request.method === 'HEAD') {
        return '501 Not Implemented';
    }
    return '200 OK';
}

async function entityById(id: string): Promise<string> {
    return validInput('') (1)
        .otherwise('abc') (2)
        .to(apiRequest('HEAD')) (3)
        .into(apiResponse) (4)
        .catch(() => '503 Service Unavailable'); (5)
}
1 The id input is not valid.
2 Fallback to abc as a valid id.
3 The Result.otherwise() always returns a Success, so the whole chain now is strictly a Success.
4 When we have a Request, we use Result.into() to switch into the apiResponse Promise.
5 Now we have a Promise chain and can continue computation.

Result.that()

  • Result.that(filter: Predicate<T>, error: Value<Error>): Result<T>

    • For a Success, if the value satisfies a given filter, returns itself. Otherwise, returns a Failure with a given error.

    • For a Failure, ignores both arguments and returns itself.

  • Result.that(filter: Predicate<T>, message: Value<string>): Result<T>

    • For a Success, if the value satisfies a given filter, returns itself. Otherwise, returns a Failure with an Exception created with a given message, a {{value}} token created from the Success.value, and the Success.value passed into the ExceptionContext.

    • For a Failure, ignores both arguments and returns itself.

  • Lifts:

    • that<T>(filter: Predicate<T>, error: Value<Error>): Unary<Result<T>, Result<T>> — creates a function to apply given filter predicate and error to the Result.that() method and return the result.

    • that<T>(filter: Predicate<T>, message: Value<string>): Unary<Result<T>, Result<T>> — creates a function to apply given filter predicate and message to the Result.that() method and return the result.

Use Result.that() to build validation chains.
import { typeError } from '@perfective/common/error';
import { isGreaterThan, isNumber } from '@perfective/common/number';
import { Result, success } from '@perfective/common/result';
import { isNotEmpty } from '@perfective/common/string';


function validId(id: string): Result<number> { (1)
    return success(id)
        .that(isNotEmpty, typeError('Input id is empty')) (2)
        .to(Number)
        .that(isNumber, 'Failed to parse input {{value}} as a number') (3)
        .that(isGreaterThan(0), 'Input id must be greater than 0'); (4)
}
1 If the input can be parsed as a number and is greater than zero, the function returns Success with a parsed number value.
2 For an empty string, the function returns a Failure with a TypeError message Input id is empty.
3 For an input that cannot be parsed as a number, like Zero, the function returns a Failure with an Exception an a tokenized message (e.g. Failed to parse input 'Zero' as a number)
4 For an input that is not greater than 0, the function returns a Failure with an Exception with message Input id must be greater than 0.

Result.which()

  • Result.which<U extends T>(typeGuard: TypeGuard<T, U>, error: Value<Error>): Result<U>

    • For a Success, if the value satisfies a given typeGuard, returns itself with the type narrowed down by the typeGuard. Otherwise, returns a Failure with a given error.

    • For a Failure, returns itself.

  • Result.which<U extends T>(typeGuard: TypeGuard<T, U>, message: Value<string>): Result<U>

    • For a Success, if the value satisfies a given typeGuard, returns itself with the type narrowed down by the typeGuard. Otherwise, returns a Failure with an Exception created with a given message, a {{value}} token created from the Success.value, and the Success.value passed into the ExceptionContext.

  • Lifts:

    • which<T, U extends T>(typeGuard: TypeGuard<T, U>, error: Value<Error>): Unary<Result<T>, Result<U>> — creates a function to apply given typeGuard and error to the Result.which() method and return the result.

    • which<T, U extends T>(typeGuard: TypeGuard<T, U>, message: Value<string>): Unary<Result<T>, Result<U>> — creates a function to apply given typeGuard and error message to the Result.which() method and return the result.

Use Result.which() to build validation chains.
import { isNotNull } from '@perfective/common';
import { decimal } from '@perfective/common/number';
import { Result, success } from '@perfective/common/result';


function validId(id: string): Result<number> {
    return success(id)
        .to(decimal) // == Result<number | null>
        .which(isNotNull, 'Failed to parse id as a number'); (1)
}
1 The decimal() function parses a string and returns number or null (if parsing failed). Result.which() allows to narrow the type with a type guard (isNotNull), so the type becomes Result<number>.

Result.when()

  • Result.when(condition: Proposition, error: Value<Error>): Result<T>

    • For a Success, if the condition is true, returns itself. Otherwise, returns a Failure with a given error.

    • For a Failure, returns itself.

  • Result.when(condition: Proposition, message: Value<string>): Result<T>

    • For a Success, if the condition is true, returns itself. Otherwise, returns a Failure with a given message.

    • For a Failure, returns itself.

  • Lifts:

    • when<T>(condition: Proposition, error: Value<Error>): Unary<Result<T>, Result<T>> — creates a function to apply given condition and error to the Result.when() method and return the result.

    • when<T>(condition: Proposition, message: Value<string>): Unary<Result<T>, Result<T>> — creates a function to apply given condition and message to the Result.when() method and return the result.

Use Result.when() to guard based on an external condition.
import { Result } from '@perfective/common/result';

interface Book {
    isbn: string;
    title: string;
}

interface UserService {
    hasPermission: (permission: string) => boolean;
}

interface BookStore {
    byIsbn: (isbn: string) => Result<Book>;
}

class BooksService {
    public constructor(
        private readonly user: UserService,
        private readonly books: BookStore,
    ) {}

    public bookByIsbn(isbn: string): Result<Book> { (1)
        return this.books.byIsbn(isbn)
            .when(() => this.user.hasPermission('read book'), 'Access denied'); (2)
    }
}
1 The booksByIsbn() method composes UserService and BookService to load books only when an active user has permissions to read them.
2 If the hasPermissions() method returns false, then the bookByIsbn() method returns a Failure with "Access denied" Exception.

Result.otherwise()

  • Result.otherwise(recovery: Recovery<T>): Success<T>

    • For a Success, returns itself.

    • For a Failure, applies its value (Error) to a given recovery callback and returns the result wrapped into a Success.

  • Lift:

    • otherwise<T>(recovery: Recovery<T>): Unary<Result<T>, Success<T>> — creates a function to apply a given recovery callback to the Result.otherwise() method and return the result.

Use Result.otherwise() to recover from an error and continue the computation chain
import { NonNegativeInteger, PositiveInteger } from '@perfective/common/number';
import { Result } from '@perfective/common/result';

interface User {
    id: NonNegativeInteger;
    username: string;
}

interface UserStore {
    byId: (id: PositiveInteger) => Result<User>;
}

interface Log {
    error: (error: Error) => void;
}

function fallback<T>(log: Log, value: T): (error: Error) => T {
    return (error: Error): T => {
        log.error(error);
        return value;
    };
}

function anonymousUser(): User {
    return {
        id: 0,
        username: 'anonymous',
    };
}

class UserService {
    public constructor(
        private readonly users: UserStore,
        private readonly log: Log,
    ) {}

    public usernameById(id: number): string {
        return this.users.byId(id) (1)
            .otherwise(fallback(this.log, anonymousUser())) (2)
            .to(user => user.username)
            .value;
    }
}
1 UserStore.byId() returns a Result<User>, so it may return a Failure.
2 Log the failure and return a fallback, so the chain can be completed.

Result.or()

  • Result.or(recovery: Recovery<T>): T

    • For a Success, returns its own value.

    • For a Failure, applies its value (Error) to a given recovery callback and returns the result.

  • Lift:

    • or<T>(recovery: Recovery<T>): Unary<Result<T>, T> — creates a function to apply a given recovery callback to the Result.or() method and return the result.

Use Result.or() to recover from an error and return the result of computation
import { isNotNull } from '@perfective/common';
import { decimal, isGreaterThan, isInteger, NonNegativeInteger, PositiveInteger } from '@perfective/common/number';
import { Result, success } from '@perfective/common/result';

interface User {
    id: NonNegativeInteger;
    username: string;
}

interface UserStore {
    byId: (id: PositiveInteger) => Result<User>;
}

interface Log {
    error: (error: Error) => void;
}

function fallback<T>(log: Log, value: T): (error: Error) => T {
    return (error: Error): T => {
        log.error(error);
        return value;
    };
}

function validId(id: string): Result<PositiveInteger> { (1)
    return success(id)
        .to(decimal) // == Result<number | null>
        .which(isNotNull, 'Failed to parse id as a number')
        .that(isInteger, 'User ID must be an integer')
        .that(isGreaterThan(0), 'User ID must be positive');
}

function anonymousUser(): User {
    return {
        id: 0,
        username: 'anonymous',
    };
}

class UserService {
    public constructor(
        private readonly users: UserStore,
        private readonly log: Log,
    ) {}

    public userById(id: string): User {
        return validId(id)
            .onto(id => this.users.byId(id)) (2)
            .or(fallback(this.log, anonymousUser())); (3)
    }
}
1 The validId() function may return a handful of different failures.
2 The UserStore.byId() method returns a Result, so it may also return a failure.
3 In case of a failure we log the error and return an anonymous user object.

Result.through()

  • Result.through(valueProcedure: UnaryVoid<T>, errorProcedure?: UnaryVoid<Error>): this:

    • For a Success, passes the value through a given valueProcedure and returns itself.

    • For a Failure, passes the value through a given errorProcedure and returns itself.

  • Result.through(procedures: BiVoidResult<T>): this:

    • For a Success, passes the value through the first procedure in the procedures pair and returns itself.

    • For a Failure, passes the value through the second procedure in the procedures pair and returns itself.

  • Lifts:

    • through<T>(value: UnaryVoid<T>, error: UnaryVoid<Error>): Unary<Result<T>, Result<T>> — creates a function to apply given value and error callbacks to the Result.through() method and return the given Result.

    • through<T>(procedures: BiVoidResult<T>): Unary<Result<T>, Result<T>> — creates a function to apply a given procedures callbacks pair to the Result.through() method and return the given Result.

import { typeError } from '@perfective/common/error';
import { empty } from '@perfective/common/function';
import { failure, Result, success } from '@perfective/common/result';
import { isEmpty } from '@perfective/common/string';

function validInput(id: string): Result<string> {
    if (isEmpty(id)) {
        return failure(typeError('Input id is empty'));
    }
    return success(id);
}

function entityByIdRequest(id: string): Result<Request> {
    return validInput(id)
        .to(apiRequest('GET'))
        .through(empty, console.error); (1)
}
1 When we have a Success, we only pass a no-op empty function. But if we have a Failure, we log an error. Either way, the Result is the same.

Type classes

Monad

The Result<T> type is a monad that provides:

  • the Result.onto() method as a bind (>>=) operator;

  • the success() constructor as a unit (return) function.

It satisfies the monad laws:

  1. unit is a left identity for bind:

    let x: T;
    let f: (value: T) => Result<T>;
    
    success(x).onto(f) === f(x);
  2. unit is a right identity for bind:

    let ma: Result<T>;
    
    ma.onto(success) === ma;
  3. bind is associative:

    let ma: Result<T>;
    let f: (value: T) => Success<U>;
    let g: (value: U) => Success<V>;
    
    ma.onto(a => f(a).onto(g)) === ma.onto(f).onto(g);

Functor

The Result<T> type is a functor that provides:

  • the Result.to() method as a fmap function.

It satisfies the functor laws:

  1. Result.to() preserves identity morphisms:

    let id = (value: T) => value;
    let value: T;
    let error: Error;
    
    success(value).to(id) === success(id(value));
    failure(error).to(id) === failure(id(error));
  2. Result.to() preserves composition of morphisms:

    let f: (value: U) => V;
    let g: (value: T) => U;
    let value: T;
    let error: Error;
    
    success(value).to(v => f(g(v))) === success(value).to(g).to(f);
    failure(error).to(v => f(g(v))) === failure(error).to(g).to(f); (1)
    1 Failure.to() ignores the input and always returns itself.

Bifunctor

The Result<T> type is a bifunctor that provides:

  • the Result.to(maps) method as the bimap function.

  • the successWith() function as the second function.

  • the failureWIth() function as the first function.

Which ensures that:

  1. Result.to(maps) preserves identity morphisms

    let id = (value: T) => value;
    let value: T;
    let error: Error;
    
    success(value).to([id, id]) === success(id(value));
    failure(error).to([id, id]) === failure(id(error));
  2. Result.to(successWith(mapValue)) preserves identity morphisms

  3. Result.to(failureWith(mapError)) preserves identity morphisms

  4. Applying the bimap function is the same as applying the first and second functions.

    let f: (value: Error) => Error;
    let s: (value: T) => U;
    let value: T;
    let error: Error;
    
    success(value).to([s, f]) === success(value).to(successWith(s)).(failureWith(f));
    failure(error).to([s, f]) === failure(error).to(successWith(s)).(failureWith(f));

String

The @perfective/common/string package works with the standard JS String type. It provides the following functions and additional types.

  • Type guards:

    • isString<T>(value: T | string): value is string — returns true if a given value is a string.

    • isNotString<T>(value: T | string): value is T — returns true if a given value is not a string.

  • Properties:

    • length(value: string): number — returns the length of a given string.

  • Predicates:

    • isEmpty(value: string): boolean — returns true if a given string is empty.

    • isNotEmpty(value: string): boolean — returns true if a given string is not empty.

  • Operators:

    • lines(value: string): string[] — splits a given string into an array of string based on the line separator (\n, \r\n, and \r).

    • lowerCase(value: string): string — converts a given string to lower case.

    • upperCase(value: string): string — converts a given string to upper case.

    • trim(value: string): string — removes whitespace from both ends of a given string.

  • Curried functions:

    • charAt(index: number): Unary<string, string> — creates a function that returns a UTF-16 code unit at a given zero-based index in the input string.

    • concat(…​strings: string[]): Unary<string, string> — creates a function that returns a string built from the input string and concatenated with given strings.

    • concatTo(value: string): Unary<string | string[], string> — creates a function that returns a string built from the input string(s) concatenated to a given string.

    • endsWith(search: string, endPosition?: number): Unary<string, boolean> — creates a function that returns true if a given search string is found at a given endPosition index of the input string.

      If endPosition is omitted, the input string length is used.

    • includes(search: string, position: number = 0): Unary<string, boolean> — creates a function that returns true if a given search string is found in the input string starting at a given position index.

    • indexOf(search: string, from: number = 0): Unary<string, number | -1> — creates a function that returns the index of the first occurrence of a given search string in the input string, starting at a given from index; or returns -1 if the given search string is not found.

    • lastIndexOf(search: string, from?: number): Unary<string, number | -1> — creates a function that returns the index of the first occurrence of a given search string in the input string, starting at a given from index; or returns -1 if the given search string is not found.

    • padEnd(length: number, fill?: string): Unary<string, string> — creates a function that pads the of end the input string with a given fill string up to the target length.

    • padStart(length: number, fill?: string): Unary<string, string> — creates a function that pads the of start the input string with a given fill string up to the target length.

    • repeat(count: number): Unary<string, string> — creates a function that creates a string consisting of a given count of copies of the input string.

    • replace(search: string | RegExp, replacement: string): Unary<string, string> — creates a function that replaces given search substrings in the input string with a given replacement.

    • replaceWith(search: string | RegExp, replacement: Replacement): Unary<string, string> — creates a function that replaces given search substrings in the input string with a result of the replacement function (invoked on every match).

    • search(search: RegExp): Unary<string, number | -1> — creates a function that returns the index of the first occurrence of a given regular expression in the input string; or returns -1 if the given expression is not found.

    • slice(start: number, end?: number): Unary<string, string> — creates a function that returns a section of the input string from a given start index to the end of the string, or to a given end index (exclusive).

    • split(separator: string | RegExp, limit?: number): Unary<string, string[]> — creates a function that creates an ordered list of substrings by splitting the input string using a given separator and up to an optional limit.

    • startsWith(search: string, from: number = 0): Unary<string, boolean> — creates a function that returns true if the input string begins with a given search substring at a given from index.

  • Utf16CodeUnit: — an integer between 0 and 65535 (0xFFFF) representing a UTF-16 code unit.

    • charCodeAt(index: number): Unary<string, Utf16CodeUnit> — creates a function that returns a UTF-16 code unit value at a given zero-based index in the input string.

  • CodePoint: — an integer between 0 and 0x10FFFF (inclusive) representing a Unicode code point.

    • codePointAt(position: number): Unary<string, CodePoint | undefined> — creates a function that returns a code point value of the character at a given index in the input string; or returns undefined if the given index is out of range.

  • UnicodeNormalizationForm:

    • normalize(form: UnicodeNormalizationForm = 'NFC'): Unary<string, string> — creates a function that returns a string containing the Unicode Normalization Form of the input string for a given normalization form.

Format

  • Format — represents a template with tokens that can be turned into a string.

    • format(template: string, tokens: Tokens | unknown[] = {}): Format — creates a {@link Format} record with the given template and tokens.

    • formatted(input: Format): string — replaces Format.tokens in the Format.template and returns the resulting string.

      Each token is wrapped in the double curly braces. For example, a template with a token {{foo}} will be replaced by the string value of the token foo.

  • Tokens: — a mapping between a token and its string value.

    • tokens(tokens: unknown[] | Tokens): Tokens — creates Tokens record from a given array of positional tokens, where each token is an index of each value in the given array.

      If given a Tokens object returns the given object.

Roadmap