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

Allow otherwise to chain onto exhaustive #144

Open
Liam-Tait opened this issue Feb 8, 2023 · 9 comments
Open

Allow otherwise to chain onto exhaustive #144

Liam-Tait opened this issue Feb 8, 2023 · 9 comments
Labels
enhancement New feature or request

Comments

@Liam-Tait
Copy link

Is your feature request related to a problem? Please describe.
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

We have types, however they are not completely safe. I'd like to be able to ensure the exhaustive type check is run for case such as union where we want to handle where , but they also run an otherwise to prevent throwing of an error.

This means we can have the type checking where a type is partially complete.

Describe the solution you'd like
A clear and concise description of what you want to happen.

Add a new method exhaustiveOtherwise which does the exhaustive type check with a otherwise handler

const unit: 'days' | 'hours' = 'something'

match(unit)
.with('days', () => 'd')
.with('hours', () => 'h')
.exhaustiveOtherwise(() => 'never') // safety to prevent error

Describe alternatives you've considered
A clear and concise description of any alternative solutions or features you've considered.

Allow otherwise to chain onto the end of exhaustive

const unit: 'days' | 'hours' = 'something'

match(unit)
.with('days', () => 'd')
.with('hours', () => 'h')
.exhaustive()
.otherwise(() => 'never')

Add validation such as zod to the data to handle incorrect data (hard as I would like to transition new code to be type safe without having to re-write all types)

Additional context
Add any other context or screenshots about the feature request here.

@Liam-Tait Liam-Tait added the enhancement New feature or request label Feb 8, 2023
@gvergnaud
Copy link
Owner

Hey! I think I'd use .exhaustive() and wrap the expression into a try .. catch to get the behavior you expect. Would that solve your problem?

@eboody
Copy link

eboody commented Feb 22, 2023

Im kind of confused about this use-case. In what context would you have a scenario that matches your example of

const unit: 'days' | 'hours' = 'something'

?

@Liam-Tait
Copy link
Author

Wrapping the expressions in try .. catch would work, but makes the code verbose and a bit more complicated because of scoping

const value = match(unit)
  .with('days', () => 'd')
  .with('hours', () => 'h')
  .exhaustiveOtherwise(() => 'never')

vs

let value: string
try {
  value = match(unit)
    .with("days", () => "d")
    .with("hours", () => "h")
    .exhaustive()
} catch (error) {
  value = "never"
}

What I am working with is not an ideal case, the application has partially transitioned to TypeScript. Some types are only partially correct right now.

Adding a catch or otherwise that works with exhaustive means I can start taking advantage of exhaustiveness checking, while preventing errors at runtime while types cannot be fully trusted. Maybe the ideal approach is to have everything be unknown and handle it that way.

I don't know if this is possible, but having a P.unknown matcher that only matched unknown would also work, as then I could add this to types that are not complete yet, this would more accurately show and handle my current situation

const unit: 'days' | 'hours' | unknown;
const value = match(unit)
  .with("days", () => "d")
  .with("hours", () => "h")
  .with(P.unknown, () => 'never')
  .exhaustive()

I want to be able to:

  • Use exhaustiveness checking to check all defined types
  • Handle when the type does not match any defined type or matches unknown
  • Refactored "easily" to the ideal exhaustive once confidence is higher in the types (validation, types are correct)

One of the situations I have is where data is persisted in local storage and loaded back in without any validation, the types "should" be the same, but can be different based on an old stored version, renaming, user changing etc

@XiNiHa
Copy link

XiNiHa commented Mar 2, 2023

How about just adding an optional argument to .exhaustive() that works as same as .otherwise()? So just like:

declare const input: 'a' | 'b';
const mode = match(input)
  .with('a', () => Mode.A)
  .with('b', () => Mode.B)
  .exhaustive(() => Mode.Fallback)

In addition to that, it'd be better to have the value passed to the callback as unknown, (since it's impossible to infer valid type in this case) so users can use their own errors than the provided one:

declare const input: 'a' | 'b';
const mode = match(input)
  .with('a', () => Mode.A)
  .with('b', () => Mode.B)
  .exhaustive(v => {
    throw new ValidationError(`Unknown mode identifier: ${v}`)
  })

@eboody
Copy link

eboody commented Mar 2, 2023

I dunno it looks to me like there's a fundamental disagreement about what "exhaustive" means in this context.

In my opinion "exhaustive" loses it's meaning if it can be used in a scenario where you need a fall back

Also, as I understand it there is a performance cost to using exhaustive, so I think it's purpose is pretty narrowly defined.

If you're getting input that you can't type narrow into exhaustive checks then I think there's a strong case to be made that it would be inappropriate to use exhaustive from a conceptual as well as a performance standpoint.

@LavransBjerkestrand
Copy link

@gvergnaud are you open to PRs regarding this? Ref #38
Chaining, .exhaustiveOtherwise or some other API would be very useful at times.

Like oguimbal said:

I want the types to break. Not my app.

@LavransBjerkestrand
Copy link

Could use a try catch but I prefer not to. Here is a workaround (kind of)

import { P, match } from 'ts-pattern'

type Status = 'active' | 'inactive' | 'pending'

const status = 'active' as Status

match(status)
  .with('active', () => '...')
  .with('inactive', () => '...')
  // .with('pending', () => '...')
  // ^ uncomment resolve NonExhaustiveError
  .with(P.not({}), () => 'fallback')
  .exhaustive()
// ~~~~~~~~~~ This expression is not callable.
//              Type 'NonExhaustiveError<"pending">' has no call signatures.

@gustavopch
Copy link

gustavopch commented Aug 11, 2024

Taking inspiration from Effect's orElseAbsurd, I propose this solution:

declare const unit: string

match(unit)
  .with('days', () => 'd')
  .with('hours', () => 'h')
  .otherwiseAbsurd() // Equivalent to `.otherwise(() => { throw new Error('Absurd!') })`

I think it's much easier to grasp than the other options presented in this issue.

@kael-shipman
Copy link

I vote for @XiNiHa's solution:

How about just adding an optional argument to .exhaustive() that works as same as .otherwise()?

I just made the same suggestion over here

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

No branches or pull requests

7 participants