Skip to content

Commit

Permalink
API: make map and set Matcher functions instead of custom patterns
Browse files Browse the repository at this point in the history
  • Loading branch information
gvergnaud committed Jan 22, 2023
1 parent 7ff4cff commit 295e8f3
Show file tree
Hide file tree
Showing 10 changed files with 187 additions and 85 deletions.
41 changes: 17 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -951,9 +951,11 @@ console.log(output);
// => 'a list of posts!'
```

### Sets
### `P.set` patterns

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

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

const output = match(input)
.with(new Set([1, 'hello']), (set) => `Set contains 1 and 'hello'`)
.with(new Set([1, 2]), (set) => `Set contains 1 and 2`)
.with(new Set([P.string]), (set) => `Set contains only strings`)
.with(new Set([P.number]), (set) => `Set contains only numbers`)
.with(P.set(1), (set) => `Set contains only 1`)
.with(P.set(P.string), (set) => `Set contains only strings`)
.with(P.set(P.number), (set) => `Set contains only numbers`)
.otherwise(() => '');

console.log(output);
// => 'Set contains 1 and 2'
// => "Set contains only numbers"
```

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

If a Set pattern contains several values, it will match if the
input Set contains each of these values.

### Maps

Patterns can be Maps. They match if the input is a Map, and if each
value match the corresponding sub-pattern.
To match a Map, you can use `P.map(keyPattern, valuePattern)`.
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
match these two sub-patterns.

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

const output = match(input)
.with(new Map([['b', 2]]), (map) => `map.get('b') is 2`)
.with(new Map([['a', P.string]]), (map) => `map.get('a') is a string`)
.with(P.map(P.string, P.number), (map) => `map's type is Map<string, number>`)
.with(P.map(P.string, P.string), (map) => `map's type is Map<string, string>`)
.with(
new Map([
['a', P.number],
['c', P.number],
]),
(map) => `map.get('a') and map.get('c') are number`
P.map(P.union('a', 'c'), P.number),
(map) => `map's type is Map<'a' | 'c', number>`
)
.otherwise(() => '');

console.log(output);
// => 'map.get('b') is 2'
// => "map's type is Map<string, number>"
```

### `P.when` patterns
Expand Down
118 changes: 115 additions & 3 deletions src/patterns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
UnknownPattern,
OptionalP,
ArrayP,
MapP,
SetP,
AndP,
OrP,
NotP,
Expand Down Expand Up @@ -68,7 +70,14 @@ export function optional<
};
}

type Elem<xs> = xs extends Array<infer x> ? x : never;
type UnwrapArray<xs> = xs extends Array<infer x> ? x : never;

type UnwrapSet<xs> = xs extends Set<infer x> ? x : never;

type UnwrapMapKey<xs> = xs extends Map<infer k, any> ? k : never;

type UnwrapMapValue<xs> = xs extends Map<any, infer v> ? v : never;

type WithDefault<a, b> = [a] extends [never] ? b : a;

/**
Expand All @@ -83,7 +92,7 @@ type WithDefault<a, b> = [a] extends [never] ? b : a;
*/
export function array<
input,
const p extends Pattern<WithDefault<Elem<input>, unknown>>
const p extends Pattern<WithDefault<UnwrapArray<input>, unknown>>
>(pattern: p): ArrayP<input, p> {
return {
[symbols.matcher]() {
Expand Down Expand Up @@ -116,6 +125,109 @@ export function array<
};
}

/**
* `P.set(subpattern)` takes a sub pattern and returns a pattern that matches
* sets if all their elements match the sub pattern.
*
* [Read `P.set` documentation on GitHub](https://github.com/gvergnaud/ts-pattern#Pset-patterns)
*
* @example
* match(value)
* .with({ users: P.set(P.string) }, () => 'will match Set<string>')
*/
export function set<
input,
const p extends Pattern<WithDefault<UnwrapSet<input>, unknown>>
>(pattern: p): SetP<input, p> {
return {
[symbols.matcher]() {
return {
match: <I>(value: I | input) => {
if (!(value instanceof Set)) return { matched: false };

let selections: Record<string, unknown[]> = {};

if (value.size === 0) {
return { matched: true, selections };
}

const selector = (key: string, value: unknown) => {
selections[key] = (selections[key] || []).concat([value]);
};

const matched = setEvery(value, (v) =>
matchPattern(pattern, v, selector)
);

return { matched, selections };
},
getSelectionKeys: () => getSelectionKeys(pattern),
};
},
};
}

const setEvery = <T>(set: Set<T>, predicate: (value: T) => boolean) => {
for (const value of set) {
if (predicate(value)) continue
return false
}
return true
}

/**
* `P.set(subpattern)` takes a sub pattern and returns a pattern that matches
* sets if all their elements match the sub pattern.
*
* [Read `P.set` documentation on GitHub](https://github.com/gvergnaud/ts-pattern#Pset-patterns)
*
* @example
* match(value)
* .with({ users: P.set(P.string) }, () => 'will match Set<string>')
*/
export function map<
input,
const pkey extends Pattern<WithDefault<UnwrapMapKey<input>, unknown>>,
const pvalue extends Pattern<WithDefault<UnwrapMapValue<input>, unknown>>
>(patternKey: pkey, patternValue: pvalue): MapP<input, pkey, pvalue> {
return {
[symbols.matcher]() {
return {
match: <I>(value: I | input) => {
if (!(value instanceof Map)) return { matched: false };

let selections: Record<string, unknown[]> = {};

if (value.size === 0) {
return { matched: true, selections };
}

const selector = (key: string, value: unknown) => {
selections[key] = (selections[key] || []).concat([value]);
};

const matched = mapEvery(value, (v, k) =>{
const keyMatch = matchPattern(patternKey, k, selector)
const valueMatch = matchPattern(patternValue, v, selector)
return keyMatch && valueMatch
});

return { matched, selections };
},
getSelectionKeys: () => getSelectionKeys(patternKey).concat(getSelectionKeys(patternValue)),
};
},
};
}

const mapEvery = <K, T>(map: Map<K, T>, predicate: (value: T, key: K) => boolean) => {
for (const [key, value] of map.entries()) {
if (predicate(value, key)) continue;
return false;
}
return true;
}

/**
* `P.intersection(...patterns)` returns a pattern which matches
* only if **every** patterns provided in parameter match the input.
Expand Down Expand Up @@ -474,7 +586,7 @@ export function instanceOf<T extends AnyConstructor>(
* )
*/
export function typed<input>(): {
array<const p extends Pattern<Elem<input>>>(pattern: p): ArrayP<input, p>;
array<const p extends Pattern<UnwrapArray<input>>>(pattern: p): ArrayP<input, p>;

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

Expand Down
3 changes: 3 additions & 0 deletions src/types/FindSelected.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ export type FindSelectionUnion<
array: i extends readonly (infer ii)[]
? MapList<FindSelectionUnion<ii, pattern>>
: never;
// FIXME: selection for map and set is supported at the value level
map: never;
set: never;
optional: MapOptional<FindSelectionUnion<i, pattern>>;
or: MapOptional<
ReduceFindSelectionUnion<i, Extract<pattern, readonly any[]>>
Expand Down
27 changes: 15 additions & 12 deletions src/types/InvertPattern.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ export type InvertPattern<p> = p extends Matcher<
not: ToExclude<InvertPattern<narrowed>>;
select: InvertPattern<narrowed>;
array: InvertPattern<narrowed>[];
map: narrowed extends [infer pk, infer pv]
? Map<InvertPattern<pk>, InvertPattern<pv>>
: never;
set: Set<InvertPattern<narrowed>>;
optional: InvertPattern<narrowed> | undefined;
and: ReduceIntersection<Extract<narrowed, readonly any[]>>;
or: ReduceUnion<Extract<narrowed, readonly any[]>>;
Expand Down Expand Up @@ -78,10 +82,6 @@ export type InvertPattern<p> = p extends Matcher<
: p extends readonly []
? []
: InvertPattern<pp>[]
: p extends Map<infer pk, infer pv>
? Map<pk, InvertPattern<pv>>
: p extends Set<infer pv>
? Set<InvertPattern<pv>>
: IsPlainObject<p> extends true
? OptionalKeys<p> extends infer optKeys
? [optKeys] extends [never]
Expand Down Expand Up @@ -163,6 +163,17 @@ type InvertPatternForExcludeInternal<p, i, empty = never> =
array: i extends readonly (infer ii)[]
? InvertPatternForExcludeInternal<subpattern, ii, empty>[]
: empty;
map: subpattern extends [infer pk, infer pv]
? i extends Map<infer ik, infer iv>
? Map<
InvertPatternForExcludeInternal<pk, ik, empty>,
InvertPatternForExcludeInternal<pv, iv, empty>
>
: empty
: empty;
set: i extends Set<infer iv>
? Set<InvertPatternForExcludeInternal<subpattern, iv, empty>>
: empty;
optional:
| InvertPatternForExcludeInternal<subpattern, i, empty>
| undefined;
Expand Down Expand Up @@ -223,14 +234,6 @@ type InvertPatternForExcludeInternal<p, i, empty = never> =
? []
: InvertPatternForExcludeInternal<pp, ii, empty>[]
: empty
: p extends Map<infer pk, infer pv>
? i extends Map<any, infer iv>
? Map<pk, InvertPatternForExcludeInternal<pv, iv, empty>>
: empty
: p extends Set<infer pv>
? i extends Set<infer iv>
? Set<InvertPatternForExcludeInternal<pv, iv, empty>>
: empty
: IsPlainObject<p> extends true
? i extends object
? [keyof p & keyof i] extends [never]
Expand Down
10 changes: 7 additions & 3 deletions src/types/Pattern.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export type MatcherType =
| 'or'
| 'and'
| 'array'
| 'map'
| 'set'
| 'select'
| 'default';

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

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

export type MapP<input, pkey, pvalue> = Matcher<input, [pkey, pvalue], 'map'>;

export type SetP<input, p> = Matcher<input, p, 'set'>;

export type AndP<input, ps> = Matcher<input, ps, 'and'>;

export type OrP<input, ps> = Matcher<input, ps, 'or'>;
Expand Down Expand Up @@ -98,8 +104,6 @@ export type UnknownPattern =
| readonly []
| readonly [UnknownPattern, ...UnknownPattern[]]
| { readonly [k: string]: UnknownPattern }
| Set<UnknownPattern>
| Map<unknown, UnknownPattern>
| Primitives
| UnknownMatcher;

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

export type PatternInternal<
a,
objs = Exclude<a, Primitives | readonly any[]>,
objs = Exclude<a, Primitives | Map<any, any> | Set<any> | readonly any[]>,
arrays = Extract<a, readonly any[]>,
primitives = Extract<a, Primitives>
> =
Expand Down
18 changes: 9 additions & 9 deletions tests/exhaustive-match.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -432,13 +432,13 @@ describe('exhaustive()', () => {
const input = new Set(['']) as Input;

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

match(input)
.with(new Set([P.string]), (x) => x)
.with(new Set([P.number]), (x) => new Set([]))
.with(P.set(P.string), (x) => x)
.with(P.set(P.number), (x) => new Set([]))
.exhaustive();
});

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

expect(
match(input)
.with(new Set([P.string]), (x) => x)
.with(P.set(P.string), (x) => x)
// @ts-expect-error
.exhaustive()
).toEqual(input);

expect(
match(input)
.with(new Set([P.string]), (x) => 1)
.with(new Set([P.number]), (x) => 2)
.with(P.set(P.string), (x) => 1)
.with(P.set(P.number), (x) => 2)
.exhaustive()
).toEqual(1);
});
Expand All @@ -467,21 +467,21 @@ describe('exhaustive()', () => {

expect(
match(input)
.with(new Map([['hello' as const, P.number]]), (x) => x)
.with(P.map('hello', P.number), (x) => x)
// @ts-expect-error
.exhaustive()
).toEqual(input);

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

expect(
match(input)
.with(new Map([['hello', 1 as const]]), (x) => x)
.with(P.map('hello', 1), (x) => x)
// @ts-expect-error
.exhaustive()
).toEqual(input);
Expand Down
Loading

0 comments on commit 295e8f3

Please sign in to comment.