Skip to content

Export "infer" type alias supporting narrowing #327

Description

@YourFin

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:

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:

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:

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:
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;
}

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions