-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Comments
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 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:
|
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 ( 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 { |
I think we want to declare up front whether U is a single type parameter or a family of type parameters. Allowing 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 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 @bryanforbes you are asking for intersection, so it would solve your issue. |
Since tuples extend a union of their constituent types for indices higher than the explicitly specificed member types, won't they still work? |
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. |
function manyInTupleOut<...T>(...params: Array<...T>): [...T];
var input: [number, string, Document];
var output = manyInTupleOut(...input); You're saying that I'm saying that input's type is known to be It doesn't seem necessary to know the number of elements in input. |
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 |
Oh, didn't know that. |
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 |
@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? |
@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 |
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 |
It's because mixA, mixB and mixC are all the same type |
Related to the issue on variadic generics: #1773 |
closing in favor of #1773 |
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 fine, however with intersection types in 1.6 this could be better:
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 toassign()
and the return value would expand to something likeT & (U0 & ... UN)
. With the push for composable types and ES6 standardizing the above behavior (inObject.assign
) a syntax for generics like this would be very useful.The text was updated successfully, but these errors were encountered: