Typescript: Default generic type variables

Created on 1 Mar 2015  ·  64Comments  ·  Source: microsoft/TypeScript

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?

Committed Fixed Suggestion

Most helpful comment

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.

All 64 comments

:+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:

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

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:

  • 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!

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

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?

  • 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<{}>

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

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.

  1. Aaron creates a class that has some basic functionality around a particular type, Bar and encapsulates it in a class Foo.
  2. Bob later realizes that he also could use 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.
  3. Bob can add <T = Bar> to Foo, refactoring only the one class, while leaving all existing usage of Foo in place.
  4. Bob can use 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 HTMLElements, 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

Grammar

TypeParameter:
_BindingName_ _Constraint opt_ _Default opt_
Default:
= _Type_

Examples

// Without constraint
interface ReactComponent<TProps = {}> {
}
// With constraint
interface NodeList<T extends HTMLElement = HTMLElement> { }

Rules

  • Type parameters are identical if [... the old rules] and all declarations which specify a default specify identical defaults (see remarks)
  • A type parameter default must be assignable to the constraint of the type parameter, if one exists
  • A type parameter with a default may not be followed by a type parameter without a default

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.

Defaults in Type References

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]_

Defaults in function calls

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 Maps or Sets 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.

Syntax

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

Rules

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

  1. The default generic type can be any valid type
  2. The default generic type must be assignable to the "extends" type
  3. Default generic types can reference other generic parameters, but can not be circular (follows the current rules)
  4. The "extends" of the generic defaults to the default type when it has not been specified explicitly

Examples

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.

Another thing that would be really useful is to be able to only specify some of type variables with the rest of them being inferred.

Example use case:
With reselect you can write stuff like this:

const selector = createSelector(
  state => state.a,
  state => state.b,
  (a, b) => a + b
);

selector({a: 1, b:2})  // 3

We can type it as follows:

type Selector<S, R> = (state: S) => R;

function createSelector<S, T1, T2, R>(
  selector1: Selector<S, T1>,
  selector2: Selector<S, T2>,
  combiner: (arg1: T1, arg2: T2) => R
);

But actually everything except for S can be inferred from the code. I usually write it like this:

const selector = createSelector(
  (state: MyState) => state.a,
  (state: MyState) => state.b,
  (a, b) => a + b
);

But it would be great if I could only write MyState once:

const selector = createSelector<MyState, ?, ?, ?>(
  state => state.a,
  state => state.b,
  (a, b) => a + b
);

@aikoven You might be interested in https://github.com/Microsoft/TypeScript/issues/10571 instead. That proposal is more for the other side of the equation, while this one would be for setting the additional generics to = {} as a default when omitted.

Hi! Just to adjust a proposal:

The short form for omitting actual parameters should be

MyClassWithDefaultGenerics<>

Otherwise we would block #1213 (it would be almost impossible to implement)

@DanielRosenwasser ?
@isiahmeadows

I was surprised today to find this wasn't already in TypeScript. Sad for me. And I really don't want the trailing empty <>.

@Artazor Actually, I disagree. If necessary, you can just use a type alias with a dummy parameter (or two, etc.) for higher-kinded types, and just leave the all-default case require no parameters. Here's a quick overview of what I mean:

type KeyPromise<T extends string | number = string | number> = Promise<T>;
interface Database<P<~> extends PromiseLike<~>> {   
    query<T>(s:string, args: any[]): P<T>;
}

// Using the default parameter
type KeyDatabase = Database<KeyPromise>;

// Using a specialization (note the unused `T`)
type NumberPromise<T> = KeyPromise<number>;
type NumberDatabase = Database<NumberPromise>;

@isiahmeadows, could you suggest a reasonable behavior for the following type checking result:

interface Wrapper<T> {value:T}
interface IdWrapper<T = {}> extends Wrapper<T> { id: string }

interface Test<W<~> extends Wrapper<~>> {
      num: W<number>;
      str: W<string>;
}

type T1 = Test<Wrapper>; // ok
// T1 = { num: {value: number}; str: {value: string}}
type T2 = Test<Wrapper2>; // what should be reported here?

Should the second instantiation be rejected?
If we remove defaults on IdWrapper declaration, then T2 would be constructed without any problems.
And if we add it again, then there an error should be reported: since Wrapper2 used alone should be treated as instantiated final type, instead of expected generic that takes one parameter.

Btw, I do not understand your example. Could you elaborate what signatures are expected for the query in both examples?

I suspect that the correct behavior is to defer an application of default generic parameters till the moment when exact count of generic parameters will be determined from the context:

  • if the context asks less generic parameters -- then leave asked generic parameters open (ignoring possible defaults) and instantiate the rest with the defaults (or report an error if there are no default types specified).

In that case if we have an interface that accepts an unary type constructor

interface X<T<~>> { value: T<number> }

and a binary type constructor with two defaults

interface Y<A = string, B = boolean> { a: A, b: B}

then the application should silently convert binary Y<A,B> to the unary Y<A> = {a:A, b: boolean} by applying the trailing default parameters:

type XY = X<Y>;  /* should be {
     value: {
         a: number, // due to X
         b: boolean // Y's second default parameter
     }
} */

@Artazor That was my original intuition with my idea.

And to clarify, the query signature for my two examples would be this:

// Using the default parameter
interface KeyDatabase extends Database<KeyPromise> {
    query<T>(s:string, args: any[]): KeyPromise<T>;
}

// Using a specialization (note the unused `T`)
type NumberPromise<T> = KeyPromise<number>;
interface NumberDatabase extends Database<NumberPromise> {
    query<T>(s:string, args: any[]): KeyPromise<number>; // i.e. NumberPromise<T>
}

Generic parameter defaults will make the implementation of https://github.com/Microsoft/TypeScript/issues/1213 and https://github.com/Microsoft/TypeScript/issues/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..

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

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

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

Awesome, thanks for the excellent work @RyanCavanaugh!

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

Was this page helpful?
0 / 5 - 0 ratings