-
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
ECMAScript spread param rules causing loss of type-safety (and slower compile time?) #7347
Comments
Since this relates to compile-times relevant to Angular 2, which uses and/or is commonly paired with RxJS 5, also cc/ @IgorMinar @mhevery |
Is the request here simply to allow rest args in non-final positions? |
@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. |
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: 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 |
I am not sure i see how you can figure something like i see allowing named arguments to come at the end, and the generated code would do |
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
|
Also how would we handle constraints on variadic types? I forgot to model it in my last comment, but something like...
|
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. |
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: foo(a, ...b, c) { }; foo(1, 2);
bar(...a, b, c) { }; bar(1); |
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. |
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 |
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. |
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. function concat(...observables: Array<Observable>, scheduler: Scheduler);
function concat(...args: Array<Observable | Scheduler>) {
console.log(args.length);
// 3
}
concat(new Observable, new Observable, new Scheduler); function concat(...observables: Array<Observable>, scheduler: Scheduler) {
// do stuff
}
// error: A reset parameter must be last in a parameter list, except in overload signatures. 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. |
@jayphelps the contention there will be roughly the same as the contention around function foo(...a: TypeA[], b: TypeB, c: TypeC) { }
foo(1); // what's what?
// or worse
function bar(...a: TypeA[], b?: TypeB, c?: TypeC) { }
bar(2); // what now? What I propose is the above call to function foo(...args: Array<TypeA|TypeB|TypeC>) {
const a = args.slice(0, args.length - 3);
const b = args[args.length - 2];
const c = args[args.length - 1];
}
// or even
function foo(...args: Array<TypeA|TypeB|TypeC>) {
const c = args.pop();
const b = args.pop();
const a = args;
}
// bar gets weirder
function bar(...args: Array<TypeA|TypeB|TypeC>) {
let c: TypeC;
let b: TypeB;
if (args[args.length - 1] instanceof TypeC) {
c = <TypeC>args.pop();
}
if (args[args.length - 1] instanceof TypeB) {
b = <TypeB>args.pop();
}
const a: TypeA[] = <TypeA[]>args;
// do stuff
} Things to notice:
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. |
Okay, I misunderstood what @jayphelps was saying... but there is still one problem with his proposal, which is that the implementation signature could still be matched, which wouldn't keep the function type-safe. eg, this: function concat(...observables: Array<Observable>, scheduler: Scheduler);
function concat(...args: Array<Observable | Scheduler>) {
console.log(args.length);
// 3
}
concat(new Observable, new Observable, new Scheduler); Also matches this: concat(new Scheduler, new Scheduler, new Observable); (discussed this with Jay in person a minute ago) |
The implementation signature isn't visible to callers (for exactly this reason!) -- from the "outside", you only see the |
@RyanCavanaugh seeing that too. Emulating this behavior, I see it suggest only that signature (a pseudo variant of it, that is cause obv this syntax isn't supported). But it still let me provide the arguments in the wrong order, as ben noted above. |
So if this was added as a valid overload signature, it would make things "type-safe" for people consuming compiled .js files and their .d.ts counterparts, I guess. Because TypeScript seems to allow matching the implementation signature if you're using the .ts file directly. ... I guess you'd need a way to say "this signature is for implementation only and we don't want you using it like that". Which seems odd. |
This isn't true. function f(x: number): void;
function f(x: string): void;
function f(x: any): void { }
// Errors; this cannot see the x: any overload
f({q: 2}); |
Ah, thanks for the clarification, @mhegazy |
From the OP it seems that this proposal is not sufficient, and that variadic types are also required. this is based on the current list of signatures above you have opted to use generic type parameters for every input and accumulated in the output |
@mhegazy Yep, you're right, with one clarification. Ben will have to confirm but my discussions with him led me to believe that most operators (including function concat<R>(...observables: Array<ObservableInput<any>>, scheduler?: Scheduler): Observable<R>;
function concat<R>(...args: Array<ObservableInput<any> | Scheduler>): Observable<R> {
console.log(args.length);
// 3
} So while RxJS will need variadic generics to make all operators non-final rest params in overloads gets us ~80% closer. Anyone feel free to correct us if you believe otherwise. You guys know the compiler limits far better than we do of course! |
The existence of the uninferrable |
@RyanCavanaugh I think so users can type the returned Observable explicitly if they so choose? I'm unsure of the original intent there, if I'm honest. |
@mhegazy, you're exactly right. We' have issues with the const obsA = new Observable<A>();
const obsB = new Observable<B>();
const obsC = new Observable<C>();
obsA.withLatestFrom(obsB, obsC, (a: A, b: B, c: C) => new ReturnType(a, b, c)); // Observable<ReturnType> expected So you'd almost need a type signature like this (probably poorly designed) psuedocode: withLatestFrom<...T, R>(...observables: ...Observable<T>, (...args: ...T) => R): Observable<R> |
... however, @mhegazy, for |
Further, talking with @jayphelps, we don't really need the return type on const result: Observable<number> = obs1.concat<number>(obs2); is the same as const result: Observable<number> = obs1.concat(obs2); ... if concat returned cc/ @david-driscoll @kwonoj. |
the generic types on
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 |
Now to note, for concat/merge we could drop the requirement of variadics and just assume we're getting a named type |
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). |
@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. |
It seems like Angular 1 had this pattern everywhere for their DI, or at least something very similar. |
Either way, I'm pleased that you at least considered our needs. 👍 |
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: Tapable.prototype.applyPluginsAsyncSeries = Tapable.prototype.applyPluginsAsync = function applyPluginsAsync(name) {
var args = Array.prototype.slice.call(arguments, 1);
var callback = args.pop();
if(!this._plugins[name] || this._plugins[name].length === 0) return callback();
var plugins = this._plugins[name];
var i = 0;
var _this = this;
args.push(copyProperties(callback, function next(err) {
if(err) return callback(err);
i++;
if(i >= plugins.length) {
return callback();
}
plugins[i].apply(_this, args);
}));
plugins[0].apply(this, args);
}; And the documentation for that function specifying the following: applyPluginsAsyncSeriesapplyPluginsAsyncSeries(
name: string,
args: any...,
callback: (err: Error, result: any) -> void
)
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. |
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:
But since that's illegal, we're forced to do this:
... 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:
I believe this is directly related to compilation speed issues that @mhegazy wanted to discuss with me.
cc/ @kwonoj @david-driscoll @jayphelps
The text was updated successfully, but these errors were encountered: