Skip to content

Commit ea7a937

Browse files
committed
Add object, object.empty pattern and tests
1 parent 27f8749 commit ea7a937

File tree

4 files changed

+146
-2
lines changed

4 files changed

+146
-2
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1497,7 +1497,7 @@ const fn = (input: string) =>
14971497
.with(P.object.empty(), () => 'Empty!')
14981498
.otherwise(() => 'Full!');
14991499

1500-
console.log(fn('{}')); // Empty
1500+
console.log(fn({})); // Empty!
15011501
```
15021502

15031503
## Types

src/patterns.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
StringChainable,
3535
ArrayChainable,
3636
Variadic,
37+
ObjectChainable,
3738
} from './types/Pattern';
3839

3940
export type { Pattern, Fn as unstable_Fn };
@@ -634,6 +635,12 @@ function isNullish<T>(x: T | null | undefined): x is null | undefined {
634635
return x === null || x === undefined;
635636
}
636637

638+
function isObject<T>(x: T | object): x is object {
639+
return typeof x === 'object' &&
640+
!Array.isArray(x) &&
641+
x !== null
642+
}
643+
637644
type AnyConstructor = abstract new (...args: any[]) => any;
638645

639646
function isInstanceOf<T extends AnyConstructor>(classConstructor: T) {
@@ -1110,3 +1117,35 @@ export function shape<input, const pattern extends Pattern<input>>(
11101117
export function shape(pattern: UnknownPattern) {
11111118
return chainable(when(isMatching(pattern)));
11121119
}
1120+
1121+
/**
1122+
* `P.object.empty()` is a pattern, matching **objects** with no keys.
1123+
*
1124+
* [Read the documentation for `P.object.empty` on GitHub](https://github.com/gvergnaud/ts-pattern#pobjectempty)
1125+
*
1126+
* @example
1127+
* match(value)
1128+
* .with(P.object.empty(), () => 'will match on empty objects')
1129+
*/
1130+
const emptyObject = <input>(): GuardExcludeP<input, object, never> => when(
1131+
(value) => value && typeof value === 'object' && Object.keys(value).length === 0,
1132+
);
1133+
1134+
const objectChainable = <pattern extends Matcher<any, any, any, any, any>>(
1135+
pattern: pattern
1136+
): ObjectChainable<pattern> =>
1137+
Object.assign(chainable(pattern), {
1138+
empty: () => objectChainable(intersection(pattern, emptyObject())),
1139+
}) as any;
1140+
1141+
/**
1142+
* `P.object` is a wildcard pattern, matching any **object**.
1143+
* It lets you call methods like `.empty()`, `.and`, `.or` and `.select()`
1144+
* On structural patterns, like objects and arrays.
1145+
* [Read the documentation for `P.object` on GitHub](https://github.com/gvergnaud/ts-pattern#pobject-predicates)
1146+
*
1147+
* @example
1148+
* match(value)
1149+
* .with(P.object.empty(), () => 'will match on empty objects')
1150+
**/
1151+
export const object: ObjectChainable<any> = objectChainable(when(isObject));

src/types/Pattern.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export type MatcherType =
1515
| 'or'
1616
| 'and'
1717
| 'array'
18+
| 'object'
1819
| 'map'
1920
| 'set'
2021
| 'select'
@@ -97,6 +98,8 @@ export type CustomP<input, pattern, narrowedOrFn> = Matcher<
9798

9899
export type ArrayP<input, p> = Matcher<input, p, 'array'>;
99100

101+
export type ObjectP<input, p> = Matcher<input, p, 'object'>;
102+
100103
export type OptionalP<input, p> = Matcher<input, p, 'optional'>;
101104

102105
export type MapP<input, pkey, pvalue> = Matcher<input, [pkey, pvalue], 'map'>;
@@ -191,7 +194,6 @@ export type NullishPattern = Chainable<
191194
GuardP<unknown, null | undefined>,
192195
never
193196
>;
194-
195197
type MergeGuards<input, guard1, guard2> = [guard1, guard2] extends [
196198
GuardExcludeP<any, infer narrowed1, infer excluded1>,
197199
GuardExcludeP<any, infer narrowed2, infer excluded2>
@@ -658,3 +660,26 @@ export type ArrayChainable<
658660
},
659661
omitted
660662
>;
663+
664+
export type ObjectChainable<
665+
pattern,
666+
omitted extends string = never
667+
> = Chainable<pattern, omitted> &
668+
Omit<
669+
{
670+
/**
671+
* `.empty()` matches an empty object.
672+
*
673+
* [Read the documentation for `P.object.empty` on GitHub](https://github.com/gvergnaud/ts-pattern#pobjectempty)
674+
*
675+
* @example
676+
* match(value)
677+
* .with(P.object.empty(), () => 'empty object')
678+
*/
679+
empty<input>(): ObjectChainable<
680+
ObjectP<input, Record<string, never>>,
681+
omitted | 'empty'
682+
>;
683+
},
684+
omitted
685+
>;

tests/object.test.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { Expect, Equal } from '../src/types/helpers';
2+
import { P, match } from '../src';
3+
4+
describe('Object', () => {
5+
it('should match exact object', () => {
6+
const fn = () => 'hello';
7+
8+
const res = match({ str: fn() })
9+
.with({ str: 'world' }, (obj) => {
10+
type t = Expect<Equal<typeof obj, { str: 'world' }>>;
11+
return obj.str;
12+
})
13+
.with(P.object, (obj) => {
14+
type t = Expect<Equal<typeof obj, {
15+
readonly str: string;
16+
}>>;
17+
return 'not found';
18+
})
19+
.exhaustive();
20+
expect(res).toEqual('not found');
21+
});
22+
23+
it('should match object with nested objects', () => {
24+
const res = match({ x: { y: 1 } })
25+
.with({ x: { y: 1 } }, (obj) => {
26+
type t = Expect<Equal<typeof obj, { readonly x: { readonly y: 1 } }>>;
27+
return 'yes';
28+
})
29+
.with(P.object, (obj) => {
30+
type t = Expect<Equal<typeof obj, never>>;
31+
return 'no';
32+
})
33+
.exhaustive();
34+
expect(res).toEqual('yes');
35+
});
36+
37+
it('should match object with nested objects and arrays', () => {
38+
const res = match({ x: { y: [1] } })
39+
.with({ x: { y: [1] } }, (obj) => {
40+
type t = Expect<Equal<typeof obj, { x: { y: [1] } }>>;
41+
return 'yes';
42+
})
43+
.with(P.object, (obj) => {
44+
type t = Expect<Equal<typeof obj, { readonly x: { readonly y: readonly [1]}}>>;
45+
return 'no';
46+
})
47+
.exhaustive();
48+
expect(res).toEqual('yes');
49+
});
50+
51+
it('should match empty object', () => {
52+
const res = match({})
53+
.with(P.object.empty(), (obj) => {
54+
type t = Expect<Equal<typeof obj, {}>>;
55+
56+
return 'yes';
57+
})
58+
.with(P.object, (obj) => {
59+
type t = Expect<Equal<typeof obj, never>>;
60+
61+
return 'no';
62+
})
63+
.exhaustive();
64+
expect(res).toEqual('yes');
65+
});
66+
67+
it('should match object with optional properties', () => {
68+
const res = match({ x: 1 })
69+
.with(P.object.empty(), (obj) => {
70+
type t = Expect<Equal<typeof obj, { readonly x: 1; }>>;
71+
return 'no';
72+
})
73+
.with(P.object, (obj) => {
74+
type t = Expect<Equal<typeof obj, never>>;
75+
return 'yes';
76+
})
77+
.exhaustive();
78+
expect(res).toEqual('yes');
79+
});
80+
});

0 commit comments

Comments
 (0)