import { P, match } from 'ts-pattern';
import type { IfNever } from 'type-fest';
export namespace Partition {
export type ObjectMatchersFor<T> = Record<PropertyKey, P.Pattern<T>>;
export type MatchersFor<T> = P.Pattern<T>[] | ObjectMatchersFor<T>;
export type Result<T, Matchers extends MatchersFor<T>> = {
[K in keyof Matchers]: Array<P.infer<Matchers[K]>>;
};
export type Remaining<T, Matchers extends MatchersFor<T>> = Exclude<
T,
{ [K in keyof Matchers]: P.narrow<T, Matchers[K]> }[keyof Matchers]
>;
export type ExhaustiveResult<T, Matchers extends MatchersFor<T>> = IfNever<
Remaining<T, Matchers>,
Result<T, Matchers>,
never
>;
export type DropOthers<T, Matchers extends MatchersFor<T>> = IfNever<
Remaining<T, Matchers>,
never,
Result<T, Matchers>
>;
export type Chain<T> = {
by<Matchers extends MatchersFor<T>>(matchers: Matchers): Chain.By<T, Matchers>;
};
export namespace Chain {
export type Exhaustive<T, Matchers extends MatchersFor<T>> = {
exhuastive(): Result<T, Matchers>;
};
export type NonExhaustiveObj<T, Matchers extends ObjectMatchersFor<T>> = {
dropUnmatched(): Result<T, Matchers>;
otherwiseAt<Key extends PropertyKey>(
key: Key,
): Result<T, Matchers> & Record<Key, Remaining<T, Matchers>[]>;
};
export type NonExhaustiveTuple<T, Matchers extends P.Pattern<T>[]> = {
dropUnmatched(): Result<T, Matchers>;
keepUnmatched(): [...Result<T, Matchers>, Array<Remaining<T, Matchers>>];
};
export type By<T, Matchers extends MatchersFor<T>> = IfNever<
Remaining<T, Matchers>,
Exhaustive<T, Matchers>,
Matchers extends P.Pattern<T>[]
? NonExhaustiveTuple<T, Matchers>
: Matchers extends ObjectMatchersFor<T>
? NonExhaustiveObj<T, Matchers>
: never
>;
}
}
/**
* Partition a list by some ts-pattern patterns
*
* Called like:
* ```
* // If the argument to `.by` exhaustively cover
* // the type of `array`'s elements:
* const { bools, catNames } = partition(array)
* .by({
* bools: P.boolean,
* catNames: { kind: "cat", name: P.select(P.string) },
* })
* .exhaustive();
* // If they don't:
* const { catNames } = partition(array)
* .by({
* catNames: { kind: "cat", name: P.select(P.string) },
* })
* .dropUnmatched();
* // or
* const { bools, catNames } = partition(array)
* .by({
* catNames: { kind: "cat", name: P.select(P.string) },
* })
* .otherwiseAt("bools");
* ```
*
* Note that only the first match applies:
* ```
* const { numbers, ones } = partition([1])
* .by({
* numbers: P.number,
* ones: 1,
* })
* .exhaustive();
* // numbers = [1], ones = []
* ```
*/
export function partition<T>(iter: Iterable<T>): Partition.Chain<T> {
return {
by<Matchers extends Partition.MatchersFor<T>>(
matchers: Matchers,
): Partition.Chain.By<T, Matchers> {
return {
exhuastive: () => partitionNoOtherwise(iter, matchers),
dropUnmatched: () => partitionNoOtherwise(iter, matchers),
keepUnmatched: () => partitionOtherwiseAtEnd(iter, matchers as P.Pattern<T>[]),
otherwiseAt: <Key extends PropertyKey>(key: Key) =>
partitionOtherwiseAt(iter, key, matchers as Partition.ObjectMatchersFor<T>),
} as any;
},
};
}
function partitionNoOtherwise<T, Matchers extends Partition.MatchersFor<T>>(
iter: Iterable<T>,
patterns: Matchers,
): Partition.Result<T, Matchers> {
if (Array.isArray(patterns)) {
const ret = Array(patterns.length).fill([]) as Partition.Result<T, Matchers>;
objs: for (const obj of iter) {
for (let idx = 0; idx < patterns.length; idx++) {
if (
match(obj)
.with(patterns[idx], (o) => {
ret[idx].push(o as any);
return true;
})
.otherwise(() => false)
) {
continue objs;
}
}
}
return ret;
} else {
const ret = {} as Partition.Result<T, Matchers>;
for (const [key, _pattern] of Record.entries(patterns)) {
ret[key] = [];
}
objs: for (const obj of iter) {
for (const [key, pattern] of Record.entries(patterns)) {
if (
match(obj)
.with(pattern as any, (o) => {
ret[key].push(o as P.infer<typeof pattern>);
return true;
})
.otherwise(() => false)
) {
continue objs;
}
}
}
return ret;
}
}
function partitionOtherwiseAt<
T,
Key extends PropertyKey,
Matchers extends Partition.ObjectMatchersFor<T>,
>(
iter: Iterable<T>,
key: Key,
patterns: Matchers,
): Partition.Result<T, Matchers> & Record<Key, Partition.Remaining<T, Matchers>[]> {
const ret = {} as Partition.Result<T, Matchers> &
Record<Key, Partition.Remaining<T, Matchers>[]>;
for (const [k, _pattern] of Record.entries(patterns)) {
ret[k] = [] as any;
}
ret[key] = [] as any;
objs: for (const obj of iter) {
for (const [key, pattern] of Record.entries(patterns)) {
if (
match(obj)
.with(pattern, (o) => {
ret[key].push(o as any);
return true;
})
.otherwise(() => false)
) {
continue objs;
}
}
ret[key].push(obj as Partition.Remaining<T, Matchers>);
}
return ret;
}
function partitionOtherwiseAtEnd<T, Matchers extends P.Pattern<T>[]>(
iter: Iterable<T>,
patterns: Matchers,
): [...Partition.Result<T, Matchers>, Array<Partition.Remaining<T, Matchers>>] {
const ret = Array(patterns.length + 1).fill([]) as [
...Partition.Result<T, Matchers>,
Array<Partition.Remaining<T, Matchers>>,
];
objs: for (const obj of iter) {
for (let idx = 0; idx < patterns.length; idx++) {
if (
match(obj)
.with(patterns[idx], (o) => {
ret[idx].push(o as any);
return true;
})
.otherwise(() => false)
) {
continue objs;
}
}
ret[patterns.length].push(obj as any);
}
return ret;
}
Is your feature request related to a problem? Please describe.
I'm trying to write a
partitionfunction that usests-patterns to partition an iterable. Here are my preliminary docs:However, I cannot get the partitioned arrays to have the correct type via
ts-pattern's available exports. If I useP.narrow<OriginalT, typeof Pattern>, it correctly handles the type in cases like:But not cases like:
And if I use
P.infer, I have the opposite problem. Ideally I'd use the same type thatshas in a match arm like:From what I understand the corresponding type alias would need to take two parameters like
P.narrowto work correctly.FindSelectedlooks about right and is what appears to generates's type in that example, but I'm not sure if it would be appropriate.Describe the solution you'd like
Export
FindSelected, or a similar type.Describe alternatives you've considered
See above discussion of
P.inferandP.narrowAdditional context
Full (wip) code if curious: