Skip to content

Commit 1831e29

Browse files
committed
Fix multiple issues based on PR review feedback
- Comment: Seems like chainable would be enough here, since you can't chain empty several times - Comment: I think we could make this more efficient by using a for in loop instead of Object.keys and breaking the loop by returning false if an object own property is encountered. - Comment: It should just be Chainable here as well - Comment: I'm not sure a new pattern type is necessary here because both patterns you added are implemented with guards - Comment: Could you add test covering how P.object behaves with more inputs: Functions, Primitive values, Null. It should catch all values that are assignable to the object type, and type narrowing and exhaustive should both work - Comment: Could you remove this diff?
1 parent ea7a937 commit 1831e29

File tree

3 files changed

+72
-6
lines changed

3 files changed

+72
-6
lines changed

src/patterns.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1128,14 +1128,19 @@ export function shape(pattern: UnknownPattern) {
11281128
* .with(P.object.empty(), () => 'will match on empty objects')
11291129
*/
11301130
const emptyObject = <input>(): GuardExcludeP<input, object, never> => when(
1131-
(value) => value && typeof value === 'object' && Object.keys(value).length === 0,
1131+
(value) => {
1132+
if (!isObject(value)) return false;
1133+
1134+
for (var prop in value) return false;
1135+
return true;
1136+
},
11321137
);
11331138

11341139
const objectChainable = <pattern extends Matcher<any, any, any, any, any>>(
11351140
pattern: pattern
11361141
): ObjectChainable<pattern> =>
11371142
Object.assign(chainable(pattern), {
1138-
empty: () => objectChainable(intersection(pattern, emptyObject())),
1143+
empty: () => chainable(intersection(pattern, emptyObject())),
11391144
}) as any;
11401145

11411146
/**

src/types/Pattern.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ export type MatcherType =
1515
| 'or'
1616
| 'and'
1717
| 'array'
18-
| 'object'
1918
| 'map'
2019
| 'set'
2120
| 'select'
@@ -98,7 +97,7 @@ export type CustomP<input, pattern, narrowedOrFn> = Matcher<
9897

9998
export type ArrayP<input, p> = Matcher<input, p, 'array'>;
10099

101-
export type ObjectP<input, p> = Matcher<input, p, 'object'>;
100+
export type ObjectP<input, p> = Matcher<input, p>;
102101

103102
export type OptionalP<input, p> = Matcher<input, p, 'optional'>;
104103

@@ -194,6 +193,7 @@ export type NullishPattern = Chainable<
194193
GuardP<unknown, null | undefined>,
195194
never
196195
>;
196+
197197
type MergeGuards<input, guard1, guard2> = [guard1, guard2] extends [
198198
GuardExcludeP<any, infer narrowed1, infer excluded1>,
199199
GuardExcludeP<any, infer narrowed2, infer excluded2>
@@ -676,7 +676,7 @@ export type ObjectChainable<
676676
* match(value)
677677
* .with(P.object.empty(), () => 'empty object')
678678
*/
679-
empty<input>(): ObjectChainable<
679+
empty<input>(): Chainable<
680680
ObjectP<input, Record<string, never>>,
681681
omitted | 'empty'
682682
>;

tests/object.test.ts

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,66 @@ describe('Object', () => {
2020
expect(res).toEqual('not found');
2121
});
2222

23+
it('when input is a Function, it should not match as an exact object', () => {
24+
const fn = () => () => {};
25+
26+
const res = match(fn())
27+
.with(P.object, (obj) => {
28+
type t = Expect<Equal<typeof obj, () => void>>;
29+
return 'not found';
30+
})
31+
.otherwise(() => 'not found');
32+
expect(res).toEqual('not found');
33+
})
34+
35+
it('when input is a Number (a primitive value), it should not be matched as an exact object', () => {
36+
const fn = () => 1_000_000;
37+
38+
const res = match(fn())
39+
.with(P.object, (obj) => {
40+
type t = Expect<Equal<typeof obj, number>>;
41+
return 'not found';
42+
})
43+
.otherwise(() => 'not found');
44+
expect(res).toEqual('not found');
45+
})
46+
47+
it('when input is a String (a primitive value), it should not be matched as an exact object', () => {
48+
const fn = () => 'hello';
49+
50+
const res = match(fn())
51+
.with(P.object, (obj) => {
52+
type t = Expect<Equal<typeof obj, string>>;
53+
return 'not found';
54+
})
55+
.otherwise(() => 'not found');
56+
expect(res).toEqual('not found');
57+
})
58+
59+
it('when input is a Boolean (a primitive value), it should not be matched as an exact object', () => {
60+
const fn = () => true;
61+
62+
const res = match(fn())
63+
.with(P.object, (obj) => {
64+
type t = Expect<Equal<typeof obj, boolean>>;
65+
return 'not found';
66+
})
67+
.otherwise(() => 'not found');
68+
expect(res).toEqual('not found');
69+
})
70+
71+
it('when input is Null, it should not be matched as an exact object', () => {
72+
const fn = () => null;
73+
74+
const res = match(fn())
75+
.with(P.object, (obj) => {
76+
type t = Expect<Equal<typeof obj, null>>;
77+
return 'not found';
78+
})
79+
.otherwise(() => 'not found');
80+
expect(res).toEqual('not found');
81+
})
82+
2383
it('should match object with nested objects', () => {
2484
const res = match({ x: { y: 1 } })
2585
.with({ x: { y: 1 } }, (obj) => {
@@ -45,6 +105,7 @@ describe('Object', () => {
45105
return 'no';
46106
})
47107
.exhaustive();
108+
48109
expect(res).toEqual('yes');
49110
});
50111

@@ -64,7 +125,7 @@ describe('Object', () => {
64125
expect(res).toEqual('yes');
65126
});
66127

67-
it('should match object with optional properties', () => {
128+
it('should properly match an object against the P.object pattern, even with optional properties', () => {
68129
const res = match({ x: 1 })
69130
.with(P.object.empty(), (obj) => {
70131
type t = Expect<Equal<typeof obj, { readonly x: 1; }>>;

0 commit comments

Comments
 (0)