Description
Suggestion
A tuple of characters can be concisely represented as a string literal – for example "0123456789"
is a convenient representation of ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
for many purposes. However when using the spread operator ...
on a string literal, e.g. in a function call, TS2556 is raised with the message: "A spread argument must either have a tuple type or be passed to a rest parameter".
My suggestion is therefor to allow string literals to be treated like string tuples in conjunction with the spread operator.
🔍 Search Terms
string literal, spread operator, spread string literal, spread characters, string as tuple
✅ Viability Checklist
My suggestion meets these guidelines:
- This wouldn't be a breaking change in existing TypeScript/JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- This feature would agree with the rest of TypeScript's Design Goals.
⭐ Suggestion
When using the spread operator on a string literal it should work the same way as when it's used on a string tuple. E.g. the following expressions are equivalent in plain JS and thus should be supported the same way in TS:
fn("a", "b", "c") // OK
fn(...["a", "b", "c"] as const) // OK but impractical
fn(..."abc") // Fails but should be OK
📃 Motivating Example
I'm using ts-pattern
to match keyboard events. For this example let's say I want to use it to control volume:
const handleKeyDown = (evt: KeyboardEvent) =>
match(evt.key)
.with("+", () => updateVolume(vol => vol + 0.1))
.with("-", () => updateVolume(vol => vol - 0.1))
.with(..."0123456789", digit => updateVolume(0.1 * digit))
.otherwise(noOp)
For the +/- keys, this would increment/decrement the volume by 10%. When pressing a number key it should set the volume directly to the corresponding multiple of 10%.
Note that ts-pattern
's .with
method takes any number of arguments for pattern matching and if any of the arguments matches the input calls the final argument as a function. Of the example above the fifth line does not work with current TS (4.8.4). I've tried the following variations, all of which fail type checking:
.with(..."0123456789", digit => updateVolume(0.1 * digit))
.with(...("0123456789" as const), digit => updateVolume(0.1 * digit))
.with(...([..."0123456789"] as const), digit => updateVolume(0.1 * digit))
.with(...(Array.from("0123456789" as const)), digit => updateVolume(0.1 * digit))
However the following work:
.with("0", "1", "2", "3", "4", "5", "6", "7", "8", "9", digit => updateVolume(0.1 * digit))
.with(...["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"] as const, digit => updateVolume(0.1 * digit))
You might call this a matter of convenience but I would argue that it improves readability in those cases where it comes in handy – and code readability could always be argued to be "just" a convenience.
💻 Use Cases
Whenever you want to hard code a tuple of characters it is easy to either use a string literal directly or spread it into an array, such as
const alphabet = [..."abcdefghijklmnopqrstuvwyz"] as const
This is quite universal and can be useful whenever you need a hard coded tuple of characters. Some examples:
- all digits, all latin letters, etc. (seen above)
- when handling user input
- building a parser of some sort
- all characters valid in a password
"wasd"
(video game keyboard controls)- Characters are sometimes used like flags (e.g. as the second argument to the
RegExp
constructor). - Many educational examples/exercises revolve around string manipulation (e.g. on codewars.com).