Skip to content

Commit 7dc2485

Browse files
authored
Merge pull request #12 from seangwright/feat/library-maintenance
2 parents 75ba7b3 + 74c4e7f commit 7dc2485

18 files changed

+9209
-4989
lines changed

.vscode/tasks.json

+14-11
Original file line numberDiff line numberDiff line change
@@ -2,43 +2,46 @@
22
"version": "2.0.0",
33
"tasks": [
44
{
5+
"label": "install",
56
"type": "npm",
67
"script": "install",
7-
"group": "build",
88
"problemMatcher": [],
9-
"label": "install",
10-
"detail": "install"
9+
"detail": "Installs all dependencies"
1110
},
1211
{
12+
"label": "test",
13+
"dependsOn": ["install"],
1314
"type": "npm",
1415
"script": "test",
1516
"group": "test",
1617
"problemMatcher": [],
17-
"label": "test",
18-
"detail": "jest"
18+
"detail": "Runs the Jest tests"
1919
},
2020
{
21+
"label": "test - watch",
22+
"dependsOn": ["install"],
2123
"type": "npm",
2224
"script": "test:watch",
2325
"group": "test",
2426
"problemMatcher": [],
25-
"label": "test - watch",
26-
"detail": "jest"
27+
"detail": "Runs the Jest tests in watch mode"
2728
},
2829
{
30+
"label": "typescript - build",
31+
"dependsOn": ["install"],
2932
"type": "npm",
3033
"script": "build",
3134
"group": "build",
3235
"problemMatcher": [],
33-
"label": "npm: build",
34-
"detail": "tsc"
36+
"detail": "Runs the TypeScript compilation"
3537
},
3638
{
39+
"label": "typescript - watch",
40+
"dependsOn": ["install"],
3741
"type": "npm",
3842
"script": "start",
3943
"problemMatcher": [],
40-
"label": "npm: start",
41-
"detail": "tsc --watch"
44+
"detail": "Runs the TypeScript compilation in watch mode"
4245
}
4346
]
4447
}

README.md

+126-145
Original file line numberDiff line numberDiff line change
@@ -11,195 +11,174 @@ A TypeScript implementation of the C# library [CSharpFunctionalExtensions](https
1111
- [Modeling Missing Data - The Maybe Monad](https://dev.to/seangwright/kentico-xperience-design-patterns-modeling-missing-data-the-maybe-monad-2c7i)
1212
- [Railway Oriented Programming](https://fsharpforfunandprofit.com/rop/)
1313

14-
## Monads
15-
16-
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).
17-
18-
### Maybe
14+
## How to Use
1915

20-
`Maybe` represents a value that might or might not exist.
21-
22-
#### Some/None/From
16+
### Core Monads
2317

2418
```typescript
25-
const maybe = Maybe.some('apple');
26-
27-
console.log(maybe.hasValue); // true
28-
console.log(maybe.hasNoValue); // false
29-
console.log(maybe.getValueOrDefault('banana')); // 'apple'
30-
console.log(maybe.getValueOrThrow()); // 'apple'
19+
import {
20+
Maybe,
21+
MaybeAsync,
22+
Result,
23+
ResultAsync,
24+
} from 'typescript-functional-extensions';
3125
```
3226

33-
```typescript
34-
const maybe = Maybe.none();
35-
36-
console.log(maybe.hasValue); // false
37-
console.log(maybe.hasNoValue); // true
38-
console.log(maybe.getValueOrDefault('banana')); // 'banana'
39-
console.log(maybe.getValueOrThrow()); // throws Error 'No value'
40-
```
27+
### Utilities
4128

4229
```typescript
43-
const maybe = Maybe.from(undefined);
44-
45-
console.log(maybe.hasValue); // false
46-
console.log(maybe.hasNoValue); // true
47-
console.log(maybe.getValueOrDefault('banana')); // 'banana'
48-
console.log(maybe.getValueOrThrow()); // throws Error 'No value'
30+
import {
31+
never,
32+
isDefined,
33+
isSome,
34+
isNone,
35+
isFunction,
36+
isPromise,
37+
noop,
38+
} from 'typescript-functional-extensions';
4939
```
5040

51-
#### TryFirst
52-
5341
```typescript
54-
const maybe = Maybe.tryFirst(['apple', 'banana']);
55-
56-
console.log(maybe.getValueOrThrow()); // 'apple'
42+
import {
43+
zeroAsNone,
44+
emptyStringAsNone,
45+
emptyOrWhiteSpaceStringAsNone,
46+
} from 'typescript-functional-extensions';
5747
```
5848

59-
```typescript
60-
const maybe = Maybe.tryFirst(['apple', 'banana'], (fruit) => fruit.length > 6);
61-
62-
console.log(maybe.getValueOrThrow()); // throws Error 'No value'
63-
```
64-
65-
#### TryLast
66-
67-
```typescript
68-
const maybe = Maybe.tryLast(
69-
['apple', 'banana', 'mango'],
70-
(fruit) => fruit.length === 5
71-
);
72-
73-
console.log(maybe.getValueOrThrow()); // 'mango'
74-
```
75-
76-
```typescript
77-
const maybe = Maybe.tryLast(
78-
['apple', 'banana', 'mango'],
79-
(fruit) => fruit === 'pear'
80-
);
49+
## Monads
8150

82-
console.log(maybe.getValueOrThrow()); // throws Error 'No value'
83-
```
51+
Below are the monads included in this package and examples of their use.
8452

85-
#### Map
53+
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.
8654

87-
```typescript
88-
const maybe = Maybe.some({ type: 'fruit', name: 'apple' })
89-
.map(({ type }) => ({ type, name: 'banana' }))
90-
.map((food) => food.name)
91-
.map((name) => name.length);
55+
### Maybe
9256

93-
console.log(maybe.getValueOrThrow()); // 6
94-
```
57+
`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.
9558

9659
```typescript
97-
type Food = {
98-
type: string;
99-
name: string;
60+
type Employee = {
61+
email: string;
62+
firstName: string;
63+
lastName: string;
64+
manager: Employee | undefined;
10065
};
10166

102-
const maybe = Maybe.none<Food>()
103-
.map(({ type }) => ({ type, name: 'banana' }))
104-
.map((food) => food.name)
105-
.map((name) => name.length);
106-
107-
console.log(maybe.getValueOrThrow()); // throws Error 'No value'
108-
```
109-
110-
#### Match
111-
112-
```typescript
113-
const maybe = Maybe.some({ type: 'fruit', name: 'apple' })
114-
.map(({ type }) => ({ type, name: 'banana' }))
115-
.map((food) => food.name)
116-
.map((name) => name.length)
117-
.match({
118-
some: (number) => console.log(number),
119-
none: () => console.log('None!'),
120-
}); // 6
121-
```
122-
123-
```typescript
124-
type Food = {
125-
type: string;
126-
name: string;
127-
};
67+
function yourBusinessProcess(): Employee[] {
68+
// ...
69+
}
12870

129-
const maybe = Maybe.none<Food>()
130-
.map(({ type }) => ({ type, name: 'banana' }))
131-
.map((food) => food.name)
132-
.map((name) => name.length)
71+
const employees = yourBusinessProcess();
72+
73+
Maybe.tryFirst(employees)
74+
.tap(({ firstName, lastName, email }) =>
75+
console.log(`Found Employee: ${firstName} ${lastName}, ${email}`))
76+
.bind(employee =>
77+
Maybe.from(employee.manager)
78+
.or({
79+
80+
firstName: 'Company',
81+
lastName: 'Supervisor',
82+
manager: undefined
83+
})
84+
.map(manager => ({ manager, employee }))
85+
)
13386
.match({
134-
some: (number) => console.log(number),
135-
none: () => console.log('None!'),
136-
}); // None!
87+
some(attendees => scheduleMeeting(attendees.manager, attendees.employee)),
88+
none(() => console.log(`The business process did not return any employees`))
89+
});
13790
```
13891

139-
#### Pipe
92+
1. `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.
93+
1. `tap`'s callback is only called if an employee was found and logs out that employee's information.
94+
1. `bind`'s callback is only called if an employee was found and converts the `Maybe` wrapping it into to another `Maybe`.
95+
1. `from` wraps the employee's manager in a `Maybe`. If the employee has no manager, a `Maybe` with no value is returned.
96+
1. `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.
97+
1. `map` converts the manager to a new object which contains both the manager and employee.
98+
1. `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.
14099

141-
```typescript
142-
// custom-operators.ts
143-
import { logger, LogLevel } from 'logger';
144-
145-
export function log<TValue>(
146-
messageCreator: FunctionOfTtoK<TValue, string>,
147-
logLevel: LogLevel = 'debug'
148-
): MaybeOpFn<TValue, TValue> {
149-
return (maybe) => {
150-
if (maybe.hasValue) {
151-
logger.log(messageCreator(maybe.getValueOrThrow()), logLevel);
152-
} else {
153-
logger.error('No value found!');
154-
}
155-
156-
return maybe;
157-
};
158-
}
159-
160-
// app.ts
161-
import { log } from './custom-operators.ts';
162-
163-
const maybe = Maybe.some('apple')
164-
.pipe(log((f) => `My fruit is ${f}`, 'information'))
165-
.map((f) => `${f} and banana`)
166-
.pipe(log((f) => `Now I have ${f}`));
167-
```
100+
See more examples of `Maybe` [in the docs](./docs/maybe.md) or [in the tests](./test/maybe).
168101

169102
### MaybeAsync
170103

171104
`MaybeAsync` represents a future value (`Promise`) that might or might not exist.
172105

173-
```typescript
174-
function getFruit(day): Promise<string> {
175-
return Promise.resolve('apple');
176-
}
106+
`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>`.
177107

178-
const maybeAsync = MaybeAsync.from(getFruit());
179-
180-
const maybe = maybeAsync.toPromise();
181-
182-
console.log(maybe.getValueOrThrow()); // 'apple'
183-
```
108+
See more examples of `MaybeAsync` [in the docs](./docs/maybeAsync.md) or [in the tests](./test/maybeAsync).
184109

185110
### Result
186111

187-
`Result` represents a successful or failed operation.
112+
`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.
188113

189114
```typescript
190-
const successfulResult = Result.success('apple');
115+
type Employee = {
116+
id: number;
117+
email: string;
118+
firstName: string;
119+
lastName: string;
120+
managerId: number | undefined;
121+
};
122+
123+
function getEmployee(employeeId): Employee | undefined {
124+
const employee = getEmployee(employeeId);
191125

192-
console.log(successfulResult.getValueOrThrow()); // 'apple'
126+
if (!employee) {
127+
throw Error(`Could not find employee ${employeeId}!`);
128+
}
193129

194-
const failedResult = Result.failure('no fruit');
130+
return employee;
131+
}
195132

196-
console.log(failedResult.getErrorOrThrow()); // 'no fruit'
133+
Result.try(
134+
() => getEmployee(42),
135+
(error) => `Retrieving the employee failed: ${error}`
136+
)
137+
.ensure(
138+
(employee) => employee.email.endsWith('@business.com'),
139+
({ firstName, lastName }) =>
140+
`Employee ${firstName} ${lastName} is a contractor and not a full employee`
141+
)
142+
.bind(({ firstName, lastName, managerId }) =>
143+
Maybe.from(managerId).toResult(
144+
`Employee ${firstName} ${lastName} does not have a manager`
145+
)
146+
)
147+
.map((managerId) => ({
148+
managerId,
149+
employeeFullName: `${firstName} ${lastName}`,
150+
}))
151+
.bind(({ managerId, employeeFullName }) =>
152+
Result.try(
153+
() => getEmployee(managerId),
154+
(error) => `Retrieving the manager failed: ${error}`
155+
).map((manager) => ({ manager, employeeFullName }))
156+
)
157+
.match({
158+
success: ({ manager: { email }, employeeFullName }) =>
159+
sendReminder(email, `Remember to say hello to ${employeeFullName}`),
160+
failure: (error) => sendSupervisorAlert(error),
161+
});
197162
```
198163

164+
1. `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`.
165+
1. `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.
166+
1. `bind`'s callback is only called if the employee was found and works for the company. It converts the employee `Result` into another `Result`.
167+
1. `toResult` converts a missing `managerId` into a failed `Result`. If there is a `managerId` value, it's converted into a successful `Result`.
168+
1. `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.
169+
1. `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`.
170+
1. `try` now attempts to get the employee's manager and works the same as the first `try`.
171+
1. `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.
172+
1. `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.
173+
174+
See more examples of `Result` [in the docs](./docs/result.md) or [in the tests](./test/result).
175+
199176
### ResultAsync
200177

201178
`ResultAsync` represents a future result of an operation that either succeeds or fails.
202179

180+
`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>`.
181+
203182
```typescript
204183
function getLatestInventory(): Promise<{ apples: number }> {
205184
return Promise.reject('connection failure');
@@ -216,5 +195,7 @@ const resultAsync = ResultAsync.from(async () => {
216195

217196
const result = await resultAsync.toPromise();
218197

219-
console.log(result.getErrorOrThrow()); // 'Could not retrieve inventory: connection failure
198+
console.log(result.getErrorOrThrow()); // 'Could not retrieve inventory: connection failure'
220199
```
200+
201+
See more examples of `ResultAsync` [in the docs](./docs/resultAsync.md) or [in the tests](./test/resultAsync).

0 commit comments

Comments
 (0)