18

I want to pass arbitrary number of arguments of different types into a function and use these types. It looks like following:

function f<A>(a: A): A;
function f<A, B>(a: A, b: B): A & B;
function f<A, B, C>(a: A, b: B, c: C): A & B & C;
function f<A, B, C, D>(a: A, b: B, c: C, d: D): A & B & C & D;

function f(...args: any[]) {
    return Object.assign({}, ...args);
}

var smth = f({ x: 1 }, { y: 2 }, { z: 3 });
var res = smth.x + smth.y + smth.z;

As I want arbitrary number of parameters, I'd like to get rid of these declarations

function f<A>(a: A): A;
function f<A, B>(a: A, b: B): A & B;
function f<A, B, C>(a: A, b: B, c: C): A & B & C;
function f<A, B, C, D>(a: A, b: B, c: C, d: D): A & B & C & D;

and use a single declaration like:

function f<...T>(args: [...T]): &<...T>;

but this thing is syntactically wrong.

Is there a way to rewrite it in a correct way?

PS: Same question in Russian.

Qwertiy
  • 19,681
  • 15
  • 61
  • 128
  • 4
    Related: [Proposal: Variadic Kinds](https://github.com/Microsoft/TypeScript/issues/5453) – CRice Jun 06 '18 at 20:50
  • @CRice, no workaround in my case, except specifiing tons of overloads? Also seems like proposal is only about arrays and I don't see there anything like `&<...T>`. – Qwertiy Jun 06 '18 at 20:53
  • 1
    There's a little discussion about it in that proposal, and I noticed they link to another issue [describing this almost exactly](https://github.com/Microsoft/TypeScript/issues/3870). I don't know of any way to do it without overloads currently. Even the [stdlib's definition for `Object.assign`](https://github.com/Microsoft/TypeScript/blob/master/lib/lib.es6.d.ts#L4434) uses a giant overload. But there may be a way I'm just not aware of. – CRice Jun 06 '18 at 21:12

1 Answers1

14

Edit for 3.0

While the original answer is correct, since I first gave it typescript has changed. In typescript 3.0 it is possible to use tuples in rest parameters to capture the type of the arguments in a tuple

type UnionToIntersection<U> =
    (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never

function f<A extends any[]>(...args: A): UnionToIntersection<A[number]> { return null! }
var smth = f({ x: 1 }, new A(), new B());  // will have type  A & B & { x: number; }

Original answer

While as others have mentioned Proposal: Variadic Kinds would help with this task, we can find some workarounds for your specific example.

If we write the function signature with a single type argument, we can get a union type of the arguments:

function f<A>(...args: A[]): A {}
var smth = f({ x: 1 }, { y: 2 }, { z: 3 });
typeof smth = {
    x: number;
    y?: undefined;
    z?: undefined;
} | {
    y: number;
    x?: undefined;
    z?: undefined;
} | {
    z: number;
    x?: undefined;
    y?: undefined;
}

The problem with this approach is that if we use a class instead of an object literal the compiler will refuse to infer the union and give us an error instead. If we let the rest parameter go (...) and just use an array the compiler will infer a union of the parameter type:

function f<A>(args: A[]): A { /*…*/}
var smth = f([{ x: 1 }, new A(), new B()]); 
typeof smth == A | B | {
    x: number;
}

So now we have a union of types, but you want an intersection. We can convert the union to an intersection using conditional types (see this answer)

type UnionToIntersection<U> = 
(U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never

function f<A>(args: A[]): UnionToIntersection<A> {
    return Object.assign({}, ...args);
}

class A { z: number }
class B { y: number }

var smth = f([{ x: 1 }, new A(), new B()]); // will have type  A & B & { x: number; }
var res = smth.x + smth.y + smth.z;

Hope this helps, and gives you a usable workaround at least until we get variadic kinds.

Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357