ZK has been a server-centric solution for more than a decade. In recent years, we noticed the need for cloud-native support and have made this the main goal of our upcoming new version, ZK 10. The new feature will alleviate servers’ burden by transferring much of the model-view-model bindings to the client side so that the server side becomes as stateless as possible. This brings benefits such as reduced server memory consumption, simplified load balancing for ZK 10 clustered backends, and potentially easier integration with other frontend frameworks.
What did Java get right to enable us an 8x boost in productivity? We conclude that the availability of static analysis is the primary factor.
We design and write programs long before programs are executed and often before compilation. Normally, we refactor, implement new features, and fix bugs by modifying source code instead of modifying the compiler-generated machine code or the memory of the live program. That is, programmers analyze programs statically (before execution) as opposed to dynamically (during execution).
Not only is static analysis more natural to humans, but static analysis is also easier to automate. Nowadays, compilers not only generate machine code from source code but also perform the sort of analysis that humans would do on source code like name resolution, initialization guards, dead-code analysis, etc.
undefined instead of
Compare this with Java, where we have the compiler to aid our reasoning “as we type.” With TypeScript, the compiler will perform “automatic semicolon insertion” analysis followed by dead code analysis, yielding:
Humans can never beat the meticulousness of machines. By delegating this sort of monotonous but critical tasks to machines, we can free up a huge amount of time while achieving unprecedented reliability.
We evaluated the following 6 options and settled on TypeScript due to its extensive ECMA standard conformance, complete support for all mainstream JS module systems, and massive ecosystem. We provide a comparison of them at the end of the article. Here is a short synopsis.
- Google’s Closure Compiler: All types are specified in JSDoc, thereby bloating code and making inline type assertion very clumsy
- Facebook’s Flow: A much smaller ecosystem in terms of tooling and libraries compared to TypeScript
- Microsoft’s TypeScript: The most mature and complete solution
- ReScript: Requires a paradigm shift to purely functional programming; otherwise, very promising
Semi-Automated Migration to TypeScript
zk.$extends function, as shown on the left-hand side. We intend to transform it to the semantically equivalent TypeScript snippet on the right-hand side.
There are hundreds of such cases among which many have close to 50 properties. If we were to rewrite manually, it would not only take a very long time but be riddled with typos. Upon closer inspection, the transformation rules are quite straightforward. It should be subject to automation! Then, the process would be fast and reliable.
Fortunately, there is jscodeshift that does the parsing and consolidation of source code and provides a set of useful APIs for AST modification. Furthermore, there is AST Explorer that acts as a real-time IDE for jscodeshift so we can develop our jscodeshift transformation script productively. Better yet, we can author a custom typescript-eslint rule that spawns the jscodeshift script upon the presence of
zk.$extends. Then, we can automatically apply the transformation to the whole codebase with the command
Let’s turn to the type
T in the example above. Since jscodeshift presents us with the lossless AST (including comments), we can author a visitor that extracts the
@return JSDoc of
getter() if it can be found; if not, we can let the visitor walk into the method body of
getter() and try to deduce the type
T, e.g., deduce
T to be
string if the return value of
getter() is the concatenation of
this._field2 with some string. If still no avail, specify
void, so that after jscodeshift is applied, the TypeScript compiler will warn us about a type mismatch. This way we can perform as much automated inference as possible before manual intervention and the sections required for manual inspection will be accurately surfaced by the compiler due to our fault injection.
Besides whole file transformations like jscodeshift that can only run in batch mode, the typescript-eslint project allows us to author small and precise rules that update source code in an IDE, like VSCode, in real-time. For instance, we can author a rule that marks properties of classes or namespaces that begin or end with single underscores as
@internal, so that documentation extraction tools and type definition bundlers can ignore them:
Regarding the example above, one would have to determine the existence of property-associating JSDoc, the pre-existence of the
@internal tag, and the position to insert the
@internal tag if missing. Since typescript-eslint also presents us with a lossless AST, it is easy to find the associating JSDoc of class or namespace properties. The only non-trivial task left is to parse, transform, and consolidate JSDoc fragments. Fortunately, this can be achieved with the TSDoc parser. Similar to activating jscodeshift via typescript-eslint in the first example, this second example is a case of delegating JSDoc transformation to the TSDoc parser upon a typescript-eslint rule match.
eslint --fix command. The importance of static analysis cannot be emphasized enough!
Bravo! Zk 10 Has Completely Migrated to TypeScript
eslint --fix), thanks to the typescript-eslint project that enables lots of extra type-aware rules, we also wrote our own rules, and we are guaranteed to never make those mistakes ever again in the future. This means less mental burden and a better conscience for the ZK development team.
Our Client MVVM effort also becomes much more manageable with TypeScript in place. The development experience is close to that of Java. In fact, some aspects are even better, as TypeScript has better type narrowing, structural typing, refinement types via literal types, and intersection/union types.
As for our users, ZK 10 has become more reliable. Furthermore, our type definitions are freely available, so that ZK 10 users can customize the ZK frontend components with ease and confidence. In addition, users can scale their applications during execution with Client MVVM. Adopting TypeScript in ZK 10 further enables us to scale correctness during development. Both are fundamental improvements.
Google’s Closure Compiler
- Type system soundness unknown; Assumed as unsound, as sound type systems are rare
@interfacedenotes nominal types whereas
@recorddenotes structural types
- All type annotations are specified in comments leading to code bloat, and comments often go out of sync with the code.
- Most advanced and aggressive code optimization among all options listed here
- Find more information on GitHub
- Unsound type system
- Nominal types for ES6 classes and structural types for everything else, unlike TypeScript where all types are structural; whereas in Java, all types are nominal
- Compared to TypeScript, Flow has a much smaller ecosystem in terms of tooling (compatible formatter, linter, IDE plugin) and libraries (TypeScript even has the DefinitelyTyped project to host type definitions on NPM)
- Find more information in Flow Documentation
- Unsound type system
- All types are structural, which is the most natural way to model dynamic types statically, but the ability to mark certain types as nominal would be good to have. Flow and the Closure Compiler have an edge in this respect.
- Also supports Closure-Compiler-style type annotations in comments
- Best-in-class tooling and a massive ecosystem; built-in support by VSCode; hence, its availability is almost ubiquitous
- Each enum variant is a separate subtype, unlike all other type systems we have ever encountered, including Rust, Scala 3, Lean 4, and Coq
- Find more information in The TypeScript Handbook
- Leverages the awesome type system of Scala 3, which is sound
- Seamlessly shares build scripts (sbt) and code with any Scala 3 project
- Learn more on the Scala.js site
- Interoperation with TypeScript via genType
- As of ReScript 10.1,
- Might require familiarity with more advanced functional programming techniques and purely functional data structures
- Learn more in the ReScript Language Manual documentation
#Automating #Migration #Framework #DZone