Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added a safe exhaustiveness check #219

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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();
});
});