ts-pattern
ts-pattern copied to clipboard
Pattern matching on array length
Is your feature request related to a problem? Please describe. It is difficult to tersely pattern match array elements and tail in TypeScript. See this elm syntax:
listInfo : List a -> String
listInfo list =
case list of
[] -> "This is an empty list"
[ a ] -> "This list has exactly one element a"
[ a, b ] -> "This list has exactly two elements a, b"
a::b::[] -> "This list also has exactly two elements a, b"
[ a, b, c ] -> "This list has exactly three elements a, b, c"
a::b::c::rest -> "This list has more than 3 elements a, b, c; `rest` is a `List a`"
x::xs -> "x is the first element of this list (guaranteed to exist), xs is the rest. You can guarantee xs to be non-empty if you previously match for `[ a ]` or ` a::[]`"
JavaScript/TypeScript offers out of the box a form of destructuring that is very limited:
const destructureArray = ([x, ...xs]) => `The array's head is ${x}, the array's tail is ${xs}`
There are many type safety issues with the above approach:
- x is
undefined
if the input is an empty array, or if the first array element is explicitlyundefined
- it doesn't allow for branching depending on how many array elements exist
Describe the solution you'd like A comprehensive way to pattern match on array length and individual elements at the start of the array. Here's an example API that most closely resembles Elm/Haskell pattern matching.
const nums: number[] = [1, 2, 3, 4, 5, 6]
match(nums)
.with(P.list(), (emptyArr) => "type of emptyArr is `[]` ")
.with(P.list(P.el), (arr) => "type of arr is `[number]`")
.with(P.list(P.el, P.el), (arr) => "type of arr is [number, number]")
.with(P.list(P.el, P.rest), (arr) => "type of arr is `[number, ...number[]]`")
.with(P.list(P.el, P.el, P.rest), (arr) => "type of arr is `[number, number, ...number[]]`")
.exhaustive()
match(nums)
.with(P.list(P.el), ([a]) => "type of a is `number` as opposed to `number | undefined`")
.with(P.list(P.el, P.el), ([a, b]) => "a and b are both of type number as opposed to `number | undefined`")
.with(P.list(P.el, P.rest), ([x, ...xs]) => "type of x is `number`, type of xs is `number[]`")
.with(P.list(P.el, P.el, P.rest), ([a, b, ...rest]) => "a and b are `number`, rest is number[]")
.exhaustive()
This is an example API but ergonomics should be studied further.
Careful consideration must be given to what is considered an exhaustive pattern matching of a list and how pattern order matters (or doesn't). This should be researched and confirmed, but for quick reference, an exhaustiveness check would look like:
-
P.list()
case must be present -
P.list(P.rest)
must be present, unlessP.list(P.el)
is present, in which caseP.list(P.rest)
may be substituted withP.list(P.el, P.rest)
, etc
Describe alternatives you've considered
I've attempted using type guards and .when
but there are a number of issues:
const nums: number[] = [1, 2, 3, 4, 5, 6]
const isEmpty = <T,>(arr: T[]): arr is [] =>
arr.length === 0
const hasOneEl = <T,>(arr: T[]): arr is [T] =>
arr.length === 1
const hasTwoEls = <T,>(arr: T[]): arr is [T, T] =>
arr.length === 2
const hasAtLeastOneEl = <T,>(arr: T[]): arr is [T, ...T[]] =>
arr.length >= 1
match(nums)
.when(isEmpty, (emptyArr) => "OK: type of emptyArr is `[]` ")
.when(hasOneEl, (arr) => "ISSUE: type of arr is `[unknown]`")
.when(hasOneEl, (arr: [number]) => "MEH: type of arr is [number], but manual annotation necessary")
.when(hasOneEl, (arr: [string]) => "ISSUE: coercion of of arr to [string] is possible")
.when((nums) => hasOneEl(nums), (arr) => "ISSUE: type of arr is `number[]`")
.when((nums) => hasOneEl(nums), (arr: [number]) => "MEH: type of arr is `[number]` with anontation")
.when((nums) => hasOneEl(nums), (arr: [string]) => "OK: this is not possible")
.when(hasTwoEls, (_arr) => "ISSUE: type of arr is [unknown, unknown]")
.when(hasAtLeastOneEl, (arr) => "ISSUE: type of arr is `[unknown, ...unknown[]]`")
.when((nums) => hasAtLeastOneEl(nums), (arr) => "ISSUE: type of arr is number[]")
.exhaustive(/* ISSUE: does not detect that this pattern matching is exhaustive */)
As an alternative to my P.list example API, something similar to my attempt could look like:
match(nums)
.when(P.isEmpty, (emptyArr) => "type of emptyArr is `[]` ")
.when(P.hasOneEl, (arr) => "type of arr is `[number]`")
.when(P.hasTwoEls, (arr) => "type of arr is [number, number]")
.when(P.hasAtLeastOneEl, (arr) => "type of arr is `[number, ...number[]]`")
.when(P.hasAtLeastTwoEls, (arr) => "type of arr is `[number, number, ...number[]]`")
.exhaustive()
Assuming the type issues and pointfree fiddliness can be solved, such a solution, despite being different from Elm/Haskell-type matching, might be easier to understand for Typescript as well as easier to implement.
Additional context /
Hi
That's a good feature request, I was thinking about adding a P.rest(subpattern?)
pattern that could be combine with tuple in patterns that would look like [1, 2, P.rest(P.number)]
(which would match the type [1, 2, ...number[]]
) to cover this use case. Would that work for you?
Duh, that seems like the simplest change, with all the benefits. Sounds good.
Since I like listing out the different usages:
const nums: number[] = [1, 2, 3, 4, 5, 6]
match(nums)
// Already implemented
.with([], (emptyArr) => "type of emptyArr is `[]`")
.with([P.number], (arr) => "type of arr is `[number]`")
.with([P.number, P.number], (arr) => "type of arr is [number, number]")
// New syntax
.with([P.number, P.rest(P.number)], (arr) => "type of arr is `[number, ...number[]]`")
.with([1, P.number, P.rest(P.number)]), (arr) => "type of arr is `[1, number, ...number[]]`")
.exhaustive()
// Combined with JS destructuring
match(nums)
// Already implemented
.with([P.number], ([a]) => "type of a is `number` as opposed to `number | undefined`")
.with([P.number, P.number], ([a, b]) => "a and b are both of type number as opposed to `number | undefined`")
// New syntax
.with([1, P.rest(P.number)], ([x, ...xs]) => "type of x is `1`, type of xs is `number[]`")
.with([P.number, P.number, P.rest(P.number)], ([a, b, ...rest]) => "a and b are `number`, rest is number[]")
.exhaustive()
_
I gave this a little more thoughts and I'm leaning toward an API that would look like this instead:
const xs: unknown[] = [1, 2, 3, 'a', 'b', 'c'];
match(xs)
.with([P.any, ...P.array()], (xs) => []) // xs: [unknown, ...unknown[]]
.with([...P.array(), 7], (xs) => []) // xs: [...unknown[], number]
.with([42, ...P.array(P.number)], (xs) => []) // xs: [42, ...number[]]
.with([42, ...P.array(P.number), '!'], (xs) => []) // xs: [42, ...number[], '!']
.with([1, 2, ...P.array(P.number)], (xs) => []) // xs: [1, 2, ...number[]]
.with([...P.array(P.string), 'a', 'b'], (xs) => []) // xs: [...string[], 'a', 'b']
.otherwise(() => [])
This way we can cover all the features of Variadic Tuple types, like leading spread [...type1[], type2]
, middle spread [type1, ...type2[], type3]
.
_
I started implementing this, here is the WIP PR 👉 https://github.com/gvergnaud/ts-pattern/pull/114
Wow, can't wait to use this!
This is going to be part of TS-Pattern v5 (draft pr: #139)
Added in https://github.com/gvergnaud/ts-pattern/releases/tag/v5.0.0!