Repository: TypeScript
Issue: Design Meeting Notes, 6/7/2024
Labels: Design Notes

# Control Flow over Callback Arguments, and the `Deferred` Marker Type

https://github.com/microsoft/TypeScript/pull/58729

* 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.

          ```ts
          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 `y` even though `y` isn't ever referenced.

        * 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 `y` before 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!

              ```ts
              let x = 0;
              doStuff(() => {
                  let y = 123;
                  y = 456;
                  return y;
              });

              x;
              ```
                * In the above example, `y` is mutated, but `x` itself is never referenced.
* 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 `Deferred` type at that position.
* 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 `Deferred` is 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 `Deferred` in certain places.
    * Only let it get used in parameters for now and expand down the road.
* Nice that `Deferred<...>` is no new syntax, similar to `NoInfer`.
* Going back to the beginning - this analysis is not just for assignments, it's also for array mutations.

  ```ts
  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 `await`ing, aliasing, etc.
    * Though it raises some questions about `Deferred` as well.

      ```ts
      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 `Deferred` as "next turn of the event loop" or "not immediately".