2

A bit hard to explain it in the title but basically, I'd like to be able to declare a type array of fixed length strings from 1 to N:

interface Command {
  [key: string]: (args: [string, ...string[]]) => boolean;
}

const cmd: Command = {
  TEST: (args: [string, string]) => args[0] === args[1],
  TEST2: (args: [string]) => args[0] === 'hello'
}

so this doesn't work as [string, string] is different from string[]:

Type '(args: [string]) => boolean' is not assignable to type '(args: [string, ...string[]]) => boolean'.

A solution could be to define like all kind of arguments:

interface Command {
  [key: string]: (args: [string] | [string, string] | [string, string, string]) => boolean;
}

But a bit too verbose for something simple (agreed it can be encapsulated in an interface), anyway is there another elegant solution to this that I'm not seeing?

Thanks,

Ervadac
  • 936
  • 3
  • 9
  • 26
  • No it's not possible. Typescript doesn't supports types with specified string length. But there maybe something that can help you, check out this answer: https://stackoverflow.com/a/54832231/7616528 – TheMisir Feb 19 '20 at 00:00
  • Your "verbose" solution doesn't work either, right? It gives the same error. I'm not really sure what you want `Command` to look like. Can you show how you would plan to use a value `c` of type `Command`? Say, you have the function `c.f`... are you allowed to call it with `c.f(["hello"])`? Can you call it with `c.f(["hello","goodbye"])`? – jcalz Feb 19 '20 at 02:14

1 Answers1

2

I suspect that you actually need Command to be generic, so that its properties can actually be used. If you have a function type like (arg: [string] | [string, string]) => boolean, then you cannot implement it with a (arg: [string]) => boolean or with a (arg: [string, string]) => boolean. Function types vary contravariantly in their arguments' types. You can widen, but not narrow, the type of a function argument. Unless you want to require that all methods of type Command must accept every possible string array of length one or more, you have to do something with generics.

Here's a possible type for Command, along with a helper function asCommand() which lets the compiler infer the proper value of T given a value of type Command<T> without forcing you to write it out yourself:

type Command<T> = { [K in keyof T]: (args: T[K]) => boolean }

const asCommand = <T extends Record<keyof T, [string, ...string[]]>>(
    cmd: Command<T>
) => cmd;

Then, your cmd constant can be declared like this with no errors:

const cmd = asCommand({
    TEST: (args: [string, string]) => args[0] === args[1],
    TEST2: (args: [string]) => args[0] === 'hello'
});

and the compiler remembers that cmd.TEST takes a pair and cmd.TEST2 takes a one-tuple:

cmd.TEST(["", ""]); // okay
cmd.TEST([]); // error
cmd.TEST([""]); // error
cmd.TEST(["", "", ""]); // error

cmd.TEST2([""]); // okay
cmd.TEST2([]); // error
cmd.TEST2(["", ""]); // error
cmd.TEST2(["", "", ""]); // error

And you're not allowed to give a property that accepts only zero-length tuples:

const badCmd = asCommand({
    OOPS: (args: []) => false, // error!
})

I hope that gives you some direction; good luck!

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360