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

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

Open
EdmundMai opened this issue Sep 21, 2023 · 6 comments

Comments

@EdmundMai
Copy link

EdmundMai commented Sep 21, 2023

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?

@darrylnoakes
Copy link

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.

@darrylnoakes
Copy link

darrylnoakes commented Sep 21, 2023

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.

@EdmundMai
Copy link
Author

@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

@darrylnoakes
Copy link

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,
);

@gvergnaud
Copy link
Owner

gvergnaud commented Sep 25, 2023

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.

@dylanphantixr
Copy link

I have another solution in the interim:

enum Avv {
  A = "A",
  B = "B",
  C = "C",
}

const Bvv = [Avv.A, Avv.B] as const
const Cvv = [Avv.C] as const

interface Dvv {
  readonly kind: Avv
  readonly value: number
}

function isIn<T>(values: readonly T[]): (x: any) => x is T {
  return (x): x is T => values.includes(x)
}

const obj: Dvv = {
      kind: Avv.A,
      value: 1,
    } as any

match(obj)
      .with({ kind: P.when(isIn(Bvv)) }, () => {})
      .with({ kind: P.when(isIn(Cvv)) }, () => {})
      .exhaustive()

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

4 participants