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

Rest type parameter in generics for use with intersection types #3870

Closed
bryanforbes opened this issue Jul 15, 2015 · 20 comments
Closed

Rest type parameter in generics for use with intersection types #3870

bryanforbes opened this issue Jul 15, 2015 · 20 comments
Labels
Duplicate An existing issue was already created Suggestion An idea for TypeScript

Comments

@bryanforbes
Copy link
Contributor

A common pattern in JavaScript libraries is to copy properties from a variable number of arguments onto the first argument. This is also the behavior of Object.assign. A simple implementation of this pattern in TypeScript might look like this:

function assign(destination: any, ...sources: any[]): any {
    sources.forEach(function (source) {
        for (let key in source) {
            if (source.hasOwnProperty(key)) {
                destination[key] = source[key];
            }
        }
    });
    return destination;
}

This works fine, however with intersection types in 1.6 this could be better:

function assign<T, U>(destination: T, ...sources: U[]): T & U {
    ...
}

This works for one argument, but more than one fail because U isn't actually the type needed for the intersection. What is needed (as suggested here) is a rest type parameter. It might look something similar to the following:

function assign<T, ...U>(destination: T, ...sources: U[]): T & U {

U would be the intersection of all rest parameters passed to assign() and the return value would expand to something like T & (U0 & ... UN). With the push for composable types and ES6 standardizing the above behavior (in Object.assign) a syntax for generics like this would be very useful.

@danquirk
Copy link
Member

This has definitely come up before at least in conversation/comments (particularly in the context of intersection types + mixins) but I can't find an issue for it at the moment.

@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 Jul 15, 2015
@JsonFreeman
Copy link
Contributor

I think it could make sense to declare a family of type parameters <T, ...U>. For type argument inference, this could mean that U is actually the list of all the inference candidates, rather than their common type.

The thing is, we would have to define a set of spread operators that apply to type parameter families. Intersection and union are certainly prime candidates. Tuples are perhaps another, and maybe even parameter lists.

I think we could say that it is illegal to spread a type parameter family in its parameter list. It can only be spread in the return type.

@JsonFreeman
Copy link
Contributor

Oh wait, there is a problem. Of all the spread operators I have suggested, only union is commutative. That is bad for all the others, because if we spread inference candidates in a non-commutative way, we will surface the order of the inference candidates. I think we do not want to do that.

@JsonFreeman
Copy link
Contributor

We could say that a rest type parameter can only be used in a rest parameter, and spread in the return type. And then it would be a family of types formed in the following way:

  • The family would have one element for each argument matching the rest parameter.
  • Within the rest parameter, if the type parameter is referenced multiple times, then you must take the common supertype of all the inferences made from within a single argument. That way, the only order we surface in the family is the order of the arguments the caller passed, which is not controversial.

@bryanforbes
Copy link
Contributor Author

As I mentioned on #3622, there are actually two things needed for this to work: indicating that a generic type for a rest parameter is a union type (rather than a common sub-type of all of the rest parameters) and a way to intersect those with another type so they end up being an intersection of all (T & U0 & U1 rather than T & (U0 | U1)).

For the first (indicating a union instead of a common supertype), I'm not sure if it would be better in the generic type parameters or in the function parameters:

function assign<T, ...U>(target: T, ...sources: U[]): /* return type TBD */ {

function assign<T, U>(target: T, ...sources: ...U): /* return type TBD */ {

For the second (converting a union to an intersection), it might be nice to use &&:

function assign<T, ...U>(target: T, ...sources: U[]): T && U {

@JsonFreeman
Copy link
Contributor

I think we want to declare up front whether U is a single type parameter or a family of type parameters. Allowing U in some places and ...U in others (in the parameter list and return type) will make it confusing. So I would prefer to declare ...U in the type parameter list, and spread it in the return type.

I do not think there is anything magical about unions. I think it makes more sense to just think of U as a family of type parameters (with no particular operation in mind). The operation can be determined in the return type when you spread U.

I think there are two possible ways to handle spreading. One is to say that T & ...U means that the elements of U are intersected. However, this does not allow you to write something equivalent to T & (U0 | U1) or T & [U0, U1]. That may be fine. If we want more expressiveness though, we may want to make the spread specify which aggregator to use, something like T & ...&U or T & ...|U. I actually prefer the first option, where the aggregation mechanism is determined by the parent operator.

For starters, there are three places we could allow spreading:

  1. Intersection: T & ...U
  2. Union: T | ...U
  3. Tuples: [T, ...U]

@JsonFreeman
Copy link
Contributor

Oh, there's a problem. Because of the ES6 spread operator, the compiler might not know how many arguments were passed. Suppose we treat each spread argument as one element of the family. That is consistent, but we might get the final arity of the family wrong. To that end, we cannot spread the family in any arity-sensitive way. So we could only spread in intersections and unions, but not tuples. I think this is fine, and @bryanforbes you are asking for intersection, so it would solve your issue.

@Arnavion
Copy link
Contributor

Arnavion commented Aug 3, 2015

Since tuples extend a union of their constituent types for indices higher than the explicitly specificed member types, won't they still work?

@JsonFreeman
Copy link
Contributor

But tuples have an arity, and I'm saying that the arity would have to depend on how many arguments were passed. But the number of arguments cannot be known statically when it is a spread argument.

@Arnavion
Copy link
Contributor

Arnavion commented Aug 4, 2015

function manyInTupleOut<...T>(...params: Array<...T>): [...T];

var input: [number, string, Document];
var output = manyInTupleOut(...input);

You're saying that ...input expands to an unknown number of elements, so the specialization of manyInTupleOut is unknown.

I'm saying that input's type is known to be [number, string, Document, number|string|Document, number|string|Document, ...] even if the actual number of elements is not known. So the specialization of manyInTupleOut can be <number, string, Document, number|string|Document, number|string|Document, ...>. Thus output will be of type [number, string, Document, number|string|Document, number|string|Document, ...] which is indistinguishable from the intended [number, string, Document]

It doesn't seem necessary to know the number of elements in input.

@JsonFreeman
Copy link
Contributor

Ah I see what you're saying. But the arity does matter, because you are not allowed to assign a shorter tuple to a longer tuple type.

var a: [number, string];
var b: [number, string, number | string];

a = b; // Allowed
b = a; // Error

@Arnavion
Copy link
Contributor

Arnavion commented Aug 4, 2015

Oh, didn't know that.

@blakeembrey
Copy link
Contributor

Just a note on another (related?) use-case, but I was thinking about how/if this could be used to describe partial application? Right now I write out each combination manually which increases exponentially in size.

function partial <T, ...U> (fn: (...args: U[]) => T, ...args: U[]): (/* What goes here? */) => T

@JsonFreeman
Copy link
Contributor

@blakeembrey How would the function decide how many arguments to "use up" in the partial application? Why would it even need to take all of them, if it is only going to apply the function to some of the arguments?

@blakeembrey
Copy link
Contributor

@JsonFreeman Honestly, not sure, I haven't thought on it for long enough - just saw it was similar - so I posted it hoping someone smarter than I could toil with it for a moment. The only syntax I can think of would be two spread arguments - though illegal.

function partial <T, ...U, ...V> (fn: (...args: U[], ...illegal: V[]) => T, ...args: U[]): (...args: V[]) => T

@JsonFreeman
Copy link
Contributor

Oh, I think I misunderstood. I see what you are trying to do now. You're right, I don't think the proposal here could achieve it because it requires selecting only a portion of the type parameters in the family. I am not sure whether it is better to propose an alternative higher order generics scheme that solves this and the original issue together, or if they are better explored separately.

@JsonFreeman
Copy link
Contributor

I don't think intersection is special. I think there are a number of ways you might want to combine multiple types. So I think it does not make sense to assume that ...T would be intersection. Also, in that case, what would it mean if you just referenced T instead of ...T?

@Arnavion
Copy link
Contributor

@jbondc

Current behavior is typeof mixA which I don't really understand.

It's because mixA, mixB and mixC are all the same type {}, so typeof mixA is a valid specialization for T. Try adding unique members to them.

@sccolbert
Copy link

Related to the issue on variadic generics: #1773

@mhegazy
Copy link
Contributor

mhegazy commented Feb 22, 2016

closing in favor of #1773

@mhegazy mhegazy closed this as completed Feb 22, 2016
@mhegazy mhegazy added the Duplicate An existing issue was already created label Feb 22, 2016
@mhegazy mhegazy removed the Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. label Feb 22, 2016
@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Duplicate An existing issue was already created Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

7 participants