You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Rest type parameter in generics for use with intersection types #3870
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:
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:
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.
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.
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.
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.
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.
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:
functionassign<T, ...U>(target: T, ...sources: U[]): /* return type TBD */{functionassign<T,U>(target: T, ...sources: ...U): /* return type TBD */{
For the second (converting a union to an intersection), it might be nice to use &&:
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:
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 Bryan Forbes (@bryanforbes) you are asking for intersection, so it would solve your issue.
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.
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.
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.
Blake Embrey (@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?
Jason Freeman (@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.
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.
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?
Activity
danquirk commentedon Jul 15, 2015
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.
JsonFreeman commentedon Jul 16, 2015
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 commentedon Jul 16, 2015
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 commentedon Jul 16, 2015
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:
bryanforbes commentedon Jul 16, 2015
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 & U1rather thanT & (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:
For the second (converting a union to an intersection), it might be nice to use
&&:JsonFreeman commentedon Jul 16, 2015
I think we want to declare up front whether U is a single type parameter or a family of type parameters. Allowing
Uin some places and...Uin others (in the parameter list and return type) will make it confusing. So I would prefer to declare...Uin 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 & ...Umeans that the elements of U are intersected. However, this does not allow you to write something equivalent toT & (U0 | U1)orT & [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 likeT & ...&UorT & ...|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:
T & ...UT | ...U[T, ...U]JsonFreeman commentedon Jul 16, 2015
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 Bryan Forbes (@bryanforbes) you are asking for intersection, so it would solve your issue.
Arnavion commentedon 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 commentedon Aug 4, 2015
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 commentedon Aug 4, 2015
You're saying that
...inputexpands 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 commentedon Aug 4, 2015
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.
Arnavion commentedon Aug 4, 2015
Oh, didn't know that.
blakeembrey commentedon Sep 7, 2015
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.
JsonFreeman commentedon Sep 8, 2015
Blake Embrey (@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 commentedon Sep 8, 2015
Jason Freeman (@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.
JsonFreeman commentedon Sep 8, 2015
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 commentedon Sep 25, 2015
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
...Twould be intersection. Also, in that case, what would it mean if you just referencedTinstead of...T?Arnavion commentedon Sep 30, 2015
Jon (@jbondc)
It's because mixA, mixB and mixC are all the same type
{}, sotypeof mixAis a valid specialization for T. Try adding unique members to them.sccolbert commentedon Oct 8, 2015
Related to the issue on variadic generics: #1773
mhegazy commentedon Feb 22, 2016
closing in favor of #1773