Skip to content

Commit 42a461c

Browse files
committed
feat(P.object): Make P.object consistent with the object TS type
1 parent f17c02c commit 42a461c

File tree

4 files changed

+195
-195
lines changed

4 files changed

+195
-195
lines changed

README.md

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

14861486
## `P.object` predicates
14871487

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

1490-
### `P.object.empty`
1490+
### `P.object`
14911491

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

14941496
```ts
1495-
const fn = (input: string) =>
1497+
import { match, P } from 'ts-pattern';
1498+
1499+
const fn = (input: unknown) =>
14961500
match(input)
1497-
.with(P.object.empty(), () => 'Empty!')
1498-
.otherwise(() => 'Full!');
1501+
.with(P.object, () => '')
1502+
.otherwise(() => '');
1503+
1504+
console.log(fn({})); //
1505+
console.log(fn({ hello: 'world!' })); //
1506+
console.log(fn([])); //
1507+
console.log(fn(() => {})); //
1508+
1509+
console.log(fn(1, true, 'hi')); // ❌ ❌ ❌
1510+
```
1511+
1512+
### `P.object.empty()`
1513+
1514+
`P.object.empty()` matches the empty object `{}`:
1515+
1516+
```ts
1517+
import { isMatching, P } from 'ts-pattern';
1518+
1519+
console.log(isMatching(P.object.empty(), {})); // true
1520+
```
1521+
1522+
`P.object.empty()` does **not** match empty arrays, 0 values or nullish values:
1523+
1524+
```ts
1525+
import { isMatching, P } from 'ts-pattern';
14991526

1500-
console.log(fn({})); // Empty!
1527+
console.log(isMatching(P.object.empty(), [])); // false
1528+
console.log(isMatching(P.object.empty(), 0)); // false
1529+
console.log(isMatching(P.object.empty(), null)); // false
1530+
console.log(isMatching(P.object.empty(), undefined)); // false
15011531
```
15021532

15031533
## Types

src/patterns.ts

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

4042
export type { Pattern, Fn as unstable_Fn };
@@ -156,6 +158,14 @@ function arrayChainable<pattern extends Matcher<any, any, any, any, any>>(
156158
}) as any;
157159
}
158160

161+
function objectChainable<pattern extends Matcher<any, any, any, any, any>>(
162+
pattern: pattern
163+
): ObjectChainable<pattern> {
164+
return Object.assign(chainable(pattern), {
165+
empty: () => emptyObject,
166+
}) as any;
167+
}
168+
159169
/**
160170
* `P.optional(subpattern)` takes a sub pattern and returns a pattern which matches if the
161171
* key is undefined or if it is defined and the sub pattern matches its value.
@@ -635,10 +645,15 @@ function isNullish<T>(x: T | null | undefined): x is null | undefined {
635645
return x === null || x === undefined;
636646
}
637647

638-
function isObject<T>(x: T | object): x is object {
639-
return typeof x === 'object' &&
640-
!Array.isArray(x) &&
641-
x !== null
648+
function isObject(x: unknown): x is object {
649+
return !!x && (typeof x === 'object' || typeof x === 'function');
650+
}
651+
652+
function isEmptyObject(x: unknown) {
653+
if (!x || typeof x !== 'object') return false;
654+
if (Array.isArray(x)) return false;
655+
for (const _key in x) return false;
656+
return true;
642657
}
643658

644659
type AnyConstructor = abstract new (...args: any[]) => any;
@@ -1083,6 +1098,28 @@ export const symbol: SymbolPattern = chainable(when(isSymbol));
10831098
*/
10841099
export const nullish: NullishPattern = chainable(when(isNullish));
10851100

1101+
/**
1102+
* `P.object.empty()` is a pattern, matching **objects** with no keys.
1103+
*
1104+
* [Read the documentation for `P.object.empty()` on GitHub](https://github.com/gvergnaud/ts-pattern#pobjectempty)
1105+
*
1106+
* @example
1107+
* match(value)
1108+
* .with(P.object.empty(), () => 'will match on empty objects')
1109+
*/
1110+
const emptyObject: EmptyObjectPattern = chainable(when(isEmptyObject));
1111+
1112+
/**
1113+
* `P.object` is a wildcard pattern, matching any **object**.
1114+
*
1115+
* [Read the documentation for `P.object` on GitHub](https://github.com/gvergnaud/ts-pattern#pobject-predicates)
1116+
*
1117+
* @example
1118+
* match(value)
1119+
* .with(P.object, () => 'will match on objects')
1120+
**/
1121+
export const object: ObjectPattern = objectChainable(when(isObject));
1122+
10861123
/**
10871124
* `P.instanceOf(SomeClass)` is a pattern matching instances of a given class.
10881125
*
@@ -1117,40 +1154,3 @@ export function shape<input, const pattern extends Pattern<input>>(
11171154
export function shape(pattern: UnknownPattern) {
11181155
return chainable(when(isMatching(pattern)));
11191156
}
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) => {
1132-
if (!isObject(value)) return false;
1133-
1134-
for (var prop in value) return false;
1135-
return true;
1136-
},
1137-
);
1138-
1139-
const objectChainable = <pattern extends Matcher<any, any, any, any, any>>(
1140-
pattern: pattern
1141-
): ObjectChainable<pattern> =>
1142-
Object.assign(chainable(pattern), {
1143-
empty: () => chainable(intersection(pattern, emptyObject())),
1144-
}) as any;
1145-
1146-
/**
1147-
* `P.object` is a wildcard pattern, matching any **object**.
1148-
* It lets you call methods like `.empty()`, `.and`, `.or` and `.select()`
1149-
* On structural patterns, like objects and arrays.
1150-
* [Read the documentation for `P.object` on GitHub](https://github.com/gvergnaud/ts-pattern#pobject-predicates)
1151-
*
1152-
* @example
1153-
* match(value)
1154-
* .with(P.object.empty(), () => 'will match on empty objects')
1155-
**/
1156-
export const object: ObjectChainable<GuardP<unknown, object>> = objectChainable(when(isObject));

src/types/Pattern.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -164,10 +164,12 @@ type KnownPatternInternal<
164164
> =
165165
| primitives
166166
| PatternMatcher<a>
167-
| ([objs] extends [never] ? never : ObjectPattern<Readonly<MergeUnion<objs>>>)
167+
| ([objs] extends [never]
168+
? never
169+
: ObjectLiteralPattern<Readonly<MergeUnion<objs>>>)
168170
| ([arrays] extends [never] ? never : ArrayPattern<arrays>);
169171

170-
type ObjectPattern<a> =
172+
type ObjectLiteralPattern<a> =
171173
| {
172174
readonly [k in keyof a]?: Pattern<a[k]>;
173175
}
@@ -193,6 +195,11 @@ export type NullishPattern = Chainable<
193195
GuardP<unknown, null | undefined>,
194196
never
195197
>;
198+
export type ObjectPattern = ObjectChainable<GuardP<unknown, object>, never>;
199+
export type EmptyObjectPattern = Chainable<
200+
GuardExcludeP<unknown, object, never>,
201+
never
202+
>;
196203

197204
type MergeGuards<input, guard1, guard2> = [guard1, guard2] extends [
198205
GuardExcludeP<any, infer narrowed1, infer excluded1>,
@@ -668,18 +675,15 @@ export type ObjectChainable<
668675
Omit<
669676
{
670677
/**
671-
* `.empty()` matches an empty object.
678+
* `P.object.empty()` is a pattern, matching **objects** with no keys.
672679
*
673-
* [Read the documentation for `P.object.empty` on GitHub](https://github.com/gvergnaud/ts-pattern#pobjectempty)
680+
* [Read the documentation for `P.object.empty()` on GitHub](https://github.com/gvergnaud/ts-pattern#pobjectempty)
674681
*
675682
* @example
676-
* match(value)
677-
* .with(P.object.empty(), () => 'empty object')
683+
* match(value)
684+
* .with(P.object.empty(), () => 'will match on empty objects')
678685
*/
679-
empty<input>(): Chainable<
680-
GuardExcludeP<input, {}, never>,
681-
omitted | 'empty'
682-
>;
686+
empty: () => EmptyObjectPattern;
683687
},
684688
omitted
685-
>;
689+
>;

0 commit comments

Comments
 (0)