gvergnaud/ts-pattern

Handle readonly arrays in .exhaustive()

philer opened this issue · 2 comments

philer commented

With tuple patterns and P.array() it is possible to exhaustively match variable length arrays – but not if they are marked readonly.

For example this classic recursive sum implementation passes type checking (ts-pattern 5.0.5, typescript 5.3.2):

const sum = (xs: number[]): number =>
  match(xs)
    .with([], () => 0)
    .with([P._, ...P.array()], ([x, ...xs]) => x + sum(xs))
    .exhaustive()

However when I change the parameter type from number[] to readonly number[]or ReadonlyArray<number>, I get a type error on .exhaustive():

Type 'NonExhaustiveError<readonly []>' has no call signatures.

Describe the solution you'd like

I'm honestly not quite sure how this could be solved, as I don't know enough about the details of the current implementation of exhaustiveness checking for tuples and arrays. Ideally I'd say just default the existing patterns to readonly, but that might cause issues when users actually want to mutate the matched array (I'd say "just don't do that" but I'm not sure if that conforms with the philosophy of this project).
An alternative might be to add something like a P.readonlyArray().

Describe alternatives you've considered

I don't know if there are any. The problem occurred to me when I had a readonly array that I want to pass to a function defined similar to sum in the example above. With the parameter type (xs: number[]) => ... I can not pass a readonly number[] to sum, as the type checker can not guarantee that sum won't mutate the array. Obviously a simple example like sum above could be implemented more effectively via .reduce() (or just a for loop) but given that pattern matching on tuples/arrays exists in ts-pattern I believe it should also cover their immutable variants.

PS: It is entirely possible that I've just missed something and there is already a solution to my problem, in which case I'd be happy to find out about it. Either way, thanks for this amazing project! :)

Thanks for opening this well written issue! This was simply an oversight, I would expect the matched value to be readonly [] in the handler function of the .with([], (x) => ...) expression here. I opened a PR to fix this.

This is technically a type-level breaking change, but I'm going to treat this as a patch since this behavior was unexpected and the fix doesn't affect the runtime.

philer commented

That was quick. Thanks! <3