Closed
Description
I think we're hitting a similar situation to #1336 with RxJS 5, which is causing some crazy workarounds for some minimal type-safety, which in turn is causing slower build times.
Ideally what we'd have is this:
// Observable.prototype.concat
concat<R>(...observables: ObservableInput<any>[], scheduler?: Scheduler): Observable<R>
But since that's illegal, we're forced to do this:
concat<R>(...args: Array<ObservableInput<any> | Scheduler>): Observable<R>
... that clearly doesn't give us what we want for type safety, so we end up defining a crazy list of common type signatures like:
/* tslint:disable:max-line-length */
export interface ConcatSignature<T> {
(scheduler?: Scheduler): Observable<T>;
<T2>(v2: ObservableInput<T2>, scheduler?: Scheduler): Observable<T | T2>;
<T2, T3>(v2: ObservableInput<T2>, v3: ObservableInput<T3>, scheduler?: Scheduler): Observable<T | T2 | T3>;
<T2, T3, T4>(v2: ObservableInput<T2>, v3: ObservableInput<T3>, v4: ObservableInput<T4>, scheduler?: Scheduler): Observable<T | T2 | T3 | T4>;
<T2, T3, T4, T5>(v2: ObservableInput<T2>, v3: ObservableInput<T3>, v4: ObservableInput<T4>, v5: ObservableInput<T5>, scheduler?: Scheduler): Observable<T | T2 | T3 | T4 | T5>;
<T2, T3, T4, T5, T6>(v2: ObservableInput<T2>, v3: ObservableInput<T3>, v4: ObservableInput<T4>, v5: ObservableInput<T5>, v6: ObservableInput<T6>, scheduler?: Scheduler): Observable<T | T2 | T3 | T4 | T5 | T6>;
(...observables: Array<ObservableInput<T> | Scheduler>): Observable<T>;
<R>(...observables: Array<ObservableInput<any> | Scheduler>): Observable<R>;
}
/* tslint:enable:max-line-length */
I believe this is directly related to compilation speed issues that @mhegazy wanted to discuss with me.
Metadata
Metadata
Assignees
Labels
Type
Projects
Milestone
Relationships
Development
No branches or pull requests
Activity
benlesh commentedon Mar 2, 2016
Since this relates to compile-times relevant to Angular 2, which uses and/or is commonly paired with RxJS 5, also cc/ @IgorMinar @mhevery
RyanCavanaugh commentedon Mar 2, 2016
Is the request here simply to allow rest args in non-final positions?
benlesh commentedon Mar 2, 2016
@RyanCavanaugh, I think so yes.
But there would need to be rules around it. For example, you couldn't have optional params before rest params, but you could have them after.
benlesh commentedon Mar 2, 2016
Now in ECMAScript, I think they could loosen the rules, but not as much as you could in a typed language. For example, without types:
foo(...bar, baz, ...qux)
would be impossible to suss out. But with types, it's actually doable at compile time:foo(...bar:number[], baz:string, ...qux:number[])
.We don't have to go all-in with this change, but I would be happy with just allowing them in places that are doable in ECMAScript
mhegazy commentedon Mar 2, 2016
I am not sure i see how you can figure something like
foo(...bar:number[], baz:string, ...qux:number[]).
?i see allowing named arguments to come at the end, and the generated code would do
arguments[arguments.length-position]
but do not see how you can do the multiple rest args case.david-driscoll commentedon Mar 2, 2016
In theory you could do multiple rest arguments, but you would have to code gen some metadata into it (which hurts my head when thinking about JS <-> TS).
Keep in mind this particular problem isn't new to RxJS5, this also applies to RxJS4. I can't think of any other big names where I've seen this pattern though. In the Rx world it makes sense, and once you're used to Rx concepts, it's honestly fairly intuitive.
Variadic Types may very well fix this problem.
combineLatest is a pretty popular (at least for me 😄)
Possible Variadic version
ie
Merge is another fun one...
Possible varidaic version
T
but this isn't always the case, so the result is a union of allT
sdavid-driscoll commentedon Mar 2, 2016
Also how would we handle constraints on variadic types?
I forgot to model it in my last comment, but something like...
RyanCavanaugh commentedon Mar 2, 2016
There's already #1773 tracking variadic type parameters so let's not complicate the discussion here with that.
I agree that more than one rest parameter should not be allowed in the event that we do support this.
To be clear, this is already the case. The ES spec (https://tc39.github.io/ecma262/#sec-function-definitions) only allows rest args in the last parameter position. As far as I know, there isn't even a Stage 0 proposal for supporting them elsewhere.
benlesh commentedon Mar 2, 2016
Here's a pertinent discussion on ESDiscuss: https://esdiscuss.org/topic/rest-parameters
I think a simple set of rules can enable up front rest params in ECMAScript.
In TypeScript, the last rule isn't as necessary, because of type checking I think.
And I think TypeScript, at least, could throw compilation errors if a function is called with a shorter than necessary number of arguments, and the ECMAScript implementation itself could throw a run time error in that case... for example:
These should be errors, IMO:
RyanCavanaugh commentedon Mar 2, 2016
The thread there generally points to this not ever happening in ECMAScript. I don't see why any of their arguments are less correct in TypeScript.
benlesh commentedon Mar 3, 2016
The "arguments" are mostly just "what ifs" without answers.
If a solid set of rules was made around it, I don't see why it couldn't be implemented. If TypeScript supported plugins I'd add it myself. The real trick, though, is the type-safety. That's an actual problem.
@jayphelps actuall pointed out that Swift actually supports this feature, but that seems to be enabled by the fact that swift has named arguments after the first argument like
concat(a, b, c, scheduler: myScheduler)
would matchconcat(observables: Observable..., scheduler: Scheduler)
benlesh commentedon Mar 3, 2016
rest feature or no feature, I need a way to reliably and efficiently support type-safety with this method signature. That's the real issue at hand.
jayphelps commentedon Mar 4, 2016
One solution that wouldn't impact ECMAScript spec is to only allow first param spread in abstract/overload signatures but not in the actual implementation/concrete signature.
That means that it's a purely compile type-related feature, never a runtime one. This is obviously a bit of a quirk, but would solve the ECMA argument AFAIK.
benlesh commentedon Mar 4, 2016
@jayphelps the contention there will be roughly the same as the contention around
foo(a, ...b, c)
though:What I propose is the above call to
foo(1)
should be compilation errors in TypeScript, it should probably be a runtime error as well. But the call tobar(2)
should probably be fine. If you look at how someone would have implement the above methods it'll be more apparent:Things to notice:
foo(...a, b, c)
andbar(...a, b?, c?)
.foo
will throw if you pass an invalid number of arguments. We could guard against that, sure, but most likely developers wouldn't do that.bar
impl is mostly fine and will make "smarter" decisions about how to handle calls to it likebar(new TypeA())
,bar(new TypeC())
andbar(new TypeB())
... but won't really know what to do about out of order calls likebar(new TypeC(), new TypeA())
.What I'd like is to support the type signatures above with rest params at the front or even in the middle and generate accompanying code that adheres to some rules and enforces those rules. Front will do for now.
15 remaining items
david-driscoll commentedon Mar 8, 2016
the generic types on
concat
,merge
are useful because they let us resolve something like...to something like....
The less manual inference I as the consumer has to do, the easier my life is. In addition if you make a small incompatible change at the top of a set of operations, it cascades gracefully down letting you see how badly you failed.
I'm fine with ignoring variadics for the moment to deal with this specific scenario (rest args), but variadics will be useful going forward beyond just
combineLatest
andzip
.david-driscoll commentedon Mar 8, 2016
Now to note, for concat/merge we could drop the requirement of variadics and just assume we're getting a named type
T
, and then returningObservable<T>
. That covers most situations, but then some operations, where you want to have a stream ofApple
s orOrange
s would have to be explicitly typed.RyanCavanaugh commentedon Mar 15, 2016
This pattern seems to be relatively rare and would be quite complex to handle during overload resolution, which makes tons of assumptions about parameter ordering. If we see other libraries putting things at the end we can reconsider (i.e. if you encounter other functions with this pattern, please leave a comment with a link).
jayphelps commentedon Mar 15, 2016
@RyanCavanaugh understandable.
It's definitely not a super common problem. I imagine it's usually APIs dealing with a callback, since they're typically the last arg but even then, not something I see often.
benlesh commentedon Mar 21, 2016
It seems like Angular 1 had this pattern everywhere for their DI, or at least something very similar.
benlesh commentedon Mar 21, 2016
Either way, I'm pleased that you at least considered our needs. 👍
TheLarkInn commentedon Sep 21, 2016
Since this is relevant to my current endeavors I'm going to comment here in the case it gets revisited.
In attempting to convert one of our webpack core libraries, I too, came across the same issue when trying to implement the following: https://github.com/webpack/tapable#applypluginsasyncseries
This is the current JavaScript implementation:
And the documentation for that function specifying the following:
applyPluginsAsyncSeries
I will take the same approach that @Blesh et al are taking for RxJS. Only problem is that the number of arguments can reach up to 50+ (because in webpack core, almost every bit of functionality uses this plugin system).
Thanks for tracking this!
UPDATE:
I mispoke on this, the arguments list can be pretty large still but plugins registration happens at a different point etc.