Skip to content

Typed ...rest parameters with generics #1024

Description

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.

Activity

saschanaz commented on Nov 1, 2014

@saschanaz
Contributor

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]);

andrewvarga commented on Nov 1, 2014

@andrewvarga
Author

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?

RyanCavanaugh commented on Nov 3, 2014

@RyanCavanaugh
Member

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!

andrewvarga commented on Dec 24, 2014

@andrewvarga
Author

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?

DanielRosenwasser commented on Dec 25, 2014

@DanielRosenwasser
Member

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 (Anders Hejlsberg (@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);

andrewvarga commented on Dec 25, 2014

@andrewvarga
Author

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.

DanielRosenwasser commented on Dec 25, 2014

@DanielRosenwasser
Member

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.

andrewvarga commented on Dec 26, 2014

@andrewvarga
Author

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?

DanielRosenwasser commented on Dec 26, 2014

@DanielRosenwasser
Member

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]);

ahejlsberg commented on Dec 26, 2014

@ahejlsberg
Member

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();
    }
}

andrewvarga commented on Dec 26, 2014

@andrewvarga
Author

Daniel Rosenwasser (@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.

Anders Hejlsberg (@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).

alexburner commented on Nov 12, 2017

@alexburner

Ryan Cavanaugh (@RyanCavanaugh) Daniel Rosenwasser (@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).

alexburner commented on Nov 12, 2017

@alexburner

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.

locked and limited conversation to collaborators on Jun 18, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    DuplicateAn existing issue was already createdFixedA PR has been merged for this issue

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions