UNPKG

10.7 kBMarkdownView Raw
1# Typescript Functional Extensions
2
3[![NPM](https://nodei.co/npm/typescript-functional-extensions.png)](https://nodei.co/npm/typescript-functional-extensions/)
4
5A TypeScript implementation of the C# library [CSharpFunctionalExtensions](https://github.com/vkhorikov/CSharpFunctionalExtensions), including synchronous and asynchronous `Maybe` and `Result` monads.
6
7## Community
8
9### Related Projects
10
11- [NestJS typescript-functional-extensions utilities](https://github.com/startupdevhouse/typescript-functional-extensions-nestjs) (A library of utilities for working with `typescript-functional-extensions` in [NestJS](https://nestjs.com/) projects)
12
13### Influences
14
15- [fp-ts](https://github.com/gcanti/fp-ts) (Typed functional programming in TypeScript)
16- [CSharpFunctionalExtensions](https://github.com/vkhorikov/CSharpFunctionalExtensions) (A library to help write C# code in more functional way)
17
18### Further Reading
19
20- [Functional Extensions for C#](https://github.com/vkhorikov/CSharpFunctionalExtensions)
21- [Functors, Applicatives, And Monads In Pictures](https://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html)
22- [Railway Oriented Programming](https://fsharpforfunandprofit.com/rop/)
23- [Modeling Missing Data - The Maybe Monad](https://dev.to/seangwright/kentico-xperience-design-patterns-modeling-missing-data-the-maybe-monad-2c7i)
24- [Handling Failures - The Result Monad](https://dev.to/seangwright/kentico-xperience-design-patterns-handling-failures-the-result-monad-1j25)
25
26## How to Use
27
28### npm
29
30```bash
31npm i typescript-functional-extensions
32```
33
34### Unpkg
35
36> Supported since v1.4.0+
37
38#### Full distributed source
39
40<https://unpkg.com/browse/typescript-functional-extensions@2.0.0/>
41
42#### ES Modules
43
44<https://unpkg.com/typescript-functional-extensions@version/dist/esm/file>
45
46Example:
47
48<https://unpkg.com/typescript-functional-extensions@2.0.0/dist/esm/maybe.js>
49
50```javascript
51const { Maybe } = await import(
52 'https://unpkg.com/typescript-functional-extensions@2.0.0/dist/esm/maybe.js'
53);
54
55const maybe = Maybe.some('test');
56```
57
58### Module Sizes
59
60The distributed library is currently not minified. Below are the module sizes when minified (using UglifyJs) and GZipped:
61
62- api.js: 0.15 kb
63- index.js: 0.09 kb
64- maybe.js: 0.81 kb
65- maybe.utilities.js: 0.27 kb
66- maybeAsync.js: 0.64 kb
67- result.js: 1.28 kb
68- resultAsync.js: 0.76 kb
69- unit.js: 0.13 kb
70- utilities.js: 0.27 kb
71
72Total: 4.39 kb
73
74### Core Monads
75
76```typescript
77import {
78 Maybe,
79 MaybeAsync,
80 Result,
81 ResultAsync,
82} from 'typescript-functional-extensions';
83```
84
85### Utilities
86
87```typescript
88import {
89 never,
90 isDefined,
91 isSome,
92 isNone,
93 isFunction,
94 isPromise,
95 noop,
96} from 'typescript-functional-extensions';
97```
98
99```typescript
100import {
101 zeroAsNone,
102 emptyStringAsNone,
103 emptyOrWhiteSpaceStringAsNone,
104} from 'typescript-functional-extensions';
105```
106
107```typescript
108import {
109 fetchResponse,
110 fetchJsonResponse,
111} from 'typescript-functional-extensions';
112```
113
114## Monads
115
116Below are the monads included in this package and examples of their use.
117
118More examples of all monads and their methods can be found in the library unit tests or in the dedicated documentation files for each type.
119
120### Maybe
121
122`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.
123
124```typescript
125type Employee = {
126 email: string;
127 firstName: string;
128 lastName: string;
129 manager: Employee | undefined;
130};
131
132function yourBusinessProcess(): Employee[] {
133 // ...
134}
135
136const employees = yourBusinessProcess();
137
138Maybe.tryFirst(employees)
139 .tap(({ firstName, lastName, email }) =>
140 console.log(`Found Employee: ${firstName} ${lastName}, ${email}`))
141 .bind(employee =>
142 Maybe.from(employee.manager)
143 .or({
144 email: 'supervisor@business.com',
145 firstName: 'Company',
146 lastName: 'Supervisor',
147 manager: undefined
148 })
149 .map(manager => ({ manager, employee }))
150 )
151 .match({
152 some(attendees => scheduleMeeting(attendees.manager, attendees.employee)),
153 none(() => console.log(`The business process did not return any employees`))
154 });
155```
156
1571. `tryFirst` finds the first employee in the array and wraps it in a `Maybe`. If the array is empty, a `Maybe` with no value is returned.
1581. `tap`'s callback is only called if an employee was found and logs out that employee's information.
1591. `bind`'s callback is only called if an employee was found and converts the `Maybe` wrapping it into to another `Maybe`.
1601. `from` wraps the employee's manager in a `Maybe`. If the employee has no manager, a `Maybe` with no value is returned.
1611. `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.
1621. `map` converts the manager to a new object which contains both the manager and employee.
1631. `match` executes its `some` function if an employee was originally found and that employee has a manager. Since we supplied a fallback manager with `or`, the `some` function of `match` will execute if we found an employee. The `none` function of `match` executes if we didn't find any employees.
164
165See more examples of `Maybe` [in the docs](./docs/maybe.md) or [in the tests](./test/maybe).
166
167### MaybeAsync
168
169`MaybeAsync` represents a future value (`Promise`) that might or might not exist.
170
171`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>`.
172
173See more examples of `MaybeAsync` [in the docs](./docs/maybeAsync.md) or [in the tests](./test/maybeAsync).
174
175### Result
176
177`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.
178
179```typescript
180type Employee = {
181 id: number;
182 email: string;
183 firstName: string;
184 lastName: string;
185 managerId: number | undefined;
186};
187
188function getEmployee(employeeId): Employee | undefined {
189 const employee = getEmployee(employeeId);
190
191 if (!employee) {
192 throw Error(`Could not find employee ${employeeId}!`);
193 }
194
195 return employee;
196}
197
198Result.try(
199 () => getEmployee(42),
200 (error) => `Retrieving the employee failed: ${error}`
201)
202 .ensure(
203 (employee) => employee.email.endsWith('@business.com'),
204 ({ firstName, lastName }) =>
205 `Employee ${firstName} ${lastName} is a contractor and not a full employee`
206 )
207 .bind(({ firstName, lastName, managerId }) =>
208 Maybe.from(managerId).toResult(
209 `Employee ${firstName} ${lastName} does not have a manager`
210 )
211 )
212 .map((managerId) => ({
213 managerId,
214 employeeFullName: `${firstName} ${lastName}`,
215 }))
216 .bind(({ managerId, employeeFullName }) =>
217 Result.try(
218 () => getEmployee(managerId),
219 (error) => `Retrieving the manager failed: ${error}`
220 ).map((manager) => ({ manager, employeeFullName }))
221 )
222 .match({
223 success: ({ manager: { email }, employeeFullName }) =>
224 sendReminder(email, `Remember to say hello to ${employeeFullName}`),
225 failure: (error) => sendSupervisorAlert(error),
226 });
227```
228
2291. `try` executes the function to retrieve the employee, converting any thrown errors into a failed `Result` with the error message defined by the second parameter. If the employee is found, it returns a successful `Result`.
2301. `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 failed `Result` is returned with the error message defined in the second parameter. If the check passes, the original successful `Result` is returned.
2311. `bind`'s callback is only called if the employee was found and works for the company. It converts the employee `Result` into another `Result`.
2321. `toResult` converts a missing `managerId` into a failed `Result`. If there is a `managerId` value, it's converted into a successful `Result`.
2331. `map`'s callback is only called if the `managerId` exists and converts the `managerId` into a new object to capture both the id and the employee's full name.
2341. `bind`'s callback is only called if the original employee was found and that employee had a `managerId`. It converts the id and employee name into a new `Result`.
2351. `try` now attempts to get the employee's manager and works the same as the first `try`.
2361. `map`'s callback is only called if the original employee was found, has a `managerId` and that manager was also found. It converts the manager returned by `try` to a new object capturing both the manager and employee's name.
2371. `match`'s `success` callback is only called if all the required information was retrieved and sends a reminder to the employee's manager. The `failure` 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.
238
239See more examples of `Result` [in the docs](./docs/result.md) or [in the tests](./test/result).
240
241### ResultAsync
242
243`ResultAsync` represents a future result of an operation that either succeeds or fails.
244
245`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>`.
246
247```typescript
248function getLatestInventory(): Promise<{ apples: number }> {
249 return Promise.reject('connection failure');
250}
251
252const resultAsync = ResultAsync.from(async () => {
253 try {
254 const value = await getLatestInventory();
255 return Result.success(value);
256 } catch (error: unknown) {
257 return Result.failure(`Could not retrieve inventory: ${error}`);
258 }
259});
260
261const result = await resultAsync.toPromise();
262
263console.log(result.getErrorOrThrow()); // 'Could not retrieve inventory: connection failure'
264```
265
266See more examples of `ResultAsync` [in the docs](./docs/resultAsync.md) or [in the tests](./test/resultAsync).
267
268## Contributing
269
270To build this project, you must have v18.12.1 or higher
271of the [Node.js](https://nodejs.org/dist/v18.12.1/node-v18.12.1-x64.msi) installed.
272
273If you've found a bug or have a feature request, please [open an issue](https://github.com/seangwright/typescript-functional-extensions/issues/new) on GitHub.
274
275If you'd like to make a contribution, you can create a [Pull Request](https://github.com/seangwright/typescript-functional-extensions/compare) on GitHub.