Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Behavior on patterns whose members are not statically-known at compile time #2

Open
hasundue opened this issue Apr 21, 2024 · 2 comments

Comments

@hasundue
Copy link

hasundue commented Apr 21, 2024

Suppose we want to provide a function that extracts values from an object by keys with a type guard, for example:

import { assertEquals, assertThrows } from "jsr:@std/assert";
import { assertType, Has } from "jsr:@std/testing/types";

Deno.test("extract", () => {
  // Users can define their own extract function
  const extractDate = (value: unknown) =>
    extract(
      value,
      ["day", "month"],
      (value: unknown): value is number => typeof value === "number",
    );

  const matched = extractDate({ day: 21, month: 4, location: "Tokyo" });
  assertType<Has<typeof matched, number[]>>(true);
  assertEquals(matched, [21, 4]);

  assertThrows(() => extractDate({ day: 21, location: "Osaka" }));
});

I find the library makes me implement this kind of function quite elegantly:

import { match, placeholder as _, } from "jsr:@core/[email protected]";
import { associateWith } from "jsr:@std/collections/associate-with";

function extract<V extends unknown>(
  from: unknown, // expected to extend Record<string, V>
  by: string[],
  guard: (value: unknown) => value is V, // type guard for each entry
): V[] {
  const pattern = associateWith(by, (it) => _(it, guard));
  // => Record<string, RegularPlaceholder<string, (value: unknown) => value is V>>
  const result = match(pattern, from);
  // => undefined
  if (!result) {
    throw new TypeError(`Could not extract expected values from ${from}.`);
  }
  return by.map((key) => result[key]));
  // => never[]
}

This code runs as expected and passes the test. But the problem here is that the type of result is inferred as undefined, which does not seem consistent with the actual behavior.

We can improve this a bit by binding the type of by to a type parameter:

import {
  match,
  placeholder as _,
  RegularPlaceholder,
} from "jsr:@core/[email protected]";
import { associateWith } from "jsr:@std/collections/associate-with";

function extract<K extends string, V extends unknown>(
  from: unknown, // expected to extend Record<K, V>
  by: K[],
  guard: (value: unknown) => value is V, // type guard for each entry
): V[] {
  const pattern = associateWith(
    by,
    (it) => _(it, guard),
  ) as { [L in K]: RegularPlaceholder<L, (value: unknown) => value is V> };

  const result = match(pattern, from);
  // => Match<{ [L in K]: RegularPlaceholder<L, (value: unknown) => value is V>; }> | undefined

  if (result === undefined) {
    throw new TypeError(`Could not extract expected values from ${from}.`);
  }
  return by.map((key) => result[key]);
  // => Type 'Match<{ [L in K]: RegularPlaceholder<L, (value: unknown) => value is V>; }>[K][]' is not assignable to type 'V[]'.
}

This code also passes the test. Now the type of result is inferred to be non-nullable, but still does not allow further operations on it.

I would like to see the type of result more "expected", like Record<string, V> | undefined in the first implementation, and Record<K, V> | undefined in the second one.

I'm totally not sure if this is a justified request in terms of the original concept of the library, but I don't beleive it a bad idea to share what I experimented with you here 😃

@tani
Copy link
Collaborator

tani commented Apr 22, 2024

Hi, now I fix the issue on issue-2 branch. Please check it.
I would like to hear your feedback. Cheers!

@hasundue
Copy link
Author

Thank you for a quick response!

Now the type of result in the first implementation is inferred as non-nullable;

function extract<V extends unknown>(
  from: unknown, // expected to extend Record<string, V>
  by: string[],
  guard: (value: unknown) => value is V, // type guard for each entry
): V[] {
  const pattern = associateWith(by, (it) => _(it, guard));
  // => Record<string, RegularPlaceholder<string, (value: unknown) => value is V>>
  const result = match(pattern, from);
  // => Record<Key, unknown> | undefined
  if (!result) {
    throw new TypeError(`Could not extract expected values from ${from}.`);
  }
  return by.map((key) => result[key]);
  // => unknown[]
}

While no changes in the other one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants