match
match copied to clipboard
Behavior on patterns whose members are not statically-known at compile time
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 😃