Announcing @perfective/common v0.10.0
Towards the best `Result`
After eight months of development, the new 0.10.0 version of the @perfective/common package is available.
This release has focused on three major things:
Getting the
Resultmonad on par with theMaybemonad.Introducing the new
@perfective/common/datepackage.Organizing the planning process to move towards the v1.0 release.
Minor changes and improvements were the result of the major changes.
Improved Result monad
The @perfective/common/result monad with the Result monad was introduced in the v0.9.0 release. The Result is a concrete case of the Either monad holding a Success value or a Failure error.
In v0.9.0, the Result class had only four methods:
Result.onto()as the bind operator (>>=)Result.to()as the fmap and bimap functions.Result.into()as the fold method.Result.through()to apply the value to a given procedure.
In v0.10.0, the Result class was extended with the filtering methods:
Result.that()checks theSuccessvalue with a givenPredicateand either returns the sameSuccessor aFailurewith a givenError.Result.which()works the same way as Result.that(), but allows passing a type guard and narrows the value type.Result.when()keeps the Success or switches to a Failure based on the external condition.
And with the recovery methods:
Result.or()– allows terminating theResultchain and fallback to a recovery value.Result.otherwise()— allows to fallback to a recovery value and continue theResultchain.
In this form, the Result monad achieves nearly full parity with the Maybe monad (except for the Maybe.pick() method).
Consider an HTTP endpoint implementation to load a user from a database by an unsafe user id input.
Assume you have the following functions:
export interface User {
// User data
}
/** Returns `true` if the active user has admin access. */
declare function hasAdminAccess(): boolean;
/** Builds an SQL query to load a user with a given `id`. */
declare function userByIdQuery(id: number): string;
/** Sends a given `sql` to the database and returns a User. */
declare function userQueryResult(sql: string): Promise<User>;
/** Logs a given error */
declare function logError(error: Error): void;With the new Result functionality, it is now possible to express validation and transformation of a value in one chain.
import { isNotNull } from '@perfective/common';
import { typeError } from ‘@perfective/common/error’;
import { naught } from '@perfective/common/function';
import { decimal, isNonNegativeInteger } from '@perfective/common/number';
import { rejected } from '@perfective/common/promise';
import { Result, success } from '@perfective/common/result';
import { isString } from '@perfective/common/string';
function validUserId(id: unknown): Result<number> {
return success(id)
.which(isString, typeError('Input must be a string'))
.to(decimal)
.which(isNotNull, 'Failed to parse user ID')
.that(isNonNegativeInteger, 'Invalid user ID');
}
async function userResponseById(id: unknown): Promise<User> {
return success(id)
.when(hasAdminAccess, 'Access Denied')
.onto(validUserId)
.to(userByIdQuery)
.through(naught, logError)
.into(userForQuery, rejected);
}Breaking changes
After introducing the Result.otherwise() method, the recovery() function was deprecated.
The resultFrom() function has been changed to wrap functions that may throw an error into a try-catch block and return a Success or a Failure. It allows the creation of the Result.onto() method callbacks.
Future considerations
The major question for future improvement is whether some Result methods should use the try-catch blocks and guarantee a safe return to the computation chain. The concern is a possible performance hit.
Another area of improvement is interoperability with the Promise chains. A Result is just a synchronous version of a Promise, and the ability to switch a Promise to a Result and back would be useful.
New Date package
The new @perfective/common/date package introduces several functions for the Date class and the Timestamp alias for a number.
A Date can now be instantiated using the date() constructor function. It supports Date, string, and Timestamp (number) inputs, but unlike the new Date() constructor, the date() function returns null instead of an “Invalid date” Date.
Two edge case constructors for a Date are also available: the now() and epoch() functions. The now() function uses the Date.now() static method, so the current date/time can be mocked.
A Timestamp can be extracted from a Date or parsed from a string using the timestamp() constructor function. Like the date() function, it returns null for invalid dates and unparseable inputs.
Both the date() and timestamp() functions are designed to return null in case of a failure, allowing convenient use in the Maybe chain.
Future considerations
While returning null for invalid values is acceptable, there may be better approaches. Throwing an error instead seems more reasonable, as it provides a clear cause of a failure.
But throwing an error would require extensive use of try-catch or using the Result monad (especially if it will handle the try-catch internally).
A riskier alternative is to rethink the Maybe and Result packages roles in the @perfective/common. Currently, both monads are considered high-level dependencies: they cannot be used in any other sub-packages (except for other high-level dependencies, like Match).
Reorganized roadmap
The roadmap was reorganized from scratch. It now contains all built-in JavaScript objects, their properties, methods, and a link to their earliest ECMAScript specification sections.
The reorganization goal was to document mapping between a property or a method of the built-in objects and a corresponding method(s) in the @perfective/common library.
While working on the roadmap, some general rules for the v1.0 release were described. As originally planned, the @perfective/common library targets ECMAScript 6 (aka ECMAScript 2015) with some support for the later ECMAScript versions.
For example, ECMAScript 2023 introduced the Array.prototype.toReversed() and Array.prototype.toSorted() methods. The reversed() and sorted() function already exist in the @perfective/common/array package.
At the same time, it became clear that support of the binary-focused types and functions (like TypedArray won’t be included in the v1.0. The @perfective/common library is focused on making business application code readable. Binary-focused functions are used for lower-level code and are rarely used directly in the business-level code.
Future considerations
There are only a few major features required to tag v1.0:
Internationalization/Localization support based on the built-in
Intlobjects.MapandSetsupport.
The challenge is to decide how to support both theMaybeandResultmonads forMapandSet.URIandURLsupport.
Strictly speaking, theURLclass is not a built-in object but is widely supported and can be a part of the library.An
Input(validation) monad to replace the existing input functions.
Additionally, the problem of returning null vs. throwing an error must be resolved across the whole library.
Improvements
The Panic type and the panic() function from @perfective/common/error now support cause: Error. This change allowed the deprecation of Rethrow and rethrow().
The @perfective/common/boolean package now provides the isBoolean()/isNotBoolean() type guards and the isTruthy()/isFalsy() functions.
Documentation
All exported types and functions now have a JSDoc comment, so the purpose and logic of the function can be checked in your IDE without reading the code.
TypeScript 5.3 compatibility
TypeScript v5.3 introduced a type inference change that broke @perfective/common/maybe nothing() and nil() functions. The code like:
const maybe: Maybe<number> = nothing();started to fail, as the nothing() return type was inferred as number | null | undefined instead of just number.
To ensure backward compatibility, the nothing() and nil() functions now return the Nothing<Present<T>> type.
Deprecations
The naught() function from @perfective/common/maybe is renamed into the nil() function (as the different nil() function was deprecated in v0.9.0 and is now removed).
The empty() function from @perfective/common/function is renamed into the naught() function.
The Rethrow type and rethrow() function from @perfective/common/error are deprecated in favor of the existing Panic and panic().
The unknownError() function from @perfective/common/error is renamed into the caughtError() function.
The isTruthy() and isFalsy() functions are moved from @perfective/common/object into @perfective/common/boolean.
Supported Node.js versions
Node.js 16 is no longer supported, as it is no longer an LTS version. Only Node v18 and v20 are used for the CI builds.
Removed code
Functions and types deprecated since v0.9.0 are now removed:
arrayFromArrayLike(),arrayFromIterable(),flatten()from@perfective/common/array.Match.that(),Match.to(),Statement,StatementEntry,statements, andWhen.then()from@perfective/common/match.Maybe.lift()/lift(),Maybe.run()/run(),Nullable,Nil,Only/Solum,nullable(),nil(),only()/solum(),Optional,None,Some,optional(),none(),some()from@perfective/common/maybe.Runandresult()from@perfective/common/promise.Output,isNotOutput(),isOutput(),output()from@perfective/common/string
What’s Next?
The next version will be focused on:
Finishing the Date class support (the
@perfective/common/datepackage)Introducing localization support (the
@perfective/common/i18npackage)Improving consistency of the existing packages.

