Skip to content

Commit e6bf0af

Browse files
committed
feat(P.object): Make P.object consistent with the object TS type
1 parent 2f49cf2 commit e6bf0af

File tree

4 files changed

+194
-191
lines changed

4 files changed

+194
-191
lines changed

README.md

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1500,19 +1500,49 @@ console.log(fn(-3.141592), fn(7)); // logs '✅ ❌'
15001500

15011501
## `P.object` predicates
15021502

1503-
`P.object` has a number of methods to help you match on specific object.
1503+
`P.object` is itself a pattern, but also a module containing predicates related to object types.
15041504

1505-
### `P.object.empty`
1505+
### `P.object`
15061506

1507-
`P.object.empty` matches empty object
1507+
`P.object` matches any value assignable to the `object` TypeScript type. This includes all object literals, but also **arrays** and **functions**!
1508+
1509+
`P.object` does not match primitive types, like strings or numbers.
15081510

15091511
```ts
1510-
const fn = (input: string) =>
1512+
import { match, P } from 'ts-pattern';
1513+
1514+
const fn = (input: unknown) =>
15111515
match(input)
1512-
.with(P.object.empty(), () => 'Empty!')
1513-
.otherwise(() => 'Full!');
1516+
.with(P.object, () => '')
1517+
.otherwise(() => '');
1518+
1519+
console.log(fn({})); //
1520+
console.log(fn({ hello: 'world!' })); //
1521+
console.log(fn([])); //
1522+
console.log(fn(() => {})); //
1523+
1524+
console.log(fn(1, true, 'hi')); // ❌ ❌ ❌
1525+
```
1526+
1527+
### `P.object.empty()`
1528+
1529+
`P.object.empty()` matches the empty object `{}`:
1530+
1531+
```ts
1532+
import { isMatching, P } from 'ts-pattern';
1533+
1534+
console.log(isMatching(P.object.empty(), {})); // true
1535+
```
1536+
1537+
`P.object.empty()` does **not** match empty arrays, 0 values or nullish values:
1538+
1539+
```ts
1540+
import { isMatching, P } from 'ts-pattern';
15141541

1515-
console.log(fn({})); // Empty!
1542+
console.log(isMatching(P.object.empty(), [])); // false
1543+
console.log(isMatching(P.object.empty(), 0)); // false
1544+
console.log(isMatching(P.object.empty(), null)); // false
1545+
console.log(isMatching(P.object.empty(), undefined)); // false
15161546
```
15171547

15181548
## Types

src/patterns.ts

Lines changed: 41 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ import {
3636
Variadic,
3737
NonNullablePattern,
3838
ObjectChainable,
39+
ObjectPattern,
40+
EmptyObjectPattern,
3941
} from './types/Pattern';
4042

4143
export type { Pattern, Fn as unstable_Fn };
@@ -157,6 +159,14 @@ function arrayChainable<pattern extends Matcher<any, any, any, any, any>>(
157159
}) as any;
158160
}
159161

162+
function objectChainable<pattern extends Matcher<any, any, any, any, any>>(
163+
pattern: pattern
164+
): ObjectChainable<pattern> {
165+
return Object.assign(chainable(pattern), {
166+
empty: () => emptyObject,
167+
}) as any;
168+
}
169+
160170
/**
161171
* `P.optional(subpattern)` takes a sub pattern and returns a pattern which matches if the
162172
* key is undefined or if it is defined and the sub pattern matches its value.
@@ -640,8 +650,15 @@ function isNonNullable(x: unknown): x is {} {
640650
return x !== null && x !== undefined;
641651
}
642652

643-
function isObject<T>(x: T | object): x is object {
644-
return typeof x === 'object' && !Array.isArray(x) && x !== null;
653+
function isObject(x: unknown): x is object {
654+
return !!x && (typeof x === 'object' || typeof x === 'function');
655+
}
656+
657+
function isEmptyObject(x: unknown) {
658+
if (!x || typeof x !== 'object') return false;
659+
if (Array.isArray(x)) return false;
660+
for (const _key in x) return false;
661+
return true;
645662
}
646663

647664
type AnyConstructor = abstract new (...args: any[]) => any;
@@ -1096,6 +1113,28 @@ export const nullish: NullishPattern = chainable(when(isNullish));
10961113
*/
10971114
export const nonNullable: NonNullablePattern = chainable(when(isNonNullable));
10981115

1116+
/**
1117+
* `P.object.empty()` is a pattern, matching **objects** with no keys.
1118+
*
1119+
* [Read the documentation for `P.object.empty()` on GitHub](https://github.com/gvergnaud/ts-pattern#pobjectempty)
1120+
*
1121+
* @example
1122+
* match(value)
1123+
* .with(P.object.empty(), () => 'will match on empty objects')
1124+
*/
1125+
const emptyObject: EmptyObjectPattern = chainable(when(isEmptyObject));
1126+
1127+
/**
1128+
* `P.object` is a wildcard pattern, matching any **object**.
1129+
*
1130+
* [Read the documentation for `P.object` on GitHub](https://github.com/gvergnaud/ts-pattern#pobject-predicates)
1131+
*
1132+
* @example
1133+
* match(value)
1134+
* .with(P.object, () => 'will match on objects')
1135+
**/
1136+
export const object: ObjectPattern = objectChainable(when(isObject));
1137+
10991138
/**
11001139
* `P.instanceOf(SomeClass)` is a pattern matching instances of a given class.
11011140
*
@@ -1130,39 +1169,3 @@ export function shape<input, const pattern extends Pattern<input>>(
11301169
export function shape(pattern: UnknownPattern) {
11311170
return chainable(when(isMatching(pattern)));
11321171
}
1133-
1134-
/**
1135-
* `P.object.empty()` is a pattern, matching **objects** with no keys.
1136-
*
1137-
* [Read the documentation for `P.object.empty` on GitHub](https://github.com/gvergnaud/ts-pattern#pobjectempty)
1138-
*
1139-
* @example
1140-
* match(value)
1141-
* .with(P.object.empty(), () => 'will match on empty objects')
1142-
*/
1143-
const emptyObject = <input>(): GuardExcludeP<input, object, never> =>
1144-
when((value) => {
1145-
if (!isObject(value)) return false;
1146-
1147-
for (var prop in value) return false;
1148-
return true;
1149-
});
1150-
1151-
const objectChainable = <pattern extends Matcher<any, any, any, any, any>>(
1152-
pattern: pattern
1153-
): ObjectChainable<pattern> =>
1154-
Object.assign(chainable(pattern), {
1155-
empty: () => chainable(intersection(pattern, emptyObject())),
1156-
}) as any;
1157-
1158-
/**
1159-
* `P.object` is a wildcard pattern, matching any **object**.
1160-
* It lets you call methods like `.empty()`, `.and`, `.or` and `.select()`
1161-
* On structural patterns, like objects and arrays.
1162-
* [Read the documentation for `P.object` on GitHub](https://github.com/gvergnaud/ts-pattern#pobject-predicates)
1163-
*
1164-
* @example
1165-
* match(value)
1166-
* .with(P.object.empty(), () => 'will match on empty objects')
1167-
**/
1168-
export const object: ObjectChainable<GuardP<unknown, object>> = objectChainable(when(isObject));

src/types/Pattern.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -159,10 +159,12 @@ type KnownPatternInternal<
159159
> =
160160
| primitives
161161
| PatternMatcher<a>
162-
| ([objs] extends [never] ? never : ObjectPattern<Readonly<MergeUnion<objs>>>)
162+
| ([objs] extends [never]
163+
? never
164+
: ObjectLiteralPattern<Readonly<MergeUnion<objs>>>)
163165
| ([arrays] extends [never] ? never : ArrayPattern<arrays>);
164166

165-
type ObjectPattern<a> =
167+
type ObjectLiteralPattern<a> =
166168
| {
167169
readonly [k in keyof a]?: Pattern<a[k]>;
168170
}
@@ -188,6 +190,11 @@ export type NullishPattern = Chainable<
188190
GuardP<unknown, null | undefined>,
189191
never
190192
>;
193+
export type ObjectPattern = ObjectChainable<GuardP<unknown, object>, never>;
194+
export type EmptyObjectPattern = Chainable<
195+
GuardExcludeP<unknown, object, never>,
196+
never
197+
>;
191198

192199
export type NonNullablePattern = Chainable<GuardP<unknown, {}>, never>;
193200

@@ -665,18 +672,15 @@ export type ObjectChainable<
665672
Omit<
666673
{
667674
/**
668-
* `.empty()` matches an empty object.
675+
* `P.object.empty()` is a pattern, matching **objects** with no keys.
669676
*
670-
* [Read the documentation for `P.object.empty` on GitHub](https://github.com/gvergnaud/ts-pattern#pobjectempty)
677+
* [Read the documentation for `P.object.empty()` on GitHub](https://github.com/gvergnaud/ts-pattern#pobjectempty)
671678
*
672679
* @example
673-
* match(value)
674-
* .with(P.object.empty(), () => 'empty object')
680+
* match(value)
681+
* .with(P.object.empty(), () => 'will match on empty objects')
675682
*/
676-
empty<input>(): Chainable<
677-
GuardExcludeP<input, {}, never>,
678-
omitted | 'empty'
679-
>;
683+
empty: () => EmptyObjectPattern;
680684
},
681685
omitted
682686
>;

0 commit comments

Comments
 (0)