ITADN

Export "infer" type alias supporting narrowing

#327OpenYourFin 创建于 2025-08-07
enhancement
Y
YourFincommented
**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 条评论