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

Suggestion: Generic Parameter Overloads #209

Closed
omidkrad opened this issue Jul 23, 2014 · 14 comments
Closed

Suggestion: Generic Parameter Overloads #209

omidkrad opened this issue Jul 23, 2014 · 14 comments
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript

Comments

@omidkrad
Copy link

Suggestion moved over from codeplex.

The suggestion is to allow generic parameter overloads like the following:

declare module Backbone {
    class Model{}
    class Events{}
    class ViewOptions<TModel extends Model> {}
    class Collection<TModel extends Model> {}

    // Generic parameter overload
    class View extends View<Backbone.Model>;
    class View<TModel extends Model> extends Events { // Don't take as duplicate identifier
        constructor(options?: ViewOptions<TModel>);
        model: TModel;
        collection: Collection<TModel>;
    }
}

// With Generics Parameter overloads, old non-generic consumer
// code works as before and does not need to change.
class MyView extends Backbone.View {
}
var myView = new MyView();
var model = myView.model; // model is of type Backbone.Model

// Can gradually introduce generic parameters to Views
class DerivedModel extends Backbone.Model { 
}
var myView2 = new Backbone.View<DerivedModel>();
var model2 = myView2.model; // model2 is of type DerivedModel

This is important because this would allow people to introduce generics in their library declarations without breaking large amounts of existing code for the consumers. We do this a lot in C#, and it is compatible with TypeScript design because it won't make any changes in the resulting javascript, and works pretty much like method overloads in TypeScript.

Related discussion: https://typescript.codeplex.com/discussions/452741

@danquirk
Copy link
Member

Couple of questions that come out of thinking about this:

  1. Would a default value for type parameters in classes not accomplish the same thing? That is:
class View<TModel extends Backbone.Model = Backbone.Model> {
...
}
// currently typed as View, with this feature would be now typed as View<Backbone.Model> 
// because of the default type param value
var v: View;
  1. The above leads to the question of whether you'd ever have a default type parameter value other than any or the constraint type. In that case, could the compiler just do this for you and instead of adding complexity to the language with a new piece of syntax it would do something like:
var v: View; // infers View<Backbone.Model> because View had a constraint
var a: Array; // infers Array<any> because Array has no constraint on T

In the past we didn't want to infer any for T which is why we required type parameters for generic types used in type positions. This is mitigated by using --noImplicitAny but it's a little ugly to require that flag to make generics behave well for simple mistakes (ie forgetting to add a type parameter).

@omidkrad
Copy link
Author

Yes, both alternatives can do it. I like the second one even better that the compiler figures the type parameter for you. I think that would cover most cases, but just in case, combination of these two also seems to be viable.

@omidkrad
Copy link
Author

One case I thought you don't want the compiler to automatically choose the default generic type for you is when you want to keep it to be generic, for example in Collection and ViewOptions below:

class View<TModel extends Model> extends Events {
    constructor(options?: ViewOptions<TModel>);
    model: TModel;
    collection: Collection<TModel>;
}

So it's still good to enforce --noImplicitAny here.

@mikecann
Copy link

I dont want to be the "Me Too" guy but this is definitely needed for a signals library im using!

class Signal {
    }

    class Signal<T> {
    }

    class Signal<T,U> {
    }

    class Signal<T, U, V> {
    }

@johnnyreilly
Copy link

👍

@sccolbert
Copy link

👍

@omidkrad
Copy link
Author

omidkrad commented Dec 4, 2014

Glad this is still being considered. This feature can be taken as two things:

  1. Generic Parameter Overloads, where same class name can get multiple definitions with different number of generic type parameters.
  2. Default value for generic type parameters, as @danquirk said.

The first one can cover use cases for the second one, but the opposite is not true. If true C#-like Generic Parameter Overloads is supported where each overload defines its own implementation, then the compiler has to give a different name to each overload in the generated JS. For example, View, View_$1, View_$2, etc. which can make it more complex. This feature is going to be nice, but I think allowing default value for generic type parameters is more needed, and would make zero footprint in the generated JS.

@RonPenton
Copy link

Any movement on this one? I've run into the situation where I want to do this:

    export interface Func<R> { (): R; }
    export interface Func<P, R> { (parameter: P): R; }
    export interface Func<P1, P2, R> { (parameter1: P1, parameter2: P2): R; }
    export interface Func<P1, P2, P3, R> { (parameter1: P1, parameter2: P2, parameter3: P3): R; }
    export interface Func<P1, P2, P3, P4, R> { (parameter1: P1, parameter2: P2, parameter3: P3, parameter4: P4): R; }

But Instead I have to define them like this:

    export interface Func0<R> { (): R; }
    export interface Func1<P, R> { (parameter: P): R; }
    export interface Func2<P1, P2, R> { (parameter1: P1, parameter2: P2): R; }
    export interface Func3<P1, P2, P3, R> { (parameter1: P1, parameter2: P2, parameter3: P3): R; }
    export interface Func4<P1, P2, P3, P4, R> { (parameter1: P1, parameter2: P2, parameter3: P3, parameter4: P4): R; }

It's not the end of the world, but it is pretty annoying.

@weswigham
Copy link
Member

@RonPenton We added generic defaults in #13487 for TS 2.3, so yes - we implemented the equivalent alternative to this.

@lukas-zech-software
Copy link

lukas-zech-software commented Sep 12, 2017

@weswigham I don't see how defaults are solving the problem with @RonPenton's example

I tried this

export interface Func<R=void> {  (): R; }
export interface Func<R=void, P1=void> {  (param1: P1): R; }
export interface Func<R=void, P1=void, P2=void> {  (param1: P1, param2: P2): R; }

const fn1: Func                          = () => {};
const fn2: Func<string>                  = () => '';
const fn3: Func<string, number>          = (param1: number) => ''; //error
const fn4: Func<string, number, boolean> = (param1: number, param2: boolean) => ''; //error

but the compiler complains at
fn3 with TS2322:Type '(param1: number) => string' is not assignable to type 'Func<string, number, void>'.

and at

fn4 with TS2322:Type '(param1: number, param2: boolean) => string' is not assignable to type 'Func<string, number, boolean>'.

it seem like the compiler will always take the first overload of Func

or am I doing something wrong here?

@weswigham
Copy link
Member

weswigham commented Sep 12, 2017

You should only need to have one interface declaration, for one thing:

export interface Func<R=void, P1=never, P2=never> {  (param1: P1, param2: P2): R; }

const fn1: Func                          = () => {};
const fn2: Func<string>                  = () => '';
const fn3: Func<string, number>          = (param1) => '';
const fn4: Func<string, number, boolean> = (param1, param2) => '';

The rest are just adding more call signatures, which will get resolved using overload resolution. If you add them; you need to make sure the "most specific" ones are added first, otherwise what you described can happen (they're all being matched against (): R, since it is ordered "first").

@lukas-zech-software
Copy link

lukas-zech-software commented Sep 12, 2017

There are some drawbacks with that solution:
All function now must provide 2 parameters.

export interface Func<R=void, P1=never, P2=never> {  (param1: P1, param2: P2): R; }

const fn1: Func = () => {};
fn1(); // TS2554:Expected 2 arguments, but got 0.

If I make all parameters optional, then there is no type safety in calling the methods with all expected parameters ....

export interface Func<R=void, P1=never, P2=never> {  (param1?: P1, param2?: P2): R; }

const fn1: Func                          = () => {};
const fn2: Func<string>                  = () => '';
const fn3: Func<string, number>          = (param1) => '';
const fn4: Func<string, number, boolean> = (param1, param2) => '';

fn1(); // fine
fn2(); // fine
fn3(); // should raise error but does not
fn4(); // should raise error but does not

@RonPenton
Copy link

@weswigham I don't believe that solves the issue. I tried to use default generic parameters like @lukas-zech-software did and ran into the same problems. The compiler expects parameters that don't exist.

Try out your code, you'll see that it doesn't work:

export interface Func<R=void, P1=never, P2=never> {  (param1: P1, param2: P2): R; }

const fn1: Func                          = () => {};
const fn2: Func<string>                  = () => '';
const fn3: Func<string, number>          = (param1) => '';
const fn4: Func<string, number, boolean> = (param1, param2) => '';

fn1();                  // Expected 2 arguments, but got 0.
const x = fn2();        // Expected 2 arguments, but got 0.
const y = fn3(123);     // Expected 2 arguments, but got 1.
const z = fn4(1, true); // This is the only one that works. 

@liminf
Copy link

liminf commented Dec 15, 2017

I don't think default values work for the case I am interested in, either. I'm trying to get generic parameter binding working. Creators of a bindable would define the parameters that it accepts, and consumers would bind values for parameters through a "bind" call. Once all parameters are bound then the bind method is no longer available and the call method becomes available.

This looks something like this

interface IBindable<RequiredParams if-non-empty-object> {
    bind : <ParamSet>(p : ParamSet) => IBindable<RequiredParams less params in ParamSet>;
}
interface IBindable<RequiredParams if-empty-object> {
    call: () => Result;
}

In some ways this is very much like the classic C++ factorial template metaprogramming example

@microsoft microsoft locked and limited conversation to collaborators Jun 18, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

10 participants