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(); + }); +});