microsoft/TypeScript

Typed ...rest parameters with generics

andrewvarga opened this issue ยท 13 comments

Is it possible to add type info to the rest parameters in a way that each individual parameter can have a different type?

This works, but with this there can be any number parameters, and all have to have the same type:

function myFunction<T>(...args:T[]) {
}
myFunction<number>(1, 3);

It would be really useful if I could force the exact number of arguments and the types for each one, but all this within generics, so something like this (but this is obviously syntactically wrong):

function myFunction<T>(...args:T) {
}

interface MyParams {
    [0]:string;
    [1]:number;
}

myFunction<MyParams>("a", 1)

An example use case is writing an observer class that the user could specify the arguments of the listener functions for:

class Observer<T> {
    addListener(...args:T) {
    }
}

If this is not supported, what do you recommend using instead? I can use any[] of course, or living with the constraint of having a fixed number of fixed typed parameters.

I think you will be able to do a similar thing with tuple types.

function myFunction<T extends any[]>(argary: T) {
}

myFunction<[string, number]>(["a", 1]);

That really looks like what I will need, thanks! Do you know when 1.3 will be released and if the current 1.3 is stable enough to use in production?

1.3 is pretty stable; I'd give it a shot. We're using it internally, at least.

Feel free to drop a comment here and I'll re-open this if tuple types turn out to not be what you're looking for and you want to discuss further. Thanks!

I played with this now, but I couldn't achieve what I really would like to do.

The example from above works:
myFunction<[string, number]>(["a", 1]);

but I don't want to pass an array to myFunction, only the elements of the array like this:
myFunction<[string, number]>("a", 1);

I tried to achieve that like this:

function myFunction<T extends any[]>(...args:T)
{
}
myFunction<[string, number]>("a", 1);

The problem is, the compiler is giving me this error:
A rest parameter must be of an array type.(parameter) args: T extends any[]

I don't understand why T is not an array type in this case?

The spec says

A parameter with a type annotation is considered to be of that type. A type annotation for a rest parameter must denote an array type.

We should consider whether it is permissible to type a rest parameter with a bare type parameter in certain circumstances (@ahejlsberg), though as I state below, I'd need to see a use case that isn't subsumed by union types.


What are you trying to do exactly? I'm curious about the utility of rest parameters typed as tuple types; I can't imagine a function implementation doing anything with it other than use the union type of the arguments, in which case you'll be able to just use the following in TypeScript 1.4:

function bahHumbug<T>(...args: T[])
{
}
bahHumbug<string|number>("a", 1);

Thanks for the reply!

The reason I don't understand that error message is that T extends any[] so it's an array type (?) and also if I write any[] instead of T it compiles.
What I'm trying to do is basically a generic Observer class, something like this:

// aka "Subject" in Observer pattern
class Dispatcher<T extends any[]>
{
    public addListener(listener:(...args:T)=>any) 
    {
        // ...
    }
    public dispatch(...args:T) {
        // ...
    }
}

var dispatcher:Dispatcher<[string, number]> = new Dispatcher();

// should work:
dispatcher.addListener(myGoodListener);
// should throw compile error:
dispatcher.addListener(myBadListener);

// should work:
dispatcher.dispatch("hey", 1);
// should throw compile error:
dispatcher.dispatch(1);

function myGoodListener(p1:string, p2:number) 
{
    // ...
}

function myBadListener(p1:number)
{
    // ...
}

This now gives the previously mentioned error: "A rest parameter must be of an array type".
I can achieve half of this though by making the generic T type in Dispatcher be a subclass of Function, and with that I can ensure addListener is only accepting correct listeners. The problem is the dispatch() call that I couldn't get to work type-safely.

Another approach that works is if I restrict to use just one parameter to dispatch, with that everything works, but the absolute best would be if I could allow the users of this class to use it with any number of parameters but all of them being type-safe.

I think we've discussed something like this, but It's something to consider a bit more.

Given that it's only 2 extra characters to dispatch/notify, and that listeners address their elements in the same manner, this shouldn't be an awful workaround in the mean time. As a separate matter, the downlevel emit for rest parameters is probably less efficient than using a tuple anyhow, given that optimizers have a difficult time when using arguments.

What do you refer to as those 2 extra characters? You mean to dispatch with a single array like this as suggested by SaschaNaz ?

myFunction<[string, number]>(["a", 1]);

Or which workaround were you referring to?

Yes, the extra two characters are [ and ]. Just make args not be a rest parameter. Instead, you'll be passing your arguments in as tuples.

class Dispatcher<T>
{
    public addListener(listener: (args: T) => void): void
    {
        // ...
    }
    public dispatch(args: T): void {
        // ...
    }
}

var dispatcher = new Dispatcher<[string, number]>();


function myGoodListener(args: [string, number]) 
{
    // ...
}

// Note: this one will work when we have destructuring
/*
function anotherGoodListener([p1, p2]: [string, number])
{
    var myStr = "Hello " + p1;
    var myNum = p2 + 1;
}
*/

dispatcher.addListener(myGoodListener);
// dispatcher.addListener(anotherGoodListener);

dispatcher.dispatch(["hey", 1]);

Without additional features the closest you can get is to define family of Dispatcher interfaces that follow this pattern:

interface Dispatcher1<T0> {
    addListener(listener: (p0: T0) => void): void;
    dispatch(p0: T0): void;
}
interface Dispatcher2<T0, T1> {
    addListener(listener: (p0: T0, p1: T1) => void): void;
    dispatch(p0: T0, p1: T1): void;
}
interface Dispatcher3<T0, T1, T2> {
    addListener(listener: (p0: T0, p1: T1, p2: T2) => void): void;
    dispatch(p0: T0, p1: T1, p2: T2): void;
}
// ... up to some meaningful limit

and then have a general purpose implementation with an overloaded factory method (or something similar to that effect):

class Dispatcher {
    public addListener(listener: (...args: any[]) => void): void {
        // ...
    }
    public dispatch(...args: any[]): void {
        // ...
    }
    public static create<T0>(): Dispatcher1<T0>;
    public static create<T0, T1>(): Dispatcher2<T0, T1>;
    public static create<T0, T1, T2>(): Dispatcher3<T0, T1, T2>;
    public static create(): Dispatcher {
        return new Dispatcher();
    }
}

@DanielRosenwasser thanks, yes that could work. One reason I wanted to avoid that approach is that it may be wrong from a performance point of view to create a new array at each dispatch call (although in most cases it probably doesn't matter), and you have a good point about arguments not being efficient for optimizers.

@ahejlsberg thanks, that is quite close to what I was looking for, I don't think I would need more than a few parameters anyway.

(Btw thank you all for your work on TS, it's a really huge step to enhance JS in my opinion).

@RyanCavanaugh @DanielRosenwasser, I believe I've found a situation in which a tuple doesn't cut the mustard. In this case, I'm trying to extend the types for a third-party library redux-loop (redux middleware that uses elm as its inspiration for handling async & other side effects).

This lib extends the redux reducer to return both the state object and a Cmd, which can fire an action, run a function, batch multiple child Cmds, etc. They have an example reducer here.

I'd like to extend their types for Cmd.run, which allows a user to specify a function to be run, along with args to be passed to that function, and "action creators" to be called on success or failure:

Cmd.run(fetchUser, {
  successActionCreator: userFetchSuccessfulAction,
  failActionCreator: userFetchFailedAction,
  args: ['123']
})

Currently, there is no type safety between the arguments of the function to be run and the value of the args option. Nor is there type safety between the return value of the function, and the arguments of the successActionCreator option.

The current interface for Cmd.run looks like:

static readonly run: <A extends Action>(
  f: Function,
  options?: {
    args?: any[];
    failActionCreator?: ActionCreator<A>;
    successActionCreator?: ActionCreator<A>;
    forceSync?: boolean;
  }
) => RunCmd<A>;

I'd like to do something that enforces a match between inputs and outputs of f, options.args, and options.successActionCreator. Something like:

static readonly run: <A extends Action, Args, Result>(
  f: (...args: Args) => Result,
  options?: {
    args?: Args;
    failActionCreator?: () => A;
    successActionCreator?: (result?: Result) => A;
    forceSync?: boolean;
  }
) => RunCmd<A>;

However, trying this, I get the error that brought me to this issue:

A rest parameter must be of an array type.

Is there a way I can accomplish this type safety, without altering redux-loop?

Additionally, it would be swell if I could use Args and Result to indicate where values should match within the Cmd.run type, without requiring the user to pass explicit types in through a generic. But I don't know a way to indicate two types should match, without actually defining them (either as generics, or as specific types).

Digging further, I ran across a proposal for Variadic Kinds which directly addresses this issue. It looks like I may have to hold out hope for that. But if anyone has clever workarounds in the meantime, I wouldn't complain.