Skip to content

Commit e7dc7b5

Browse files
authored
Merge pull request #2 from rackspace/feature/result
Added Result class and normalized with Maybe
2 parents c880367 + 0b72951 commit e7dc7b5

File tree

10 files changed

+1842
-586
lines changed

10 files changed

+1842
-586
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
Thumbs.db
1414

1515
# Project specific
16+
coverage
1617
node_modules
1718
npm-debug.log
1819
dist

README.md

Lines changed: 115 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,28 @@
1-
# maybe-result - Safe function return handling without null and undefined in Typescript and Javascript
1+
# maybe-result - Safe function return handling in Typescript and Javascript
22

3-
## Introduction
3+
Deciding when a function should return `undefined`, throw an `Error`, or return some other indicator that
4+
"something isn't right" is tough. We don't always know how users _calling_ our function
5+
will use it, and we want to be clear, clean, and safe.
46

5-
In many languages, we have concepts of exceptions but also a `null` value of some sort.
7+
This library provides two approaches for wrapping function results:
8+
9+
- [Maybe](#maybe) for when something may or may not exist
10+
- [Result](#result) for when you might have an error, but want to let the caller decide how to handle it
11+
12+
## Maybe
13+
14+
In many languages, we have concepts of exceptions and `null` values.
615
(JavaScript has both `null` and `undefined`. Ugh!)
716

817
Often a function will need to indicate when a value _maybe_ exists, or it does not.
918
In JavaScript, the "does not" is usually returned as `undefined` or `null`, but sometimes
10-
a function will *throw* an `Error` type instead. Thus, the developer needs to figure out
19+
a function will _throw_ an `Error` type instead. Thus, the developer needs to figure out
1120
how that particular function behaves and adapt to that if they want to handle
1221
the missing value.
1322

1423
Finally, throwing Errors in TypeScript can be expensive, as a stack trace must be
1524
generated and cross-referenced to the `.js.map` files. These stack traces to your
16-
TypeScript source are immensely useful to trace actual errors, but are wasted
25+
TypeScript source are immensely useful for tracing actual errors, but they are wasted
1726
processing when ignored.
1827

1928
The `Maybe` type makes this cleaner. Elm was an early language that defined this.
@@ -26,16 +35,18 @@ the possibly missing value. The caller can explicitly check for `isValue` or
2635
It is now the caller's choice. There are many other helper functions too, such as
2736
to unwrap with a default value to return in place of throwing if `isNone`.
2837

29-
This is not an "anti-throw" utility like Rust's `Result` type is.
3038
In JavaScript we like to throw `Error` types, but in other languages we call these _exceptions_.
3139
**Throwing is still good for _exceptional_ cases. `Maybe` is for "normal" control flows.**
3240

41+
`Maybe` is not only for function return values. It may be used elsewhere where you want a type-safe
42+
and immutable alternative to `undefined` and `null`.
43+
3344
Here's a nice introduction to the concept:
3445
[Implementing a Maybe Pattern using a TypeScript Type Guard](https://medium.com/@sitapati/implementing-a-maybe-pattern-using-a-typescript-type-guard-81b55efc0af0)
3546

36-
## Example by story
47+
### Example by Story
3748

38-
You might have defined a data repository class (access to a data store) like this:
49+
You might define a data repository class (access to a data store) like this:
3950

4051
```ts
4152
class WidgetRepository {
@@ -45,21 +56,22 @@ class WidgetRepository {
4556
}
4657
```
4758

48-
If the Widget isn't found, you throw a `NotFoundError`. All is well, until you start _expecting_
59+
If the Widget isn't found, you throw a `NotFoundError`. All is well until you start _expecting_
4960
a Widget not to be found. That becomes valid flow, so you find yourself writing this a lot:
5061

5162
```ts
52-
let widget: Widget | undefined;
53-
try {
54-
widget = await repo.get(widgetID);
63+
let widget: Widget | undefined;
64+
try {
65+
widget = await widgetRepo.get(widgetID);
66+
} catch (error) {
67+
if (!(error instanceof NotFoundError)) {
68+
throw error;
5569
}
56-
catch (error) {
57-
if (!(error instanceof NotFoundError)) {
58-
throw error;
59-
}
60-
}
61-
62-
if (widget) { /* ... */ }
70+
}
71+
72+
if (widget) {
73+
/* ... */
74+
}
6375
```
6476

6577
You may be willing to do that once... but not more. So you first try to change the repository:
@@ -72,7 +84,7 @@ class WidgetRepository {
7284
}
7385
```
7486

75-
Now it returns `undefined` instead of throwing. Oh, but what a hassle now you have to _check_ for
87+
Now it returns `undefined` instead of throwing. Oh, but what a hassle - now you have to _check_ for
7688
`undefined` _every time_ you call the function! So instead, you define _two_ functions:
7789

7890
```ts
@@ -88,48 +100,119 @@ class WidgetRepository {
88100

89101
That makes it easier. It works. You just have to write _two_ functions every time you write a get function. 🙄
90102

91-
**OR...** use Maybe
103+
**OR...** use `Maybe` 🎉
92104

93105
```ts
94106
class WidgetRepository {
95-
get(widgetID: string): PromiseMaybe<Widget> {
107+
async get(widgetID: string): PromiseMaybe<Widget> {
96108
// implementation ...
97109
}
98110
}
99111

100112
// One place elsewhere where you want to throw if not found
101-
const widget = Maybe.unwrap(await get(widgetID));
113+
const widget = Maybe.unwrap(await widgetRepo.get(widgetID));
102114

103115
// Another place elsewhere where you want to handle the mising lookup
104-
const widget = Maybe.unwrapOrNull(await get(widgetID));
116+
const widget = Maybe.unwrapOrNull(await widgetRepo.get(widgetID));
105117
if (widget) {
106118
// do work
107119
} else {
108120
// do other work
109121
}
110122

111123
// Someplace where you have a default
112-
const widget = (await get(widgetID)).unwrapOr(defaultWidget);
124+
const widget = (await widgetRepo.get(widgetID)).unwrapOr(defaultWidget);
113125
```
114126

127+
---
128+
115129
There are many other functions both on the `Maybe` instance and static helper functions in
116130
the `Maybe` namespace.
117131

132+
## Result
133+
134+
Unlike `Maybe`, which simply has some value or no value and doesn't want to return `undefined`,
135+
`Result` is for when you have an **error** and don't want to `throw`.
136+
Similar to `Maybe`, this is all about the function giving the caller the _choice_ of
137+
how to handle a situation - in this case an _exceptional_ situation.
138+
139+
This is modeled off of the Rust `Result` type, but made to pair cleanly with this
140+
implementation of `Maybe`.
141+
142+
### Example
143+
144+
Expanding on the previous example of a `WidgetRepository`,
145+
let's add a function in the repository that creates a new widget.
146+
A `create` function should error out if the assumption that the
147+
widget doesn't yet exist is false.
148+
149+
```ts
150+
class WidgetRepository {
151+
async create(
152+
widget: CreatableWidget,
153+
): Promise<Result<Widget, ConstraintError>> {
154+
try {
155+
// implementation ...
156+
return Result.okay(newWidget);
157+
} catch (err) {
158+
return Result.error(err);
159+
}
160+
}
161+
}
162+
163+
/*
164+
* Elsewhere in the create-widget use-case...
165+
*/
166+
const createResult = await widgetRepo.create(creatableWidget);
167+
168+
if (createResult.isOkay) {
169+
return createResult.value;
170+
} else {
171+
// Throw more end-user aligned error instead of the database error
172+
throw new HttpBadRequest("Widget already exists");
173+
}
174+
175+
/*
176+
* Or more simply...
177+
*/
178+
const createResult = await widgetRepo.create(creatableWidget);
179+
return createResult.unwrapOrThrow(new HttpBadRequest("Widget already exists"));
180+
181+
/*
182+
* Or if you just want the behavior of if Result wasn't used, just unwrap it
183+
* and any contained error will throw.
184+
*/
185+
return (await widgetRepo.create(creatableWidget)).unwrap();
186+
// or slightly more readable:
187+
return Result.unwrap(await widgetRepo.create(creatableWidget));
188+
// or convert to a Maybe, so that Maybe.None is returned in place of the error
189+
return (await widgetRepo.create(creatableWidget)).toMaybe();
190+
```
191+
118192
## API Use
119193

120-
[API Documentation](https://www.jsdocs.io/package/maybe-result)
194+
Both `Maybe` and `Result` have many more member and static functions. Learn more:
121195

122-
See the [unit test suite](src/index.spec.ts) for usage examples.
196+
- [API Documentation](https://www.jsdocs.io/package/maybe-result)
197+
- Full coverage examples in the [Maybe unit test suite](src/maybe.spec.ts) and [Result unit test suite](src/result.spec.ts).
198+
- Functions are named per some foundational concepts:
199+
- `wrap` wraps up a value
200+
- `unwrap` means to extract the value
201+
- `or` performs a boolean _or_ operation between two instances
202+
- `orElse` lazily gets the second operand for an _or_ operation via a callback function _only_ if needed
203+
- `and` performs a boolean _and_ operation between two instances
204+
- `andThen` lazily gets the second operand for an _and_ operation via a callback function _only_ if needed
205+
- `map` functions transform the value to return a new instance (immutably)
123206

124207
## Origin and Alternatives
125208

126-
This implementation is based on `Option` from [ts-results](https://github.com/vultix/ts-results),
127-
which adheres to the Rust API.
128-
This library has more natual word choices, Promise support, and other enhancements.
209+
This implementation is based on [ts-results](https://github.com/vultix/ts-results),
210+
which adheres to the Rust API.
211+
This library has more natual word choices, Promise support, additional functions, and other enhancements.
129212

130213
There are many other libraries that do this same thing - just
131214
[search NPM for "maybe"](https://www.npmjs.com/search?q=maybe).
132215
It is up to you to decide which option is best for your project.
133216

134-
**The goal of this "maybe" is to be featureful, safe, and easy to understand without
135-
a study of functional programming.**
217+
_The goal of this library is to be featureful, safe, and easy to understand without
218+
a study of functional programming._

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
{
22
"name": "maybe-result",
33
"version": "0.1.0",
4-
"description": "Safe function return handling without null and undefined in Typescript and Javascript",
4+
"description": "Safe function return handling in Typescript and Javascript",
55
"keywords": [
66
"maybe",
77
"typescript",
8-
"optional",
8+
"option",
99
"result",
1010
"fp",
1111
"undefined",
@@ -55,6 +55,6 @@
5555
"dev": "tsup --watch",
5656
"lint": "eslint src",
5757
"prettier": "prettier --check src",
58-
"test": "jest"
58+
"test": "jest --coverage"
5959
}
6060
}

0 commit comments

Comments
 (0)