Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 32 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,26 +38,34 @@ if (result.isOk()) {
} else {
console.error(result.getError());
}

// or handle both cases in one expression

const message = result.match({
ok: (age) => `Age is ${age}`,
error: (error) => error.message,
});
```

## Reference

### Creating a result

| Method | Description |
| --------------------- | -------------------------------------------------------- |
| `Result.ok()` | Creates a successful `Result<void>`, with no value. |
| `Result.ok(value)` | Creates a successful result, holding a value. |
| `Result.error(error)` | Creates a failed result holding an `Error`. |
| Method | Description |
| --------------------- | --------------------------------------------------- |
| `Result.ok()` | Creates a successful `Result<void>`, with no value. |
| `Result.ok(value)` | Creates a successful result, holding a value. |
| `Result.error(error)` | Creates a failed result holding an `Error`. |

### Inspecting a result

| Method | Returns | Description |
| ------------ | -------------------- | -------------------------------------------------- |
| `isOk()` | `boolean` | `true` if the result is a success. |
| `isError()` | `boolean` | `true` if the result is a failure. |
| `getValue()` | `T \| undefined` | The value of a success. `undefined` for a failure. |
| `getError()` | `Error \| undefined` | The error of a failure. `undefined` for a success. |
| Method | Returns | Description |
| ---------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `isOk()` | `boolean` | `true` if the result is a success. |
| `isError()` | `boolean` | `true` if the result is a failure. |
| `getValue()` | `T \| undefined` | The value of a success. `undefined` for a failure. |
| `getError()` | `Error \| undefined` | The error of a failure. `undefined` for a success. |
| `match({ ok, error })` | `O \| E` | Calls `ok(value): O` for a success or `error(error): E` for a failure, and returns what the called handler returns. |

## Notes

Expand All @@ -73,6 +81,19 @@ if (result.isOk()) { /* ... */ }
if (result.getValue()) { /* ... */ }
```

### Using match

`match` handles both outcomes in one expression. You have to provide both handlers, so you can't forget the error case, and you get a value back without writing an `if`/`else`:

```ts
const message = parseAge(input).match({
ok: (age) => `Age is ${age}`,
error: (error) => `Invalid input: ${error.message}`,
});
```

Use `isOk()` / `isError()` instead when you need to return early or run statements in a branch rather than produce a value.

### Using try/catch

A function that returns a `Result` should not throw. To use code that can throw, such as the `URL` constructor, catch the exception and return it as a failure:
Expand Down
123 changes: 122 additions & 1 deletion src/result.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expect, test, describe, expectTypeOf } from 'vitest';
import { expect, test, describe, expectTypeOf, vi } from 'vitest';
import { Result } from './result';

describe('Result', () => {
Expand Down Expand Up @@ -88,4 +88,125 @@ describe('Result', () => {
}
});
});

describe('match', () => {
const createResult = (valid: boolean): Result<number> =>
valid ? Result.ok(10) : Result.error(new Error('message'));

describe('on an ok result', () => {
test('calls only the ok handler, with the value', () => {
const ok = vi.fn((v: number) => v * 2);
const error = vi.fn((e: Error) => e.message);

createResult(true).match({ ok, error });

expect(ok).toHaveBeenCalledTimes(1);
expect(ok).toHaveBeenCalledWith(10);
expect(error).not.toHaveBeenCalled();
});

test('returns the ok handler return value', () => {
const output = createResult(true).match({
ok: (v) => v * 2,
error: () => -1,
});

expect(output).toBe(20);
});
});

describe('on an error result', () => {
test('calls only the error handler, with the error', () => {
const err = new Error('boom');
const ok = vi.fn((v: number) => v);
const error = vi.fn((e: Error) => e.message);
const result: Result<number> = Result.error(err);

result.match({ ok, error });

expect(error).toHaveBeenCalledTimes(1);
expect(error).toHaveBeenCalledWith(err);
expect(ok).not.toHaveBeenCalled();
});

test('returns the error handler return value', () => {
const output = createResult(false).match({
ok: () => 'ok',
error: (e) => `failed: ${e.message}`,
});

expect(output).toBe('failed: message');
});
});

describe('edge cases', () => {
test.each([0, '', false, undefined, null])(
'routes falsy ok value %j to the ok handler',
(value) => {
const output = Result.ok(value).match({
ok: () => 'ok',
error: () => 'error',
});

expect(output).toBe('ok');
},
);

test('passes undefined to ok for Result<void>', () => {
const ok = vi.fn();
const result: Result<void> = Result.ok();

result.match({ ok, error: () => {} });

expect(ok).toHaveBeenCalledWith(undefined);
});

test('does not swallow errors thrown by a handler', () => {
expect(() =>
createResult(true).match({
ok: () => {
throw new Error('handler failed');
},
error: () => 0,
}),
).toThrow('handler failed');
});
});

describe('types', () => {
test('infers handler parameter types', () => {
createResult(true).match({
ok: (v) => {
expectTypeOf(v).toEqualTypeOf<number>();
},
error: (e) => {
expectTypeOf(e).toEqualTypeOf<Error>();
},
});
});

test('returns a union when handlers return different types', () => {
const output = createResult(true).match({
ok: (v) => v,
error: (e) => e.message,
});

expectTypeOf(output).toEqualTypeOf<number | string>();
});

test('returns a single type when handlers agree', () => {
const output = createResult(true).match({
ok: () => 'a',
error: () => 'b',
});

expectTypeOf(output).toEqualTypeOf<string>();
});

test('rejects handlers missing a branch', () => {
// @ts-expect-error: error handler is required
createResult(true).match({ ok: (v) => v });
});
});
});
});
14 changes: 14 additions & 0 deletions src/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,20 @@ class ResultImpl<T> {
getError(): Error | undefined {
return this.error;
}

match<O, E = O>({
ok,
error,
}: {
ok: (value: T) => O;
error: (value: Error) => E;
}): O | E {
if (this.isOk()) {
return ok(this.getValue() as T);
} else {
return error(this.getError() as Error);
}
}
}

export const Result = ResultImpl;
Loading