Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Typed ...rest parameters with generics #1024

Closed
andrewvarga opened this issue Nov 1, 2014 · 13 comments · Fixed by #24897
Closed

Typed ...rest parameters with generics #1024

andrewvarga opened this issue Nov 1, 2014 · 13 comments · Fixed by #24897
Labels
Duplicate An existing issue was already created Fixed A PR has been merged for this issue

Comments

@andrewvarga
Copy link

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.

@saschanaz
Copy link
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
Copy link
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 RyanCavanaugh added the Duplicate An existing issue was already created label Nov 3, 2014
@RyanCavanaugh
Copy link
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
Copy link
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
Copy link
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 (@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
Copy link
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
Copy link
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
Copy link
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
Copy link
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
Copy link
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
Copy link
Author

@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).

@alexburner
Copy link

alexburner commented Nov 12, 2017

@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).

@alexburner
Copy link

alexburner commented Nov 12, 2017

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.

@microsoft microsoft locked and limited conversation to collaborators Jun 18, 2018
@ahejlsberg ahejlsberg added the Fixed A PR has been merged for this issue label Jun 26, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Duplicate An existing issue was already created Fixed A PR has been merged for this issue
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants