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

Default generic type variables #2175

Closed
andrewvarga opened this issue Mar 1, 2015 · 64 comments · Fixed by #13487
Closed

Default generic type variables #2175

andrewvarga opened this issue Mar 1, 2015 · 64 comments · Fixed by #13487
Assignees
Labels
Committed The team has roadmapped this issue Fixed A PR has been merged for this issue Suggestion An idea for TypeScript

Comments

@andrewvarga
Copy link

It would be really useful if I could set default values for my generic type variables. In many cases, the users of my class don't want to use a different value and so it's cumbersome to having to type it in.

If I could do this:

class MyClass<T = String>
{
public userData: T;
constructor() {}
}

then the people who don't care about the type of userData (because they don't use it) and the people who are fine with it being a String don't have to set a generic type but others can if they want.

Also, these type variables could default to any or the type they extend (if they do) without explicitly defaulting them.
For example:
<T extends MyClass> would default to MyClass (without explicitly writing it)
and
<T> could default to any

What do you think?

@andrewvarga andrewvarga changed the title Default template parameters Default generic type variables Mar 1, 2015
@fdecampredon
Copy link

👍 It would greatly simplify definition for some lib, for example something like react components would ideally be :

class Component<P,S,C> {
  props: P;
  state: S;
  context:C
  ....
}

but most of the time S and C are not used, with something like :

class Component<P = {}, S = {}, C = {}> {
  props: P;
  state: S;
  context:C
  ....
}

It would be easy to create simple component that don't use those generic types:

class MyComponent extends Component {
}
// instead of
class MyComponent extends Component<{}, {}, {}>{
}

@DanielRosenwasser
Copy link
Member

Interesting idea, though it would seem that this functionality is easily achieved through a subclass.

class Foo<T,U,V> {
    // ...
}

// Or DefaultFoo or BasicFoo
class StringyFoo<U,V> extends Foo<string, U, V> {
    // ...
}

Are there any use cases where doing this becomes annoyingly difficult, or where it doesn't quite achieve the same thing?

@danquirk danquirk added Suggestion An idea for TypeScript Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. labels Mar 2, 2015
@andrewvarga
Copy link
Author

While that could be useful in some cases, I feel I wouldn't use that for these reasons:

  • it's cumbersome to write new classes just for this
  • it's inconvenient for the users of my class, ideally they shouldn't have to know that there is a different class written just for the case of having some generic types default to something.

In @fdecampredon 's example code with 3 generic variables it would be especially hard to cover all the use cases (some users might want to use just P, others just S, others just C, others P and S, etc..

@mdekrey
Copy link

mdekrey commented Apr 1, 2015

I'm working on a library right now that could have really benefited from this. When combined with constraints, I'd expect the syntax to look something like the following:

class Component<P extends IProperties = Properties, S = {}, C extends IContext = Context> {
  props: P;
  state: S;
  context: C;
  ....
}

@poeschko
Copy link

+1. This would be great indeed when working with React.

@andrewvarga
Copy link
Author

Btw, why is this labeled under "Needs Proposal" ? This is already present in C++, not in C# unfortunately. However I don't think this would be hard to implement?

@RyanCavanaugh
Copy link
Member

why is this labeled under "Needs Proposal" ? This is already present in C++

Someone needs to write down what all the rules for this would be.

For example, would defaults be legal in all generic type parameter positions? If so, what's the behavior in function calls?

declare function f<T = string>(a: T, x: (n: T) => void): void;

// What do we resolve 'T' to here? By what mechanism?
f(null, s => s.charCodeAt(0));

@tinganho
Copy link
Contributor

tinganho commented Aug 5, 2015

In addition to default type parameter, it would also be great to have named type argument. So I don't need to care about the order.

class Component extends React.Component<P = T1, S = T2> {
}

@andrewvarga
Copy link
Author

@RyanCavanaugh my first (maybe naive) idea for this was a simple preprocessor which would work like this:

  • for all cases where a generic function / class is used (called / instantiated), and a generic type variable that has a default value is not explicitly set, that default value is injected.

So in the above example, because f is called without given an explicit type it will default to the default generic argument (string) and that's it.
If you change "string" to "number" in the above snippet it will be a compile error because "s" is of type number.

Do you see any problems with this approach? Is it that with a default value we ignore the possibility of type inference that would otherwise work just by looking at the types of the passed function parameters (without setting a generic type)?
Some options are:

  • ignore type inference when there's a default generic argument (the naive idea)
  • if type inference would allow to to infer a type, use that type, otherwise use the default type
  • use the inferred type only if it's "stronger" than the default type (meaning if the inferred type is a subtype of the default type), otherwise use the default type

Let me know if I'm missing something!

@andrewvarga
Copy link
Author

@tinganho that sounds like a good idea!

@mdekrey
Copy link

mdekrey commented Aug 6, 2015

My suggestion would be that type defaulting happens in place of an any type or {} type inference; that is, to expand on @RyanCavanaugh's above example:

declare function f<T = string>(a: T, x: (n: T) => void): void;

// 'T' is a string here, because the default overrides the inference of any
f(null, s => { });

// 'T' is a number here due to existing inference
f(15, s => { });

I find it more important on classes, however, where type inference may not be able to be performed.

class MyClass<T = string, U = IMyInterface>
{
    constructor(a: T) { }
}

// 'T' is a number, because it was inferred.
// 'U' is 'IMyInterface', because it was inferred as '{}' by existing logic.
var myVar = new MyClass(15);

// 'T' is a string, because it was inferred as 'any' by existing logic.
// 'U' is 'IMyInterface', because it was inferred as '{}' by existing logic.
var myVar2 = new MyClass(null);

// 'T' is a number, because it was specified.
// 'U' is 'IMyInterface', because it was inferred as '{}'
var myVar3 = new MyClass<number>(null);

// 'T' is a number, because it was specified.
// 'U' is 'string', because it was specified'
var myVar4 = new MyClass<number, string>(null);

As indicated in my example, I'd propose that, just like default (non-type) parameters in function calls, all type parameters after the first defaulted type parameter must have a default.

I'd also suggest that type constraints are evaluated after all inference is complete, in case it matters for constraint checking. (I can't imagine a situation where it would, given type constraints cannot reference type parameters in the same type parameter list at this time.)

I'd rather not discuss named type parameters, as that could be a separate feature entirely. (Open a separate issue, perhaps, @tinganho?)

@Andael
Copy link

Andael commented Aug 8, 2015

@mdekrey I agree, it would work well if the default type was only relevant for the {} case.

Is there a use case for specifying both a default and extends? My suggestion would be to not support specifying both, since it might complicate type inference rules.

@andrewvarga
Copy link
Author

@ander-nz I think there is a use case for that, to me those are independent features, I may want to use extend to make sure a type is a subclass of something, but I may also want to provide a default type.

class ElementContainer<T = HTMLDivElement extends HTMLElement>
{
   private _el: T;
   constructor(el: T)
   {
      this._el = el;
      el.style.color = "#ff0000";
   }
}
// use:
var container: ElementContainer;

Users of the class don't need to specify a type, it will default to div, but they can specify one if they want, they can also use type inference (by instantiation) and I can also make sure T is a HTMLElement so I can use code in the class that relies on that (eg.: ".style.color = ...")

@Andael
Copy link

Andael commented Aug 9, 2015

@andrewvarga That's a good example use case for both. While I agree that those are independent features, they do of course have some overlap that would need to be considered.

@mdekrey
Copy link

mdekrey commented Aug 24, 2015

Taking from @andrewvarga's example, I'd actually prefer that the default goes after the extends clause; T needs to be what extends HTMLElement, as we know that HTMLDivElement already does.

class ElementContainer<T extends HTMLElement = HTMLDivElement>
{
   private _el: T;
   constructor(el: T)
   {
      this._el = el;
      el.style.color = "#ff0000";
   }
}
// use:
var container: ElementContainer;

As such, I guess my proposal becomes:

interface IMyBaseInterface { }

interface IMyInterface extends IMyBaseInterface { }

interface IMyOtherInterface extends IMyBaseInterface { }

class MyClass<T = string, U extends IMyBaseInterface = IMyInterface>
{
    constructor(a: T) { }
}

// 'T' is a number, because it was inferred.
// 'U' is 'IMyInterface', because it was inferred as '{}' by existing logic.
var myVar = new MyClass(15);

// 'T' is a string, because it was inferred as 'any' by existing logic.
// 'U' is 'IMyInterface', because it was inferred as '{}' by existing logic.
var myVar2 = new MyClass(null);

// 'T' is a number, because it was specified.
// 'U' is 'IMyInterface', because it was inferred as '{}'.
var myVar3 = new MyClass<number>(null);

// 'T' is a number, because it was specified.
// 'U' is 'IMyOtherInterface', because it was specified.
var myVar4 = new MyClass<number, IMyOtherInterface>(null);

Is there anything else needed to make a formal proposal?

@joshuafcole
Copy link

I second @mdekrey's proposal, I was just looking for exactly this.

@omidkrad
Copy link

Related issue: #209 - Generic Parameter Overloads

@drarmstr
Copy link

It would also be very helpful to allow the default types to themselves be generic types. For example:

class Component<T, U=T> {
    ...

This way the second type defaults to being the same as the first, but the user can override this if desired.

@RyanCavanaugh RyanCavanaugh added In Discussion Not yet reached consensus and removed Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. labels Nov 3, 2015
@RyanCavanaugh
Copy link
Member

Discussed at the design backlog review today.

How do people feel about the following proposal?

  • Generic type arguments which have a constraint default to their constraint when that argument is not specified
  • No constraint means the argument is required

In code:

interface Alpha<T extends HTMLElement> { x: T }
interface Beta<T> { x: T }
interface Gamma<T extends {}> { x: T }

var x: Alpha; // Equivalent to var x: Alpha<HTMLElement>
var y: Beta; // Error, type parameter T has no default (has no constraint)
var z: Gamma; // Equivalent to var z: Gamma<{}>

@drarmstr
Copy link

drarmstr commented Nov 3, 2015

Could you have a constraint also be a type variable?

interface foo<T, U extends T> {

On Nov 2, 2015, at 18:06, Ryan Cavanaugh [email protected] wrote:

Discussed at the design backlog review today.

How do people feel about the following proposal?

Generic type arguments which have a constraint default to their constraint when that argument is not specified
No constraint means the argument is required
In code:

interface Alpha { x: T }
interface Beta { x: T }
interface Gamma<T extends {}> { x: T }

var x: Alpha; // Equivalent to var x: Alpha
var y: Beta; // Error, type parameter T has no default (has no constraint)
var z: Gamma; // Equivalent to var z: Gamma<{}>

Reply to this email directly or view it on GitHub.

@gcnew
Copy link
Contributor

gcnew commented Jan 23, 2017

Generic parameter defaults will make the implementation of #1213 and #9949 considerably harder. They seem challenging at presents, so it doesn't help.

Defaults are a source of ambiguities as well. Adding a default may make a function backward incompatible. E.g.

declare function c<A>(f: (x: A) => A, g: (a: A) => A): A;

declare function f<T = number>(x: T): T;
declare function g<T = string>(x: T): T;

c(f, g) // A = string / number or T?

Better type inference and type aliases are more obvious and consistent solutions since defaults help only in the case envisioned by the library/typings author and are opaque to the code maintainers.

The only true benefit of defaults is generifying existing typings (as pointed out by @blakeembrey's example) but I'd much rather see real generics first..

@dead-claudia
Copy link

@gcnew

The only true benefit of defaults is generifying existing typings (as pointed out by @blakeembrey's example) but I'd much rather see real generics first.

If nested types make it, then this would all of a sudden become far more interesting. You could make a React type namespace, so you could properly type the library, including raw element references, and ensure that React Native-specific components can't be erroneously used in DOM-specific or renderer-independent components. That being simply by using a generic React type instead. It's an extra layer of verification that would allow much more thorough typing.

declare function c<A>(f: (x: A) => A, g: (a: A) => A): A;

declare function f<T = number>(x: T): T;
declare function g<T = string>(x: T): T;

c(f, g) // A = string / number or T?

That would be a good question, though.

@bcherny
Copy link

bcherny commented Feb 8, 2017

@RyanCavanaugh Are you guys planning to add a milestone to this guy? Looks like you had the spec (and implementation?) mostly fleshed out.

@RyanCavanaugh
Copy link
Member

@bcherny work in progress at #13487. I would expect this to be in 2.2 but may slip to 2.3 if we need a few more revisions on the PR.

@RyanCavanaugh RyanCavanaugh added Committed The team has roadmapped this issue and removed In Discussion Not yet reached consensus labels Feb 8, 2017
@bcherny
Copy link

bcherny commented Feb 8, 2017

Awesome, thanks for the excellent work @RyanCavanaugh!

@iamssen
Copy link

iamssen commented Feb 16, 2017

👍 Thanks. I've been wating a long time.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Committed The team has roadmapped this issue Fixed A PR has been merged for this issue Suggestion An idea for TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.