gvergnaud / ts-pattern

🎨 The exhaustive Pattern Matching library for TypeScript, with smart type inference.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Question: How can I make a pattern for "if array contains a certain element"?

EdmundMai opened this issue · comments

I am trying to detect if an array has a certain element. It's position in the array is unknown

Example of what I was thinking of (but doesn't work):

type Status = "loading" | "error" | "success"
const a: Status = "loading";
const b: Status = "loading";
const c: Status = "error";

 const array = [a, b, c]

 match(array)
  .with(["error"], () => console.log("array contains 1 or more errors"))
  .otherwise(() => console.log("no errors"))            // <--------- this prints out at the moment

Is there a matcher for a single element in an array and not just the head or last element specifically?

There are variadic tuple patterns as of V5, but you can only have one variadic pattern in the array, so that won't work for this particular case. TS doesn't allow this, either: [...string[], "foo", ...string[]] fails with "A rest element cannot follow another rest element." As an aside, [...Array<string>, "foo", ...Array<string>] is simplified to string[] and [...Array<string & {}>, "foo", ...Array<string & {}>] to ((string & {}) | "foo")[].

A usable type would likely have to be of the form ((string & {}) | "foo")[]. As far as I remember, there is work happening to make unions with basic types preserve literal types. That is all DX though; the actual effective result is string[]. However, I believe it's the best you can do.

Even if the type is not very meaningful (How could it be, actually? What more would [...string[], "foo", ...string[]] actually mean?), it does mean there is a type that can be used that is in some way slightly better than just keeping it as string[].

Still, that's just the typing. What's needed here is the run-time support.

The simplest/easiest is probably to use .when() instead of .with():

match(array)
  .when((arr) => arr.includes("error"), () => console.log("array contains 1 or more errors"))
  .otherwise(() => console.log("no errors"));

I assume this a very simplified example, because otherwise you could just use .includes() directly instead of TS-Pattern.

@darrylnoakes Great, I think when should suffice. Thanks for your help! It is interesting though that this library doesn't have an array includes element feature, might be something worth adding in V6

It is interesting though that this library doesn't have an array includes element feature, might be something worth adding in V6

I agree that having something built in for more advanced variants would be nifty. For example, matching an array that contains an object that matches a certain pattern, and selecting that object.

Basically, the normal use of P.array(pattern) is the analog of the .every() method of actual arrays.
What we need is an analog to .some(); simply, it must match if the input is an array with at least one element that matches the given pattern.

After not too much thought, I propose adding a .some() method to P.array, like the extensions to other wildcards such as P.string and P.number. For example:

match(array).with(
  P.array.some("error"), // Matches
  () => "array contains 1 or more errors",
);

match(array).with(
  P.array.some(P.select({ optionalKey: P.not(P.nullish()) })), // The selection is an _array_ of values that match. Or maybe an array of pairs of indices and values...
  (values) => values,
);

Adding this as a builtin to ts-pattern is a good idea. I'd probably name it P.array.includes(...) for consistency with both the [].includes method and the P.string.includes predicate pattern.