diff --git a/src/match.ts b/src/match.ts
index 278aefda..2acea099 100644
--- a/src/match.ts
+++ b/src/match.ts
@@ -126,6 +126,28 @@ class MatchExpression {
);
}
+ 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();
}
diff --git a/src/types/Match.ts b/src/types/Match.ts
index 77a27086..b81d9a63 100644
--- a/src/types/Match.ts
+++ b/src/types/Match.ts
@@ -197,6 +197,23 @@ export type Match<
: NonExhaustiveError
: 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 extends infer remainingCases
+ ? [remainingCases] extends [never]
+ ? (
+ handler: (input: i) => inferredOutput,
+ errorMessageHandler?: (input: string) => unknown
+ ) => PickReturnValue
+ : NonExhaustiveError
+ : never;
+
/**
* `.run()` return the resulting value.
*
diff --git a/tests/safeExhaustive.test.ts b/tests/safeExhaustive.test.ts
new file mode 100644
index 00000000..60ff34a1
--- /dev/null
+++ b/tests/safeExhaustive.test.ts
@@ -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();
+ });
+});