Typescript Functional Extensions
A TypeScript implementation of the C# library CSharpFunctionalExtensions, including synchronous and asynchronous Maybe
and Result
monads.
Community
Related Projects
- NestJS typescript-functional-extensions utilities (A library of utilities for working with
typescript-functional-extensions
in NestJS projects)
Influences
- fp-ts (Typed functional programming in TypeScript)
- CSharpFunctionalExtensions (A library to help write C# code in more functional way)
Further Reading
- Functional Extensions for C#
- Functors, Applicatives, And Monads In Pictures
- Railway Oriented Programming
- Modeling Missing Data - The Maybe Monad
- Handling Failures - The Result Monad
How to Use
npm
npm i typescript-functional-extensions
Unpkg
Supported since v1.4.0+
Full distributed source
https://unpkg.com/browse/typescript-functional-extensions@1.4.0/
ES Modules
https://unpkg.com/typescript-functional-extensions@version/dist/esm/file
Example:
https://unpkg.com/typescript-functional-extensions@1.4.0/dist/esm/maybe.js
const { Maybe } = await import(
'https://unpkg.com/typescript-functional-extensions@1.4.0/dist/esm/maybe.js'
);
const maybe = Maybe.some('test');
Module Sizes
The distributed library is currently not minified. Below are the module sizes when minified (using UglifyJs) and GZipped:
- api.js: 0.15 kb
- index.js: 0.09 kb
- maybe.js: 0.81 kb
- maybe.utilities.js: 0.27 kb
- maybeAsync.js: 0.64 kb
- result.js: 1.25 kb
- resultAsync.js: 0.74 kb
- unit.js: 0.13 kb
- utilities.js: 0.27 kb
Total: 4.33 kb
Core Monads
import {
Maybe,
MaybeAsync,
Result,
ResultAsync,
} from 'typescript-functional-extensions';
Utilities
import {
never,
isDefined,
isSome,
isNone,
isFunction,
isPromise,
noop,
} from 'typescript-functional-extensions';
import {
zeroAsNone,
emptyStringAsNone,
emptyOrWhiteSpaceStringAsNone,
} from 'typescript-functional-extensions';
import {
fetchResponse,
fetchJsonResponse,
} from 'typescript-functional-extensions';
Monads
Below are the monads included in this package and examples of their use.
More examples of all monads and their methods can be found in the library unit tests or in the dedicated documentation files for each type.
Maybe
Maybe
represents a value that might or might not exist. You can use it to declaratively describe a process (series of steps) without having to check if there is a value present.
type Employee = {
email: string;
firstName: string;
lastName: string;
manager: Employee | undefined;
};
function yourBusinessProcess(): Employee[] {
// ...
}
const employees = yourBusinessProcess();
Maybe.tryFirst(employees)
.tap(({ firstName, lastName, email }) =>
console.log(`Found Employee: ${firstName} ${lastName}, ${email}`))
.bind(employee =>
Maybe.from(employee.manager)
.or({
email: 'supervisor@business.com',
firstName: 'Company',
lastName: 'Supervisor',
manager: undefined
})
.map(manager => ({ manager, employee }))
)
.match({
some(attendees => scheduleMeeting(attendees.manager, attendees.employee)),
none(() => console.log(`The business process did not return any employees`))
});
tryFirst
finds the first employee in the array and wraps it in aMaybe
. If the array is empty, aMaybe
with no value is returned.tap
's callback is only called if an employee was found and logs out that employee's information.bind
's callback is only called if an employee was found and converts theMaybe
wrapping it into to anotherMaybe
.from
wraps the employee's manager in aMaybe
. If the employee has no manager, aMaybe
with no value is returned.or
supplies a fallback in the case that the employee has no manager so that as long as an employee was originally found, all the following operations will execute.map
converts the manager to a new object which contains both the manager and employee.match
executes itssome
function if an employee was originally found and that employee has a manager. Since we supplied a fallback manager withor
, thesome
function ofmatch
will execute if we found an employee. Thenone
function ofmatch
executes if we didn't find any employees.
See more examples of Maybe
in the docs or in the tests.
MaybeAsync
MaybeAsync
represents a future value (Promise
) that might or might not exist.
MaybeAsync
works just like Maybe
, but since it is asynchronous, its methods accept a Promise<T>
in most cases and all of its value accessing methods/getters return a Promise<T>
.
See more examples of MaybeAsync
in the docs or in the tests.
Result
Result
represents a successful or failed operation. You can use it to declaratively define a process without needing to check if previous steps succeeded or failed. It can replace processes that use throwing errors and try
/catch
to control the flow of the application, or processes where errors and data are returned from every function.
type Employee = {
id: number;
email: string;
firstName: string;
lastName: string;
managerId: number | undefined;
};
function getEmployee(employeeId): Employee | undefined {
const employee = getEmployee(employeeId);
if (!employee) {
throw Error(`Could not find employee ${employeeId}!`);
}
return employee;
}
Result.try(
() => getEmployee(42),
(error) => `Retrieving the employee failed: ${error}`
)
.ensure(
(employee) => employee.email.endsWith('@business.com'),
({ firstName, lastName }) =>
`Employee ${firstName} ${lastName} is a contractor and not a full employee`
)
.bind(({ firstName, lastName, managerId }) =>
Maybe.from(managerId).toResult(
`Employee ${firstName} ${lastName} does not have a manager`
)
)
.map((managerId) => ({
managerId,
employeeFullName: `${firstName} ${lastName}`,
}))
.bind(({ managerId, employeeFullName }) =>
Result.try(
() => getEmployee(managerId),
(error) => `Retrieving the manager failed: ${error}`
).map((manager) => ({ manager, employeeFullName }))
)
.match({
success: ({ manager: { email }, employeeFullName }) =>
sendReminder(email, `Remember to say hello to ${employeeFullName}`),
failure: (error) => sendSupervisorAlert(error),
});
try
executes the function to retrieve the employee, converting any thrown errors into a failedResult
with the error message defined by the second parameter. If the employee is found, it returns a successfulResult
.ensure
's callback is only called if an employee was successfully found. It checks if the employee works for the company by looking at their email address. If the address doesn't end in@business.com
, a failedResult
is returned with the error message defined in the second parameter. If the check passes, the original successfulResult
is returned.bind
's callback is only called if the employee was found and works for the company. It converts the employeeResult
into anotherResult
.toResult
converts a missingmanagerId
into a failedResult
. If there is amanagerId
value, it's converted into a successfulResult
.map
's callback is only called if themanagerId
exists and converts themanagerId
into a new object to capture both the id and the employee's full name.bind
's callback is only called if the original employee was found and that employee had amanagerId
. It converts the id and employee name into a newResult
.try
now attempts to get the employee's manager and works the same as the firsttry
.map
's callback is only called if the original employee was found, has amanagerId
and that manager was also found. It converts the manager returned bytry
to a new object capturing both the manager and employee's name.match
'ssuccess
callback is only called if all the required information was retrieved and sends a reminder to the employee's manager. Thefailure
callback is called if any of the required data could not be retrieved and sends an alert to the business supervisor with the error message.
See more examples of Result
in the docs or in the tests.
ResultAsync
ResultAsync
represents a future result of an operation that either succeeds or fails.
ResultAsync
works just like Result
, but since it is asynchronous, its methods accept a Promise<T>
in most cases and all of its value accessing methods/getters return a Promise<T>
.
function getLatestInventory(): Promise<{ apples: number }> {
return Promise.reject('connection failure');
}
const resultAsync = ResultAsync.from(async () => {
try {
const value = await getLatestInventory();
return Result.success(value);
} catch (error: unknown) {
return Result.failure(`Could not retrieve inventory: ${error}`);
}
});
const result = await resultAsync.toPromise();
console.log(result.getErrorOrThrow()); // 'Could not retrieve inventory: connection failure'
See more examples of ResultAsync
in the docs or in the tests.
Contributing
To build this project, you must have v18.12.1 or higher of the Node.js installed.
If you've found a bug or have a feature request, please open an issue on GitHub.
If you'd like to make a contribution, you can create a Pull Request on GitHub.