Skip to content

Support string literals as spread argument #51397

Closed
@philer

Description

@philer

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    DuplicateAn existing issue was already created

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions