ts-pattern icon indicating copy to clipboard operation
ts-pattern copied to clipboard

Allow specifying value directly instead of handler

Open robertherber opened this issue 3 years ago • 6 comments

I often find myself specifying handlers that just returns a value.

In those cases I would like to be able to specify that value directly, and not have to declare a handler.

For example:

type SimpleType = 1 | 2 | 3

const value: SimpleType = 1
 
match(value)
  .with(1, () => 'Got one!')
  .with(2, () => 'Got two!')
  .with(3, () => 'Got three!')

would be easier to read as

match(value)
  .with(1, 'Got one!')
  .with(2, 'Got two!')
  .with(3, 'Got three!')

This feels like a low hanging fruit to improve this great library even more.. :)

robertherber avatar Jul 20 '22 14:07 robertherber

Hi, thanks for the suggestion!

I thought a bit about this before and the main reason I decided not to implement it is that it makes the control flow more misleading. In a switch statement, conditions are run sequentially and invalid cases won't be executed, but it's not the case with function arguments. For example:

match(input)
  .with(1, returnsOne())
  .with(2, returnsTwo())
  .exhaustive()

And

switch (input) {
  case 1: return returnsOne();
  case 2: return returnsTwo();
}

Aren't equivalent. With ts-pattern, it will always call returnsOne and returnsTwo, but with switch only one of those two functions will run. Now imagine they also produce side effects, this behavior becomes a bug.

I'm wary of adding apis that are easy to misuse unless you know the fundamentals of the language well, but I'm curious of your counter arguments!

gvergnaud avatar Jul 25 '22 14:07 gvergnaud

As you noted, this could be easy to misuse. However, I, for one, would really find this "shortcut" really handy for many cases. Would a warning in the docs be good enough? i.e. That results that include function calls should always be wrapped in a function.

As a side note, maybe discourage patterns like the following, as it can be confusing to read, preferring wrapping the call in an arrow function. Issues:

  • Can look like just a variable is being passed, especially if some are wrapped and some aren't.
  • If some of the functions need to have arguments passed, they then have to be changed from unwrapped to wrapped, which is a pain to have to look at when using source control.

Not good:

match(input)
  .with(1, returnsOne)
  .with(2, returnsTwo)
  .exhaustive()

Also not good:

match(input)
  .with(1, returnsOne)
  .with(2, () => calculateStuff(x, y))
  .exhaustive()

darrylnoakes avatar Jul 25 '22 15:07 darrylnoakes

I think it's possible to misuse it anyways (as in @darrylnoakes examples). So I don't think that's a stopper for this simplification. I would think the best way to make users use it right is by:

  1. Providing good examples (which I think you're already doing @gvergnaud). Maybe warnings as well (it's good to understand all the code will be executed in those cases).
  2. Another idea could be to create an eslint rule that helps users to get it right. Enforcing callbacks when passing in something more complex than an already defined value for example.

robertherber avatar Jul 27 '22 15:07 robertherber

It also makes returning functions from a match a little trickier (impossible?), right? Not sure it's worth it to save the () =>, but you could always write your own const-like function:

function c<T>(t: T) {
  return () => t;
}

match(input)
  .with(1, c("Got 1"))
  .exhaustive();

scotttrinh avatar May 26 '23 17:05 scotttrinh