Export "infer" type alias supporting narrowing
enhancement
**Is your feature request related to a problem? Please describe.**
I'm trying to write a `partition` function that uses `ts-pattern`s to partition an iterable. Here are my preliminary docs:
* 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 = []
* ```
However, I cannot get the partitioned arrays to have the correct type via `ts-pattern`'s available exports. If I use `P.narrow<OriginalT, typeof Pattern>`, it correctly handles the type in cases like:
```typescript
const { cats } = partition([{ kind: "cat" as const, says: "meow"}]).by({
cats: { type: "cat" },
}).exhaustive();
cats.forEach((cat) => console.log(cat.says)
```
But not cases like:
```typescript
const { sayings } = partition([{ kind: "cat" as const, says: "meow"}]).by({
sayings: { type: "cat", says: P.select(P.string)) },
}).exhaustive();
sayings.forEach((cat) => console.log(cat.says)
```
And if I use `P.infer`, I have the opposite problem. Ideally I'd use the same type that `s` has in a match arm like:
```typescript
match({ kind: "cat", says: "meow" })
.with({ kind: "cat" }, (s) => s)
.exhaustive();
```
From what I understand the corresponding type alias would need to take two parameters like `P.narrow` to work correctly. `FindSelected` looks about right and is what appears to generate `s`'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.infer` and `P.narrow`
**Additional context**
<details> <summary>Full (wip) code if curious:</summary>
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;
}
</details>
1 条评论