Skip to content

Commit c5933db

Browse files
authored
fix: Improve when function's type safety for type guard predicates (#325)
* fix: edit when overload return type from Exclude<T,S> to R | Exclude<T,S for when predicate call is true * test: add tests to prove changed return types * test: add runtime tests to prove changed codes' correct working
1 parent f68cf1a commit c5933db

File tree

3 files changed

+113
-32
lines changed

3 files changed

+113
-32
lines changed

src/when.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ import isUndefined from "./isUndefined";
4343
function when<T, S extends T, R>(
4444
predicate: (input: T) => input is S,
4545
callback: (input: S) => R,
46-
): (a: T) => Exclude<T, S>;
46+
): (a: T) => R | Exclude<T, S>;
4747
function when<T, R>(
4848
predicate: (input: T) => boolean,
4949
callback: (input: T) => R,
@@ -52,7 +52,7 @@ function when<T, S extends T, R>(
5252
predicate: (input: T) => input is S,
5353
callback: (input: S) => R,
5454
a: T,
55-
): Exclude<T, S>;
55+
): R | Exclude<T, S>;
5656
function when<T, R>(
5757
predicate: (input: T) => boolean,
5858
callback: (input: T) => R,

test/when.spec.ts

Lines changed: 72 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,80 @@
11
import { isString, pipe, when } from "../src";
22

33
describe("when", function () {
4-
const test = <T>(value: T) =>
5-
when(
6-
(value) => value === 100,
7-
(value) => {
8-
expect(value).toBeTruthy();
9-
},
10-
value,
11-
);
12-
const withPipe = <T>(value: T) =>
13-
pipe(
14-
value,
15-
test,
16-
when(isString, () => "Hello fxts"),
17-
);
18-
19-
it("If the input value is a number, it will be filtered out by the first 'when' function. And throw undefined.", () => {
20-
const INPUT_VALUE = 100;
21-
22-
test(INPUT_VALUE);
23-
withPipe(INPUT_VALUE);
24-
});
25-
it("If the input value is a string, it will be filtered out by the second 'when' function. And return value is 'Hello fxts'", () => {
26-
const INPUT_VALUE = "Hello World";
4+
describe("with a general predicate", () => {
5+
const test = <T>(value: T) =>
6+
when(
7+
(value) => value === 100,
8+
(value) => {
9+
expect(value).toBeTruthy();
10+
return "value is 100";
11+
},
12+
value,
13+
);
14+
const withPipe = <T>(value: T) =>
15+
pipe(
16+
value,
17+
test,
18+
when(isString, () => "Hello fxts"),
19+
);
20+
21+
it("If the input value is a number 100, the callback is executed and returns its result.", () => {
22+
const INPUT_VALUE = 100;
23+
expect(test(INPUT_VALUE)).toBe("value is 100");
24+
});
25+
26+
it("If the input value does not match predicate, it returns the original value.", () => {
27+
const INPUT_VALUE = "Hello World";
28+
expect(test(INPUT_VALUE)).toBe("Hello World");
29+
});
2730

28-
expect(test(INPUT_VALUE)).toBe("Hello World");
29-
expect(withPipe(INPUT_VALUE)).toBe("Hello fxts");
31+
it("If nothing matches, all 'when' functions will be passed.", () => {
32+
const INPUT_VALUE = [1, 2, 3, 4];
33+
expect(test(INPUT_VALUE)).toEqual(INPUT_VALUE);
34+
expect(withPipe(INPUT_VALUE)).toEqual(INPUT_VALUE);
35+
});
3036
});
31-
it("If nothing matches, all 'when' functions will be passed.", () => {
32-
const INPUT_VALUE = [1, 2, 3, 4];
3337

34-
expect(test(INPUT_VALUE)).toEqual(INPUT_VALUE);
35-
expect(withPipe(INPUT_VALUE)).toEqual(INPUT_VALUE);
38+
describe("with a type guard predicate", () => {
39+
type Shape =
40+
| { type: "circle"; radius: number }
41+
| { type: "square"; side: number };
42+
43+
const isCircle = (
44+
shape: Shape,
45+
): shape is { type: "circle"; radius: number } => shape.type === "circle";
46+
47+
it("should return the result of the callback when the type guard is true", () => {
48+
const myShape: Shape = { type: "circle", radius: 10 };
49+
const result = when(
50+
isCircle,
51+
(circle) => `A circle with radius ${circle.radius}`,
52+
myShape,
53+
);
54+
expect(result).toBe("A circle with radius 10");
55+
});
56+
57+
it("should return the original value when the type guard is false", () => {
58+
const anotherShape: Shape = { type: "square", side: 5 };
59+
const result = when(
60+
isCircle,
61+
(circle) => `A circle with radius ${circle.radius}`,
62+
anotherShape,
63+
);
64+
expect(result).toEqual(anotherShape);
65+
});
66+
67+
it("should work correctly with pipe", () => {
68+
const myShape: Shape = { type: "circle", radius: 10 };
69+
const anotherShape: Shape = { type: "square", side: 5 };
70+
71+
const shapeHandler = when(
72+
isCircle,
73+
(circle) => `A circle with radius ${circle.radius}`,
74+
);
75+
76+
expect(pipe(myShape, shapeHandler)).toBe("A circle with radius 10");
77+
expect(pipe(anotherShape, shapeHandler)).toEqual(anotherShape);
78+
});
3679
});
3780
});

type-check/when.test.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,48 @@ const check4 = when(
1919
VALUE_2,
2020
);
2121

22+
// New test cases for R | Exclude<T, S>
23+
type Shape =
24+
| { type: "circle"; radius: number }
25+
| { type: "square"; side: number };
26+
const myShape: Shape = { type: "circle", radius: 10 };
27+
const anotherShape: Shape = { type: "square", side: 5 };
28+
29+
const isCircle = (
30+
shape: Shape,
31+
): shape is { type: "circle"; radius: number } => {
32+
return shape.type === "circle";
33+
};
34+
35+
// When predicate is a type guard and is true at runtime
36+
const check5 = when(
37+
isCircle,
38+
(circle) => `A circle with radius ${circle.radius}`, // R = string
39+
myShape,
40+
);
41+
42+
// When predicate is a type guard and is false at runtime
43+
const check6 = when(
44+
isCircle,
45+
(circle) => `A circle with radius ${circle.radius}`, // R = string
46+
anotherShape,
47+
);
48+
49+
// Curried version
50+
const curriedWhen = when(
51+
isCircle,
52+
(circle) => `A circle with radius ${circle.radius}`,
53+
);
54+
const check7 = curriedWhen(myShape);
55+
2256
checks([
2357
check<typeof VALUE_1, 100, Test.Pass>(),
2458
check<typeof check1, string, Test.Pass>(),
25-
check<typeof check2, number, Test.Pass>(),
59+
check<typeof check2, string | number, Test.Pass>(),
2660
check<typeof check3, typeof VALUE_2 | string, Test.Pass>(),
2761
check<typeof check4, typeof VALUE_2 | string, Test.Pass>(),
62+
// --- Added Tests ---
63+
check<typeof check5, string, Test.Pass>(),
64+
check<typeof check6, string | { type: "square"; side: number }, Test.Pass>(),
65+
check<typeof check7, string | { type: "square"; side: number }, Test.Pass>(),
2866
]);

0 commit comments

Comments
 (0)