Skip to content
Closed
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
22 changes: 22 additions & 0 deletions src/match.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,28 @@ class MatchExpression<input, output> {
);
}

safeExhaustive(
handler: (value: input) => output,
handleErrorMessage?: (input: string) => unknown
): output {
if (this.state.matched) return this.state.value;

let displayedValue;
try {
displayedValue = JSON.stringify(this.input);
} catch (e) {
displayedValue = this.input;
}
const errorMessage = `Pattern matching error: no pattern matches value ${displayedValue}`;
if (handleErrorMessage) {
handleErrorMessage(errorMessage);
} else {
console.error(errorMessage);
}

return handler(this.input);
}

run(): output {
return this.exhaustive();
}
Expand Down
17 changes: 17 additions & 0 deletions src/types/Match.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,23 @@ export type Match<
: NonExhaustiveError<remainingCases>
: never;

/**
* `.safeExhaustive()` checks that all cases are handled, and returns the result value, but does not throw an error.
* instead you can supply a default value like you can with `.otherwise()` and also can provide an `error message handler` optionally
* as a second argument
*
* [Read the documentation for `.exhaustive()` on GitHub](https://github.com/gvergnaud/ts-pattern#exhaustive)
*
* */
safeExhaustive: DeepExcludeAll<i, handledCases> extends infer remainingCases
? [remainingCases] extends [never]
? (
handler: (input: i) => inferredOutput,
errorMessageHandler?: (input: string) => unknown
) => PickReturnValue<o, inferredOutput>
: NonExhaustiveError<remainingCases>
: never;

/**
* `.run()` return the resulting value.
*
Expand Down
52 changes: 52 additions & 0 deletions tests/safeExhaustive.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { match } from '../src';

type Input = 1 | 2;

describe('safeExhaustive', () => {
let consoleSpy: ReturnType<
ReturnType<(typeof jest)['spyOn']>['mockImplementation']
>;

beforeEach(() => {
consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
});

it('should not throw an error when exhaustiveness check fails', () => {
const getAnswer = () =>
match(3 as Input)
.with(1, () => 1)
.with(2, () => 2)
.safeExhaustive(() => 3);

expect(getAnswer).not.toThrowError();
});

it('should return the default value returned from the handler if no pattern matches', () => {
const result = match(3 as Input)
.with(1, () => 1)
.with(2, () => 2)
.safeExhaustive(() => 3);

expect(result).toEqual(3);
});

it('should run console.error if no errorMessageHandler provided', () => {
match(3 as Input)
.with(1, () => 1)
.with(2, () => 2)
.safeExhaustive(() => 3);

expect(consoleSpy).toHaveBeenCalled();
});

it('should run a custom handler if provided', () => {
const handler = jest.fn();

match(3 as Input)
.with(1, () => 1)
.with(2, () => 2)
.safeExhaustive(() => 3, handler);

expect(handler).toHaveBeenCalled();
});
});