When we do, we replace it with a number, and we keep going until all nodes have been replaced. Applying this technique to our other transformations allows us to fix the first two problems, so iterators are now typed correctly, and we can use global functions and constants.
For overloads, we need something a little more complicated. What we want in our TypeScript types is something like this, where we have a correspondence between input and output types. For example, when we specify text, we expect a string. When we specify a ray buffer, we expect an array buffer, and so on. The problem is that in the C++ code for KV namespaces, there's no correspondence between input type and output result. How do we know what to generate? We need some additional human input here.
There are other places where we need human input too, to improve the developer ergonomics. We need this function for function overloads. We also need additional input to add type parameters. Sometimes you want to rename types to be less verbose, sometimes you want to replace the auto-generated definition with something different, and sometimes you want to hide the type because it shouldn't be exposed to end users. We solve this by allowing partial TypeScript code to be inserted alongside C++ code. This is encoded with the runtime type information in cap and proto, and we merge this with the generated definition. We can use this for all the cases we've highlighted, and because we're co-locating overrides with the C++ code, it's much easier for runtime developers to add these in and keep them up to date.
With that, we've solved all the problems from earlier, and we have a solid set of type definitions for workers. But there's one more thing we can do to make them even better. Sometimes people make mistakes. Developers are no different and sometimes we introduce bugs into our code. Sometimes fixing those bugs would introduce breaking behaviour changes, but we don't want to break existing deployed code. So when you upload your worker code, you have to specify a compatibility date.
We put breaking changes behind a compatibility flag which have a date when they're enabled by default. If your worker's compatibility date is after a flag's default on date, then that flag will be enabled. The problem is that some flags change the public API surface and therefore the TypeScript types. For instance, the global navigator flag adds a new navigator constant to the global scope. There are currently 41 compatibility flags, although not all of them change the type surface, but if we generate a version for the types for all possible combinations of flags, we end up with 2 trillion type definitions files which is kind of infeasible.
Instead our solution at the moment is to generate an entry point of the types for each compatibility date that changes the public API surface. This doesn't completely solve the problem as users can still selectively enable and disable flags. What we're planning to do is build Types as a Service which will be a Cloudflow worker that dynamically generates NPM packages containing TypeScript definition files based on the selected compatibility date and flags. So I'd encourage you to try out Cloudflow workers if you haven't already. You can find all of the TypeJet Reclamation scripts in the Cloudflow WorkerD GitHub repo and find me on GitHub and Twitter. Thank you very much for listening.
Comments