Repository navigation
Design Meeting Notes, 6/7/2024 #58804
Description
Control Flow over Callback Arguments, and the Deferred Marker Type
-
Want to be able to track callbacks as possible branches of control flow.
-
Unfortunately, there are plenty of callbacks that are queued and ran in a deferred manner.
-
Need a way to signal that any possibly invocation of a function will not happen immediately.
-
The direction we took was to wrap the type of the callback in a
Deferred<...>helper type. -
This is more work, so how are we avoiding it?
- Some optimizations around collapsing un-effect-ful parts of the control flow graph that we introduced in TS 5.5 - that helps a bit.
- But there is still a lot of work now resolving functons that are called to figure out if any parameter is marked with a
Deferred.-
This work would have been done regardless of if a variable is actually ever referenced in the lambda.
doStuff(() => { x = 123; }); y;
- In the above example, our callback is known to be effectful - so we would require a walk of it to determine the type of
yeven thoughyisn't ever referenced.
- In the above example, our callback is known to be effectful - so we would require a walk of it to determine the type of
-
So instead of resolving the type of the called function immediately, we do a walk of the callback's CFG to look for assignments to
ybefore we decide to resolve. -
But if you have an effectful function, aren't you going to have to resolve the call anyway?
-
Well one thing is that less unnecessary up-front work triggers fewer circularities.
-
But also, that's not necessarily the case. Local effects might have nothing to do with outer variables!
let x = 0; doStuff(() => { let y = 123; y = 456; return y; }); x;
- In the above example,
yis mutated, butxitself is never referenced.
- In the above example,
-
-
-
What happens when you have several overloads - one which is deferred?
- We don't do full overload resolution - we just look for if any signature has a
Deferredtype at that position.
- We don't do full overload resolution - we just look for if any signature has a
-
It's strange that this is an intrinsic type and not something tied to the parameter/binding.
- Can you use it in places other than parameters?
- Wait, can you not write something like
f(options: { f: Deferred<() => void> })?- You can write it, but it will not work.
-
If
Deferredis a type, you can write{ [K in keyof T]: Deferred<T[K]> }for a deferred tuple.- And then spread that in?
- Well... depends on when you want to resolve. And currently we don't.
- Speaks to this being more of a parameter modifier - because you don't want to give people the impression that it can be used everywhere.
-
We could prohibit uses of
Deferredin certain places.- Only let it get used in parameters for now and expand down the road.
-
Nice that
Deferred<...>is no new syntax, similar toNoInfer. -
Going back to the beginning - this analysis is not just for assignments, it's also for array mutations.
let a = []; a.push("abc"); invoke(() => { a.push(123); }); a; // Array<number | string>
-
Does any of this track whether or not the function is truly deferred or not?
-
No, and it's not a simple question to really answer.
-
Complications around
awaiting, aliasing, etc. -
Though it raises some questions about
Deferredas well.let fns: Array<() => void> = []; function enqueue(f: Deferred<() => void>) { // Is it okay to push a Deferred function in here? fns.push(f); } function runThem() { for (const f of fns) { f(); } } let y: number | string = 0; enqueue(() => { y = "abc"; }) y; // We correctly see that the narrowed type of 'y' is 'number' because we used 'Deferred'. let x: number | string = 0; enqueue(() => { x = "abc"; }) runThem(); x; // Because we wrote 'Deferred', we will *incorrectly* say that the narrowed type is 'number'.
-
Kind of depends on how strictly you view
Deferredas "next turn of the event loop" or "not immediately".
-
Activity
fatcerberus commentedon Jun 7, 2024
Shouldn’t it be the other way around - a
NonDeferredhelper type? If you treat “non-deferred” as the default then you end up causing the same breaks you were trying to avoid (code that assumes a callback is deferred and has its narrowings cancelled prematurely)plus I’d think that functions calling a callback immediately are probably in the minority relative to ones where it’s deferred/called asynchronously.
fatcerberus commentedon Jun 7, 2024
…unless someone just got finished reading a monad tutorial and went mad with power after it finally clicked for them 🚎
DanielRosenwasser commentedon Jun 7, 2024
The idea is that this would be behind a
--strictflag which takes a more conservative view of narrowing - and declaration sites would be able to opt out of that.fatcerberus commentedon Jun 7, 2024
Hmm… it seems like it’s probably pretty idiomatic today to do stuff like
As a matter of principle I’d feel uncomfortable passing
fooaround before all its callbacks were registered, and it wouldn’t be feasible for me to turn this proposed flag on until the library writer updated their types to align, which might make it too breaky even forstrictMy point stands about the potential for code bloat with
Deferredvs.NonDeferredtooJosh-Cena commentedon Jun 11, 2024
I would postulate that most callbacks are array method callbacks, FWIW.
DanielRosenwasser commentedon Jun 11, 2024
This is a bit of my concern too - that people would try this out, find it's too painful, and forget to try to turn it on again in the future.