Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cannot find the proper typing for a certain kind of varargs function

Please have a look at the following little demo:

  • Function f should accept ONE pair of the form ['stringX', 'stringXstringX'] (-> this is working)
  • But function g should accept A VARIABLE NUMBER of pairs of this pattern. How do I have to type function g to make this happen?
function f<T extends string, U extends `${T}${T}`>(arg: [T, U]) {}
function g(...args: ???[]) {} // what's the right type here (-> ???)?

f(['a', 'aa']) // ok
f(['a', 'ab']) // error as expected

g(
  ['a', 'aa'], // should be okay
  ['b', 'bb'], // should be okay
  ['c', 'ab']  // should result in a compile error!!!
)

» TypeScript Playground

[Edit] Of course I can change function g to have n optional arguments combined with 2*n type parameters, but this is not the solution I am looking for.

[Edit] By the way: If function g would accept ONE ARRAY of those tuples instead of varargs and behave accordingly that would also be a useful solution, but I guess it's the same problem.

[Edit] Making g a single-argument function of the following form will work but is syntactically not very nice:

// expected error at the 'ab'
g(['a', 'aa'])(['b', 'bb'])(['c', 'ab'])

See DEMO

like image 974
Natasha Avatar asked Nov 21 '21 20:11

Natasha


People also ask

How many parameters can a function have in Python?

In Python 3.7 and newer, there is no limit.

How many arguments should a function have Python?

While a function can only have one argument of variable length of each type, we can combine both types of functions in one argument. If we do, we must ensure that positional arguments come before named arguments and that fixed arguments come before those of variable length.

When to use varargs instead of methods?

As you can see, varargs can be really useful in some situations. However, if you are certain about the number of arguments passed to a method, used method overloading instead. For example, if you are certain that sumNumber() method will be used only to calculate the sum of either 2 or 3 arguments,...

What are the rules for using varargs in Python?

While using the varargs, you must follow some rules otherwise program code won't compile. The rules are as follows: There can be only one variable argument in the method. Variable argument (varargs) must be the last argument. void method (String... a, int...

What is varargs in Java?

Varargs is a short name for variable arguments. In Java, an argument of a method can accept arbitrary number of values. This argument that can accept variable number of values is called varargs. accessModifier methodName (datatype… arg) { // method body } In order to define vararg, ... (three dots) is used in the formal parameter of a method.

Why can't I use varargs with empty second parameter?

Also, the compiler may think, you are trying to call test (int n, int ... vargs) with argument passed to the first parameter with empty second parameter. Since there are two possibilities, it causes ambiguity. Because of this, sometimes you may need to use two different method names instead of overloading the varargs method.


1 Answers

My approach here would look like this:

function g<T extends string[]>(...args: { [I in keyof T]:
  [T[I], NoInfer<Dupe<T[I]>>]
}) { }

type Dupe<T> = T extends string ? `${T}${T}` : never;

type NoInfer<T> = [T][T extends any ? 0 : never];

The idea is that g() is generic in the type parameter T[], a tuple type consisting of just the string literal types from the first element of each pair of strings from args. So if you call g(["a", "aa"], ["b", "bb"], ["c", "cc"]), then T should be the tuple type ["a", "b", "c"].

The type of args can then be represented in terms of T by a mapped tuple type. For each numeric index I from the T tuple, the corresponding element of args should have a type like [T[I], Dupe<T[I]>], where Dupe<T> concatenates strings to themselves in the desired way. So if T is ["z", "y"], then the type of args should be [["z", "zz"], ["y", "yy"]].

The goal here is that the compiler should see a call like g(["x", "xx"], ["y", "oops"]) and infer T from the type of args to be ["x", "y"], and then check against the type of args and notice that while ["x", "xx"] is fine, ["y", "oops"] does not match the expected ["y", "yy"] and thus there is an error.

That's the basic approach, but there are some issues we need to work around.


First, there is an open issue at microsoft/TypeScript#27995 that causes the compiler to fail to recognize that T[I] will definitely be a string inside the mapped tuple type {[I in keyof T]: [T[I], Dupe<T[I]>]}. Even though a mapped tuple type creates another tuple type, the compiler thinks that I might be some other key of any[] like "map" or "length" and therefore T[I] could be a function type or number, etc.

And so if we had defined Dupe<T> like

type Dupe<T extends string> = `${T}${T}`;

there would be a problem where Dupe<T[I]> would cause an error. In order to circumvent that problem, I have defined Dupe<T> so that it does not require that T is a string. Instead, it just uses a conditional type to say "if T is a string, then Dupe<T> will be `${T}${T}`":

type Dupe<T> = T extends string ? `${T}${T}` : never;

So that takes care of that problem.


The next problem is that to infer T from a value of type {[I in keyof T]: [T[I], Dupe<T[I]>]}, the compiler is free to look both at the first element of each args pair (T[I]), as well as the second element (Dupe<T[I]>). Each mention of T[I] in that type is an inference site for T[I]. We want the compiler only to look at the first element of each pair when inferring T, and to just check the second element. Unfortunately, if you leave it like this, the compiler tries to use both and gets confused.

That is, when inferring T from [["y", "zz"]], the compiler will match "y" agains T[I] and "zz" against Dupe<T[I]>, and it ends up inferring the union type ["y" | "z"] for T. That's quite clever, actually, since if T really were ["y" | "z"], then the type of args would be of type ["y" | "z", "yy" | "zz"], and the value ["y", "zz"] matches it. Nice! Except you want to reject [["y", "zz"]].

So we need some way to tell the compiler not to use the second element of each pair to infer T. There is an open issue at microsoft/TypeScript#14829 which asks for some support for "non-inferential type parameter usage" of the form NoInfer<T>. A type like NoInfer<T> would eventually evaluate to T, but would block the compiler from trying to infer T from it. And so we'd write {[I in keyof T]: [T[I], NoInfer<Dupe<T[I]>>]}.

There's no native or official NoInfer<T> type in TypeScript, but there are some implementations which work in some circumstances. I tend to use this version or something like it to take advantage of the compiler's tendency to defer evaluation of unresolved conditional types:

type NoInfer<T> = [T][T extends any ? 0 : never];

If you put some specific type in for T, this will evaluate to T (e.g., NoInfer<string> is [string][string extends any ? 0 : never] which is [string][0] which is string), but when T is an unspecified generic type, the compiler leaves it unevaluated and does not eagerly replace it with T. So NoInfer<T> is opaque to the compiler when T is some generic type parameter, and we can use it to block type inference.


So there's the explanation (whew!). Let's make sure it works:

g(
  ['a', 'aa'], // okay
  ['b', 'bb'], // okay
  ['c', 'dd']  // error!
  // -> ~~~~
  // Type '"dd"' is not assignable to type '"cc"'. (2322)
)

Looks good! The compiler is happy with ["a", "aa"] and ["b", "bb"] but balks at ["c", "dd"] with a nice error message about how it expected "cc" but got "dd" instead.


This answers the question as asked. But do note that the typing for g() will only really be useful for callers of g() who do so with specific and literal strings passed in. As soon as someone calls g() with unresolved generic strings (say inside the body of some generic function), or as soon as you try to inspect T or args inside the body of the implementation of g(), you will find that the compiler cannot really understand the implications. In those cases you'll be better off widening to something like [string, string][] or the like and just being careful.

Playground link to code

like image 174
jcalz Avatar answered Oct 21 '22 07:10

jcalz