Closed andrewvarga closed 7 years ago
:+1: 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<{}, {}, {}>{
}
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?
While that could be useful in some cases, I feel I wouldn't use that for these reasons:
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..
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;
....
}
+1. This would be great indeed when working with React.
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?
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));
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> {
}
@RyanCavanaugh my first (maybe naive) idea for this was a simple preprocessor which would work like this:
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:
Let me know if I'm missing something!
@tinganho that sounds like a good idea!
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?)
@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.
@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 = ...")
@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.
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?
I second @mdekrey's proposal, I was just looking for exactly this.
Related issue: #209 - Generic Parameter Overloads
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.
Discussed at the design backlog review today.
How do people feel about the following proposal?
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<{}>
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 notifications@github.com 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.
@drarmstr see #2304
While this sounds useful too it would only be a half-baked solution for me. Is there a technical reason the default value assignment "= SomeType" couldn't be implemented?
Going back to my example:
class ElementContainer<T extends HTMLElement = HTMLDivElement>
{
private _el: T;
constructor(el: T)
{
this._el = el;
el.style.color = "#ff0000";
}
}
// use:
var container: ElementContainer;
Here I would like T to extend HTMLElement but the default should be HTMLDivElement since that's the most common use case by far (yet I don't want to hard code it to that). I could of course create a "DivContainer" like this, but the point of this feature would be that we don't have to create new classes/interfaces for each default type combination:
class DivContainer extends ElementContainer<HTMLDivElement> { ... }
Also I think it would be useful to be able to use default values without needing to have a constraint.
@andrewvarga
Is there a technical reason the default value assignment "= SomeType" couldn't be implemented?
It's not impossible; we're just looking for the simplest thing that fulfills the use cases. If constraints can be the default and satisfy 95%+ of people, it's less cognitive load and less work for people writing .d.ts or other definitions.
In your example of class ElementContainer<T extends HTMLElement = HTMLDivElement>
, we were a bit confused by what the actual implementation of ElementContainer
would look. In other words, what runtime behavior would that default correspond to? How does ElementContainer
know when to instantiate an HTMLDivElement
vs something else?
Actually, I reviewed my use-cases as well and the extends
approach wouldn't be useful either. Examples are classes for structures that really don't have a constraint, but where defaults are common. Consider a chart that is generic for an arbitrary data type, but commonly defaults to number
. A constraint wouldn't be appropriate there.
Subclasses aren't always useful, either, since some cases I am just working with .d.ts declarations of non-TypeScript libraries and I wouldn't want to add a new .js/.ts file just for that.
@RyanCavanaugh the ElementContainer is basically a wrapper class, it just represents an HTMLElement. If you're familiar with Ext.js, it's very similar in purpose to the "Ext.Element" class which has a "dom" property to access the actual HTMLElement instance. With my ElementContainer class it could be accessed with a getter named "el", I just omitted that from the code.
Btw this is a class that I actually use in all of my projects heavily, there is an interface pair too, IElementContainer which looks something like this:
interface IElementContainer<T extends HTMLElement>
{
el: T;
}
I use this interface and class for building UI components: anything that can be added to the dom will implement IElementContainer. Some examples classes are: Button, FlexContainer, Input, TimeInput, NumberInput, ColorInput, etc. ElementContainer accepts a single "element" parameter in it's constructor, which is optional and if omitted a new div will be created. (Of course it would be hard to enforce that if the type defaults to something other than HTMLDivElement like HTMLSpanElement than a span should be created instead of a div, but that's ok, it's not a requirement). ElementContainer also has some convenience methods to work with it's element, like "detach" which detaches it from it's parent (if there's one), "getChildByDataId" which returns the child that has the given "data-id" attribute (I avoid using global ids). The main use for me is that I can rely on a type that I know has an HTMLElement so I can add that to the dom, remove it, etc, of course I can do that with the actual HTMLElement, but without the convenience methods, and moving these methods to be static wouldn't be nice, currently I feel this is a nice way improve elements without messing with something like
HTMLElement.prototype.sayHello = function() { console.log("Hello"); }
If that helps I can share how the actual class looks like.
+1, useful in d3.csv function.
I don't think that I would find that the most basic extends as a default would work for nearly 95% of my use cases.
Also, I don't want every generic that specifies constraints to default; that could result in invalid usage.
I believe that a refactor "story" would illustrate a common use case where the extended value shouldn't be the default.
Bar
and encapsulates it in a class Foo
.Foo
in another area of his application if he made it generic. This new area uses the class Fighter
. Fighter
does not extend Bar
. Bob wants to refactor, but doesn't want to alter how Aaron uses the class in his areas of the site.<T = Bar>
to Foo
, refactoring only the one class, while leaving all existing usage of Foo
in place.Foo<Fighter>
where he needed to.This might be especially applicable when moving from a standard usage of HTMLInputElement
to a generic that supports other HTMLElement
s, for instance.
We need this pretty badly for React -- to support context properties we want to add an additional type parameter to the React.Component
class, but it's untenable to break every React class in the world. Proposal to follow
TypeParameter: BindingName Constraint opt Default opt Default:
=
Type
Examples
// Without constraint
interface ReactComponent<TProps = {}> {
}
// With constraint
interface NodeList<T extends HTMLElement = HTMLElement> { }
It's tempting to say "Type parameters are identical if [...] they specify identical defaults", but presumably we want to provide a default for Array
(interface Array<T = any> { ... }
) to enable people to write var x: Array
, but if all type parameter declarations must have identical defaults, we'd break everyone augmenting Array
in a very annoying way -- they'd have to have a pre-default and post-default version of their definition files.
When a generic type appears in a type reference with fewer type arguments than type parameters, the unprovided type parameters are treated as if their default type was provided instead. If no default was specified for an unprovided type parameter, an error occurs [as is the case today]
Section 4.15.2 is modified:
If the set of candidate argument types is empty, the inferred type argument for T is T's constraint.
to
If the set of candidate argument types is empty, the inferred type argument for T is T's default, or T's constraint if no default is present
A type parameter with a default may not be followed by a type parameter without a default
I don't think this constraint is necessary, mostly because inference order/usage order isn't tied to definition order - it's quite easy to write something along the lines of
function bind<Elem extends HTMLElement = BodyElement, V>(value: V, elem: Elem = document.body): Elem {}
though there's no particular reason you couldn't reorder the type parameters unless you had some circularly referential parameters whose usage was only clear in some order where the one with a default came first.
I think the problem is that given a type T<U, V?, W>
, it's not clear what T<number, string>
means. Does V
get skipped until there are 3 arguments? Or the default is useless in type reference positions?
I think that when manually specifying type parameters, it should functionally mimic default function parameters. This means that T<number, string>
passes the first two parameters, regardless of defaultness of any parameters.
@RyanCavanaugh The downside I see is that there's no way to pass undefined
in a type position to get the default value for a parameter which does not come last, as there is with function default arguments.
Wouldn't @tinganho 's idea of named type parameters help in the case of only wanting to specify a few positions?
Another example use case: In the pandas library in python, the base object Pandas.Series, is basically an ordered map/array with lots of utility methods. The constructor accepts an array of data, and another array of indices, of the same length, that map to the data. If the array of indices is not given then it defaults to using numbers for the indices. I tried porting it to typescript.
class Series<A, B> {
constructor(data: Array<A>, index?: Array<B>) {
...
}
loc(item: B) : A { ... }
}
let example1: Series<number, number> = new Series([1,2,3],[4,5,6]); //fine
example1.loc(4) // returns 1;
let example2: Series<number, string> = new Series([1,2,3],['a','b','c']); //fine
example2.loc('a') // returns 1;
let example3: Series<number, number> = new Series([1,2,3]); //won't typecheck currently unless cast
example3.loc(0) // returns 1
Even though B has a specific type if not provided, currently I don't see a way to express that. It seems like a default generic type would be the nicest way to handle it.
Working on this during my holiday flights :santa:
A few edge cases I'm looking at
// Forward reference: Legal or not?
interface Foo<T = U, U = number> { }
// Wrapped self reference: Legal or not?
interface Bar<T = Array<T>> { }
// Circular reference: Clearly illegal?
interface Qua<T = U, U = T> { }
My inclination is to say that you cannot reference any type parameters from your own declaration other than those strictly to the left (i.e. T
's default may not reference T
nor any following type parameter)
I think intuitively that ordering makes sense. If someone did want a forward reference it could achieved by reordering.
interface Foo<U = number, T = U>
I would vote for wrapped self-reference, mostly because it would add noise to type declarations if it were disallowed.
interface Bar<U, T = Array<U> > { } //looks like there are 2 type parameters but really only one is intended.
I would vote for wrapped self-reference
So clearly interface Bar<U, T = Array<U> > { }
is allowed. But I don't understand what the wrapped self-reference means.
interface Foo<T = Array<T>> { x: T };
let a: Foo;
let b = a.x;
The type of b
here is.... Array<Array<Array<Array<...forever>>>>
? Someone intended to do that?
I agree the recursive definition doesn't make much sense.
When I read interface Bar<T = Array<T>> { }
, I interpreted that as Bar takes an argument of type T, but will default to being an Array of type T if none is given.
Being able to specify a container as a default seems like a likely use case and my example was that requiring two parameters to describe that seems noisy.
Big +1 on this. I'm working on a project right now that could really benefit from this
This would be very useful in many places in my projects.
Here's a highly simplified version of a method that returns an entry from a local database. The generic type associated with an entry is TEntry
:
class MyDatabase<TEntry> {
get(key: string): TEntry {
...
}
}
I want to allow the user to 'override' that return type only for that particular get
call, but currently there is no simple way for me to achieve that. The only thing that may be done is to require the user to specify a particular return type every time they call get
(and they practically must, or else they would get the {}
type):
get<R>(key: string): R
With a generic default I could have just defined:
get<R = TEntry>(key: string): R
Which would have solved the problem very naturally.
(I have tried to work around this by using overloads (though the method itself is already overloaded so it makes it more complicated) but they don't seem to play nicely with generics)
Is there something we can do to move this forward? It seems to be neglected but this would be a very important and useful feature to add.
I currently use overloaded method signatures to achieve one possible benefits of default generic type parameters.
children<T>(): CommonIterator<Row> { /* ... */ } // Row extends Element
children(): CommonIterator<Element> { /* ... */ }
Could be simplified
children<T = Element>(): CommonIterator<T> { /* ... */ }
I only skimmed the comments, not sure if this use case is posted anywhere above. It sounds like it was kind of covered by the Dec 22 proposal though.
Is issue this supposes overloaded generic types? Not just default values.
MyGenericType<T>
MyGenericType<T1,T2>
So do we still have to specify properties which are Map
s or Set
s like this?
private foo: Map<any, any> = new Map();
As opposed to simply:
private foo: Map = new Map();
The latter complains that Generic type Map<K, V> requires 2 type arguments
@mjohnsonengr Thanks, your comment helped me to solve my own critical problem. However, generic parameter on class, it's not solved yet.
Is this discussion alive?
I hope TypeScript to support the default generic parameter even C++ supports.
This would be extremely useful!
Now that we're already past TS 2.0, is there any chance this will be added? We'd really appreciate it!
I would love this addition. An additional thing I believe it'd solve for me is actually backward compatibility and default usage when typing a definition. The situation I just run into is that I'd love to correct node.d.ts
readable streams (because they either exist in normal or object mode). However, fixing this by adding a generic breaks every single person using node.d.ts
today. With default generics, I could instead apply class Readable <T extends any = string | Buffer>
which allows people to override when in object mode (which is also pretty useful, for instance, when typing a database client that might return a readable stream in object mode). As a result, I hesitate to fix definitions to be correct.
For module authors, I think this would also be useful. Currently, if any function signature is changed to support a generic, that is technically a major breaking change. It can be tricky to remember that for TypeScript users, especially if you're a JavaScript module author who is typing the .d.ts
file on the side.
Edit: If anyone has a workaround for this though, I'd love to hear it. I suppose the best solution is probably going to be use an overload and move away from a class to be able to overload the generic by making two interfaces (the overloaded new function and the prototype).
I'll take a first pass at a proposal. It's only a quick overview and may be missing some details but I'd love to get conversation going again. The goal of the proposal is to support default generic parameters (similar to default JavaScript parameters, but for the type system). It will not cover the issue of skipping parameters (https://github.com/Microsoft/TypeScript/issues/10571) but will comment on the relationship.
For compatibility with existing TypeScript:
function foo <T = string> (x: T): void
function foo <T extends string = 'abc'> (x: T): void
This should not impact the proposal in any way, but I'll propose an alternative syntax that could be used to supercede the current extends
implementation inspired by how the type system currently works in arguments and https://flowtype.org/blog/2015/03/12/Bounded-Polymorphism.html.
function foo <T = string> (x: T): void
function foo <T: string = 'abc'> (x: T): void
Note: I will implement all the below rules using extends
to avoid undue confusion with additional syntax changes, but believe it's more succinctly described using :
syntax as the idea already exists for function parameters.
The default parameter is used when the generic has not specified explicitly. The initial proposal does not include https://github.com/Microsoft/TypeScript/issues/10571 and would, therefore, only support omitting parameters at the end of the generic list (described below). The issue of omiting generics can be proposed at a later date as the features go together nicely.
Generic parameters can currently express a type which they wish to "extend". This proposal adds the ability to specify the default type of the generic when the parameter is not explicitly stated (currently the default is whatever the generic "extends" or {}
). The proposal lightly demonstrates omitted the parameter at the end, but for all intents and purposes it can continue to follow the current rules and a future proposal will cover omitted parameters.
function test <A = HTMLElement, B extends HTMLElement = HTMLDivElement> (): void
//=> `A` is equivalent to `A extends HTMLElement` today.
//=> `B` is equivalent to `B extends HTMLElement` today, but has a more specific interface as the default.
test() //=> `typeof A = HTMLElement`, `typeof B = HTMLDivElement`.
test<HTMLDivElement>() //=> `typeof A = HTMLDivElement`, `typeof B = HTMLDivElement`.
function test <A = HTMLElement, B> (): void
// Valid, but has not major advantage until omitting parameters is proposed. Only provides benefit when no generics are provided at all.
//=> This acts like `<A extends HTMLElement, B>` today (minus the default parameter).
test() // Valid, `typeof A = HTMLElement`, `B = {}`.
test<HTMLElement>() // Invalid, supplied parameters do not match any signature of call target.
test<HTMLElement, HTMLElement>() // Valid, works the same as today.
class Readable <T: any = string | Buffer> {
read(size?: number): T;
}
new Readable() // Valid, `typeof T = string | Buffer`.
new Readable<{}>() // Valid, `typeof T = {}`.
function test <T = U, U = number> (): void
//=> This acts like `<T extends U, U extends number>` today.
test() // Valid, `typeof T = number`, `typeof U = number`.
test<123>() // Valid, `typeof T = 123`, `typeof U = number`.
test<123, 123>() // Valid, `typeof T = 123`, `typeof U = 123`.
test<123, 456>() // Invalid, follows the same rules as today.
// Note that the generic parameter (not default) acts like today and the default would not result in any constraint changes.
function test <T = number, U = T> (): void
//=> This acts exactly like `<T extends number, U extends T>` today.
test() // Valid, `typeof T = number`, `typeof U = number`.
test<123>() // Valid, `typeof T = 123`, `typeof U = 123`. Notice that as it's not specified, it has used the default which was specified.
test<123, 123>() // Valid, `typeof T = 123`, `typeof U = 123`.
test<123, 456>() // Invalid, follows the same rules as today.
function test <T, U = number> (): void
//=> This acts exactly like `<T, U extends number>` today.
test() // Valid, `typeof T = {}`, `typeof U = number`.
test<123>() // Valid, `typeof T = 123`, `typeof U = number`.
test<123, 123>() // Valid, `typeof T = 123`, `typeof U = 123`.
function test <T = number, U extends number = T> (): void
//=> This acts exactly like `<T extends number, U extends number>` today, except the last generic parameter can be omitted.
test() // Valid, `typeof T = number`, `typeof U = number`.
test<123>() // Valid, `typeof T = 123`, `typeof U = 123`.
test<123, 123>() // Valid, `typeof T = 123`, `typeof U = 123`.
test<123, 456>() // Valid, `typeof T = 123`, `typeof U = 456`.
Let me know if I missed something obvious with this, first time trying to write a proposal up and I think most of the behaviour is actually defined already. Hopefully the proposal is simpler for skipping the omitted parameters proposal, but if this is successful I can write that up next.
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:
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 aString
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 toMyClass
(without explicitly writing it) and<T>
could default toany
What do you think?