Skip to content

Commit fb3d15f

Browse files
committed
API: make map and set Matcher functions instead of custom patterns
1 parent 7ff4cff commit fb3d15f

10 files changed

Lines changed: 202 additions & 85 deletions

README.md

Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -951,9 +951,11 @@ console.log(output);
951951
// => 'a list of posts!'
952952
```
953953

954-
### Sets
954+
### `P.set` patterns
955955

956-
Patterns can be Sets.
956+
To match a Set, you can use `P.set(subpattern)`.
957+
It takes a sub-pattern, and will match if **all elements** inside the set
958+
match this sub-pattern.
957959

958960
```ts
959961
import { match, P } from 'ts-pattern';
@@ -963,26 +965,20 @@ type Input = Set<string | number>;
963965
const input: Input = new Set([1, 2, 3]);
964966

965967
const output = match(input)
966-
.with(new Set([1, 'hello']), (set) => `Set contains 1 and 'hello'`)
967-
.with(new Set([1, 2]), (set) => `Set contains 1 and 2`)
968-
.with(new Set([P.string]), (set) => `Set contains only strings`)
969-
.with(new Set([P.number]), (set) => `Set contains only numbers`)
968+
.with(P.set(1), (set) => `Set contains only 1`)
969+
.with(P.set(P.string), (set) => `Set contains only strings`)
970+
.with(P.set(P.number), (set) => `Set contains only numbers`)
970971
.otherwise(() => '');
971972

972973
console.log(output);
973-
// => 'Set contains 1 and 2'
974+
// => "Set contains only numbers"
974975
```
975976

976-
If a Set pattern contains one single wildcard pattern, it will match if
977-
each value in the input set match the wildcard.
977+
### `P.map` patterns
978978

979-
If a Set pattern contains several values, it will match if the
980-
input Set contains each of these values.
981-
982-
### Maps
983-
984-
Patterns can be Maps. They match if the input is a Map, and if each
985-
value match the corresponding sub-pattern.
979+
To match a Map, you can use `P.map(keyPattern, valuePattern)`.
980+
It takes a subpattern to match against the key, a subpattern to match agains the value, and will match if **all elements** inside this map
981+
match these two sub-patterns.
986982

987983
```ts
988984
import { match, P } from 'ts-pattern';
@@ -996,19 +992,16 @@ const input: Input = new Map([
996992
]);
997993

998994
const output = match(input)
999-
.with(new Map([['b', 2]]), (map) => `map.get('b') is 2`)
1000-
.with(new Map([['a', P.string]]), (map) => `map.get('a') is a string`)
995+
.with(P.map(P.string, P.number), (map) => `map's type is Map<string, number>`)
996+
.with(P.map(P.string, P.string), (map) => `map's type is Map<string, string>`)
1001997
.with(
1002-
new Map([
1003-
['a', P.number],
1004-
['c', P.number],
1005-
]),
1006-
(map) => `map.get('a') and map.get('c') are number`
998+
P.map(P.union('a', 'c'), P.number),
999+
(map) => `map's type is Map<'a' | 'c', number>`
10071000
)
10081001
.otherwise(() => '');
10091002

10101003
console.log(output);
1011-
// => 'map.get('b') is 2'
1004+
// => "map's type is Map<string, number>"
10121005
```
10131006

10141007
### `P.when` patterns

src/patterns.ts

Lines changed: 115 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {
77
UnknownPattern,
88
OptionalP,
99
ArrayP,
10+
MapP,
11+
SetP,
1012
AndP,
1113
OrP,
1214
NotP,
@@ -68,7 +70,14 @@ export function optional<
6870
};
6971
}
7072

71-
type Elem<xs> = xs extends Array<infer x> ? x : never;
73+
type UnwrapArray<xs> = xs extends Array<infer x> ? x : never;
74+
75+
type UnwrapSet<xs> = xs extends Set<infer x> ? x : never;
76+
77+
type UnwrapMapKey<xs> = xs extends Map<infer k, any> ? k : never;
78+
79+
type UnwrapMapValue<xs> = xs extends Map<any, infer v> ? v : never;
80+
7281
type WithDefault<a, b> = [a] extends [never] ? b : a;
7382

7483
/**
@@ -83,7 +92,7 @@ type WithDefault<a, b> = [a] extends [never] ? b : a;
8392
*/
8493
export function array<
8594
input,
86-
const p extends Pattern<WithDefault<Elem<input>, unknown>>
95+
const p extends Pattern<WithDefault<UnwrapArray<input>, unknown>>
8796
>(pattern: p): ArrayP<input, p> {
8897
return {
8998
[symbols.matcher]() {
@@ -116,6 +125,109 @@ export function array<
116125
};
117126
}
118127

128+
/**
129+
* `P.set(subpattern)` takes a sub pattern and returns a pattern that matches
130+
* sets if all their elements match the sub pattern.
131+
*
132+
* [Read `P.set` documentation on GitHub](https://github.com/gvergnaud/ts-pattern#Pset-patterns)
133+
*
134+
* @example
135+
* match(value)
136+
* .with({ users: P.set(P.string) }, () => 'will match Set<string>')
137+
*/
138+
export function set<
139+
input,
140+
const p extends Pattern<WithDefault<UnwrapSet<input>, unknown>>
141+
>(pattern: p): SetP<input, p> {
142+
return {
143+
[symbols.matcher]() {
144+
return {
145+
match: <I>(value: I | input) => {
146+
if (!(value instanceof Set)) return { matched: false };
147+
148+
let selections: Record<string, unknown[]> = {};
149+
150+
if (value.size === 0) {
151+
return { matched: true, selections };
152+
}
153+
154+
const selector = (key: string, value: unknown) => {
155+
selections[key] = (selections[key] || []).concat([value]);
156+
};
157+
158+
const matched = setEvery(value, (v) =>
159+
matchPattern(pattern, v, selector)
160+
);
161+
162+
return { matched, selections };
163+
},
164+
getSelectionKeys: () => getSelectionKeys(pattern),
165+
};
166+
},
167+
};
168+
}
169+
170+
const setEvery = <T>(set: Set<T>, predicate: (value: T) => boolean) => {
171+
for (const value of set) {
172+
if (predicate(value)) continue
173+
return false
174+
}
175+
return true
176+
}
177+
178+
/**
179+
* `P.set(subpattern)` takes a sub pattern and returns a pattern that matches
180+
* sets if all their elements match the sub pattern.
181+
*
182+
* [Read `P.set` documentation on GitHub](https://github.com/gvergnaud/ts-pattern#Pset-patterns)
183+
*
184+
* @example
185+
* match(value)
186+
* .with({ users: P.set(P.string) }, () => 'will match Set<string>')
187+
*/
188+
export function map<
189+
input,
190+
const pkey extends Pattern<WithDefault<UnwrapMapKey<input>, unknown>>,
191+
const pvalue extends Pattern<WithDefault<UnwrapMapValue<input>, unknown>>
192+
>(patternKey: pkey, patternValue: pvalue): MapP<input, pkey, pvalue> {
193+
return {
194+
[symbols.matcher]() {
195+
return {
196+
match: <I>(value: I | input) => {
197+
if (!(value instanceof Map)) return { matched: false };
198+
199+
let selections: Record<string, unknown[]> = {};
200+
201+
if (value.size === 0) {
202+
return { matched: true, selections };
203+
}
204+
205+
const selector = (key: string, value: unknown) => {
206+
selections[key] = (selections[key] || []).concat([value]);
207+
};
208+
209+
const matched = mapEvery(value, (v, k) =>{
210+
const keyMatch = matchPattern(patternKey, k, selector)
211+
const valueMatch = matchPattern(patternValue, v, selector)
212+
return keyMatch && valueMatch
213+
});
214+
215+
return { matched, selections };
216+
},
217+
getSelectionKeys: () => getSelectionKeys(patternKey).concat(getSelectionKeys(patternValue)),
218+
};
219+
},
220+
};
221+
}
222+
223+
const mapEvery = <K, T>(map: Map<K, T>, predicate: (value: T, key: K) => boolean) => {
224+
for (const [key, value] of map.entries()) {
225+
if (predicate(value, key)) continue;
226+
return false;
227+
}
228+
return true;
229+
}
230+
119231
/**
120232
* `P.intersection(...patterns)` returns a pattern which matches
121233
* only if **every** patterns provided in parameter match the input.
@@ -474,7 +586,7 @@ export function instanceOf<T extends AnyConstructor>(
474586
* )
475587
*/
476588
export function typed<input>(): {
477-
array<const p extends Pattern<Elem<input>>>(pattern: p): ArrayP<input, p>;
589+
array<const p extends Pattern<UnwrapArray<input>>>(pattern: p): ArrayP<input, p>;
478590

479591
optional<const p extends Pattern<input>>(pattern: p): OptionalP<input, p>;
480592

src/types/FindSelected.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ export type FindSelectionUnion<
5757
array: i extends readonly (infer ii)[]
5858
? MapList<FindSelectionUnion<ii, pattern>>
5959
: never;
60+
// FIXME: selection for map and set is supported at the value level
61+
map: never;
62+
set: never;
6063
optional: MapOptional<FindSelectionUnion<i, pattern>>;
6164
or: MapOptional<
6265
ReduceFindSelectionUnion<i, Extract<pattern, readonly any[]>>

src/types/InvertPattern.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ export type InvertPattern<p> = p extends Matcher<
4646
not: ToExclude<InvertPattern<narrowed>>;
4747
select: InvertPattern<narrowed>;
4848
array: InvertPattern<narrowed>[];
49+
map: narrowed extends [infer pk, infer pv]
50+
? Map<InvertPattern<pk>, InvertPattern<pv>>
51+
: never;
52+
set: Set<InvertPattern<narrowed>>;
4953
optional: InvertPattern<narrowed> | undefined;
5054
and: ReduceIntersection<Extract<narrowed, readonly any[]>>;
5155
or: ReduceUnion<Extract<narrowed, readonly any[]>>;
@@ -78,10 +82,6 @@ export type InvertPattern<p> = p extends Matcher<
7882
: p extends readonly []
7983
? []
8084
: InvertPattern<pp>[]
81-
: p extends Map<infer pk, infer pv>
82-
? Map<pk, InvertPattern<pv>>
83-
: p extends Set<infer pv>
84-
? Set<InvertPattern<pv>>
8585
: IsPlainObject<p> extends true
8686
? OptionalKeys<p> extends infer optKeys
8787
? [optKeys] extends [never]
@@ -163,6 +163,17 @@ type InvertPatternForExcludeInternal<p, i, empty = never> =
163163
array: i extends readonly (infer ii)[]
164164
? InvertPatternForExcludeInternal<subpattern, ii, empty>[]
165165
: empty;
166+
map: subpattern extends [infer pk, infer pv]
167+
? i extends Map<infer ik, infer iv>
168+
? Map<
169+
InvertPatternForExcludeInternal<pk, ik, empty>,
170+
InvertPatternForExcludeInternal<pv, iv, empty>
171+
>
172+
: empty
173+
: empty;
174+
set: i extends Set<infer iv>
175+
? Set<InvertPatternForExcludeInternal<subpattern, iv, empty>>
176+
: empty;
166177
optional:
167178
| InvertPatternForExcludeInternal<subpattern, i, empty>
168179
| undefined;
@@ -223,14 +234,6 @@ type InvertPatternForExcludeInternal<p, i, empty = never> =
223234
? []
224235
: InvertPatternForExcludeInternal<pp, ii, empty>[]
225236
: empty
226-
: p extends Map<infer pk, infer pv>
227-
? i extends Map<any, infer iv>
228-
? Map<pk, InvertPatternForExcludeInternal<pv, iv, empty>>
229-
: empty
230-
: p extends Set<infer pv>
231-
? i extends Set<infer iv>
232-
? Set<InvertPatternForExcludeInternal<pv, iv, empty>>
233-
: empty
234237
: IsPlainObject<p> extends true
235238
? i extends object
236239
? [keyof p & keyof i] extends [never]

src/types/Pattern.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export type MatcherType =
88
| 'or'
99
| 'and'
1010
| 'array'
11+
| 'map'
12+
| 'set'
1113
| 'select'
1214
| 'default';
1315

@@ -66,6 +68,10 @@ export type OptionalP<input, p> = Matcher<input, p, 'optional'>;
6668

6769
export type ArrayP<input, p> = Matcher<input, p, 'array'>;
6870

71+
export type MapP<input, pkey, pvalue> = Matcher<input, [pkey, pvalue], 'map'>;
72+
73+
export type SetP<input, p> = Matcher<input, p, 'set'>;
74+
6975
export type AndP<input, ps> = Matcher<input, ps, 'and'>;
7076

7177
export type OrP<input, ps> = Matcher<input, ps, 'or'>;
@@ -98,8 +104,6 @@ export type UnknownPattern =
98104
| readonly []
99105
| readonly [UnknownPattern, ...UnknownPattern[]]
100106
| { readonly [k: string]: UnknownPattern }
101-
| Set<UnknownPattern>
102-
| Map<unknown, UnknownPattern>
103107
| Primitives
104108
| UnknownMatcher;
105109

@@ -120,7 +124,7 @@ export type Pattern<a> = unknown extends a
120124

121125
export type PatternInternal<
122126
a,
123-
objs = Exclude<a, Primitives | readonly any[]>,
127+
objs = Exclude<a, Primitives | Map<any, any> | Set<any> | readonly any[]>,
124128
arrays = Extract<a, readonly any[]>,
125129
primitives = Extract<a, Primitives>
126130
> =

tests/exhaustive-match.test.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -432,13 +432,13 @@ describe('exhaustive()', () => {
432432
const input = new Set(['']) as Input;
433433

434434
match(input)
435-
.with(new Set([P.string]), (x) => x)
435+
.with(P.set(P.string), (x) => x)
436436
// @ts-expect-error
437437
.exhaustive();
438438

439439
match(input)
440-
.with(new Set([P.string]), (x) => x)
441-
.with(new Set([P.number]), (x) => new Set([]))
440+
.with(P.set(P.string), (x) => x)
441+
.with(P.set(P.number), (x) => new Set([]))
442442
.exhaustive();
443443
});
444444

@@ -448,15 +448,15 @@ describe('exhaustive()', () => {
448448

449449
expect(
450450
match(input)
451-
.with(new Set([P.string]), (x) => x)
451+
.with(P.set(P.string), (x) => x)
452452
// @ts-expect-error
453453
.exhaustive()
454454
).toEqual(input);
455455

456456
expect(
457457
match(input)
458-
.with(new Set([P.string]), (x) => 1)
459-
.with(new Set([P.number]), (x) => 2)
458+
.with(P.set(P.string), (x) => 1)
459+
.with(P.set(P.number), (x) => 2)
460460
.exhaustive()
461461
).toEqual(1);
462462
});
@@ -467,21 +467,21 @@ describe('exhaustive()', () => {
467467

468468
expect(
469469
match(input)
470-
.with(new Map([['hello' as const, P.number]]), (x) => x)
470+
.with(P.map('hello', P.number), (x) => x)
471471
// @ts-expect-error
472472
.exhaustive()
473473
).toEqual(input);
474474

475475
expect(
476476
match(input)
477-
.with(new Map([['hello' as const, 1 as const]]), (x) => x)
477+
.with(P.map('hello', 1), (x) => x)
478478
// @ts-expect-error
479479
.exhaustive()
480480
).toEqual(input);
481481

482482
expect(
483483
match(input)
484-
.with(new Map([['hello', 1 as const]]), (x) => x)
484+
.with(P.map('hello', 1), (x) => x)
485485
// @ts-expect-error
486486
.exhaustive()
487487
).toEqual(input);

0 commit comments

Comments
 (0)