Export "infer" type alias supporting narrowing

Author: YourFinCreated Aug 7, 2025Updated Aug 7, 2025
Labelsenhancement

Is your feature request related to a problem? Please describe.

I'm trying to write a partition function that uses ts-patterns 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(console.log)

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

Full (wip) code if curious:
bash
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;
}