Skip to content

Suggestion: Generic Parameter Overloads #209

Description

@omidkrad

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

Activity

  1. danquirk commented on Jul 30, 2014

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

  2. omidkrad commented on Jul 30, 2014

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

  3. omidkrad commented on Jul 30, 2014

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

  4. mikecann commented on Aug 14, 2014

    @mikecann

    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> {
        }
    
  5. johnnyreilly commented on Aug 14, 2014

    @johnnyreilly

    👍

  6. sccolbert commented on Dec 4, 2014

    @sccolbert

    👍

  7. omidkrad commented on Dec 4, 2014

    @omidkrad
    Author

    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 Dan Quirk (@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.

  8. RonPenton commented on Aug 31, 2017

    @RonPenton

    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.

  9. weswigham commented on Sep 12, 2017

    @weswigham
    Member

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

  10. lukas-zech-software commented on Sep 12, 2017

    @lukas-zech-software

    Wesley Wigham (@weswigham) I don't see how defaults are solving the problem with Ron Penton (@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?

  11. weswigham commented on Sep 12, 2017

    @weswigham
    Member

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

  12. lukas-zech-software commented on Sep 12, 2017

    @lukas-zech-software

    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
  13. RonPenton commented on Sep 12, 2017

    @RonPenton

    Wesley Wigham (@weswigham) I don't believe that solves the issue. I tried to use default generic parameters like Lukas Zech (@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. 
    
    
  14. liminf commented on Dec 15, 2017

    @liminf

    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

  15. 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

    Needs ProposalThis issue needs a plan that clarifies the finer details of how it could be implemented.SuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions