Why Are Some Languages 'Unable' to Compile to WebAssembly?
In my previous essay, “Dart: What You Lose on the Swings, You Gain on the Roundabouts,” I wrote the following line:
Dart may be able to support WebAssembly in the future (similar to Go; whether that is meaningful is another question), while TypeScript has no such possibility.
As a result, many TypeScript devotees were unhappy: why do you say our precious baby cannot support WebAssembly (hereafter WASM)? Someone dug up evidence: look, AssemblyScript[1] clearly supports converting TypeScript into WebAssembly, doesn’t it? I don’t know whether the people saying this have actually looked at AssemblyScript or are repeating hearsay, knowing only that such a thing exists, and—like Beijing taxi drivers—speaking in terms that sound impressive but are hard to understand. In fact, just take a look at AssemblyScript’s repo, hub clone assemblyscript/assemblyscript, run any of its examples, and you will discover:

Sorry, wrong scene:

Why? Why is it that clearly TypeScript code won’t compile with tsc? If you take a look at the documentation on its official site, assemblyscript.org, you should understand that even their basic data types are very different—for numeric types, TypeScript follows JavaScript’s number, while AssemblyScript uses WASM types such as i8, u16, and f64. So I replied:
AssemblyScript explicitly states that it is “Definitely not a TypeScript to WebAssembly compiler.” You cannot directly convert existing TypeScript into WASM. TypeScript follows JavaScript’s internal types (because it ultimately compiles to JavaScript), whereas WASM has types like u8, u16, and isize. AssemblyScript merely keeps its syntax largely consistent with TypeScript. When you write TypeScript, you need to be very careful to avoid using the entire TypeScript/JavaScript ecosystem. You also need to watch out for syntactic differences, such as
===and null checks.
Actually, AssemblyScript is just a new language wrapped in TypeScript syntax. So why can’t TypeScript itself be translated directly into WASM? Since we’ve opened this discussion, let’s briefly understand WASM and then ask a more general question: how do we determine whether a language can—or is worth—being converted to WASM?
What Is WASM? What Problem Does It Solve?
WASM’s official website, webassembly.org, defines it as follows[2]:
WebAssembly (abbreviated Wasm) is a binary instruction format for a stack-based virtual machine. Wasm is designed as a portable target for compilation of high-level languages like C/C++/Rust, enabling deployment on the web for client and server applications.
First, WASM is an instruction-set format; this instruction set can run on a specific stack-based VM. VMs are generally either register-based (Erlang) or stack-based (Java), and WASM chose the latter, largely to produce smaller program code. WASM’s predecessor was asm.js. At the time, Mozilla engineers tried compiling large C/C++ projects into an implicitly typed subset of JavaScript highly optimized for JIT compilers, and the result achieved efficiency in the browser close to native applications. Afterward, the four giants—Mozilla, Google, Microsoft, and Apple—sat down together to jointly define and develop WASM technology and support it in their respective browsers. In December 2019, WASM was officially accepted as a W3C Recommendation[3][4], becoming the fourth native executable language in the browser after HTML/JavaScript/CSS.
From the outset, WASM was designed as a language parallel to JavaScript. Its emergence is not meant to replace JavaScript; on the contrary, JavaScript also acts as the glue for executing WASM code. WASM’s strength lies in bringing existing code from outside the JavaScript ecosystem—especially C/C++—onto the web, greatly enriching the web ecosystem and making it possible to put many systems that would be difficult to rewrite in JavaScript onto the web, such as vim[5].
The earliest implementation of WASM was the Emscripten compiler, which uses LLVM to compile C/C++ code into WASM. In theory, any compiler that uses LLVM can use it to support WASM. Early Rust supported WASM through Emscripten, and later provided its own wasm32-unknown-unknown target for better WASM support. In addition to compiling C/C++ code into WASM, Emscripten also emulates a Unix runtime environment, which allows much C/C++ code to be compiled to WASM with only minor changes (mostly to build scripts). However, once you use multithreading (which WASM does not yet support), special device drivers, or blocking operations (such as block wait), the code needs considerable modification. In addition, for traditional desktop and command-line software, the parts that interact with users also need to be changed. The following figure shows the changes vim.wasm made to Vim’s interaction:

Its entire compilation process is as follows:

Although WASM has been in development for more than five years, the languages with truly complete and meaningful support for WASM today are still only C/C++/Rust. Support in other languages is either incomplete or unusable.
Take Go as an example. A basic hello world compiled to WASM is over 2 MB (probably mainly because of the Go runtime):

Although we can use all sorts of hacks to get the compiled result down to a bit over 1 MB, that result is unacceptable for the web. In addition, Go has not yet fully complied with the WASM 1.0 specification, so it doesn’t count as usable[7]. TinyGo’s support for WASM looks promising, but simply because TinyGo does not support reflection, it automatically filters out a large portion of the Go ecosystem’s dependencies, such as protobuf. Often a language’s ecosystem itself matters much more than its syntax. Syntax is not hard to replicate, but an ecosystem takes a long time to grow.
Fortunately, WASM is currently developing rapidly, with many features under discussion and implementation. For example: support for GC and support for multithreading. With these two powerful tools, especially GC support, many languages that depend on GC can produce smaller bytecode (Go weeps with joy), so WASM can be supported by more languages in the future.
Some people cite awesome-wasm-langs[8] to refute me, saying: it’s not only C/C++/Rust; dozens of languages now support WASM, such as Python. That list is indeed intimidating, but look closely: pyodide, for example—isn’t it clearly Python and its scientific-computing libraries compiled to WASM? What is actually happening is that the C code used to write the Python interpreter is compiled to WASM, and then Python code can be executed—it is not compiling Python code into WASM. Of course, if you insist on being pedantic and say that this is Python supporting WASM, I have nothing to say. Unzipped, pyodide is 300 MB, and its main WASM body is 13 MB. This is obviously not intended for normal web use cases.
I have no intention of disparaging pyodide; it is a great tool for running a data scientist’s toolkit online, just like Unreal Engine running in the browser—it has strong appeal for a specific audience (which is why they don’t mind load times). But it does not mean that if you write a hello world, you can use pyodide to get a standalone loadable WASM; those are two different things. If this is what it means for a language to support WASM, then fine—I concede; every language can “support” WASM immediately.
Why Is TypeScript Unlikely to Support Compilation to WASM?
Let’s return to the discussion at the beginning of this article. I like TypeScript very much; it is an exquisitely designed language. But I think it is unlikely that TypeScript will support compilation to WASM. There are two reasons:
First, there is a gulf between their type systems (as mentioned earlier). If you insist on compiling TypeScript to WASM, you need a language like AssemblyScript. However, that would likely mean the entire TypeScript and JavaScript ecosystem becomes unavailable. TypeScript’s greatest and most successful advantage is that it introduces a type system into a project incrementally while remaining compatible with the whole ecosystem. If that advantage does not exist, then what is the point of using it?
Second, why do we want to compile TypeScript to WASM? What benefits would it bring?
Better performance? If a project’s performance requirements are truly extreme—so extreme that TypeScript cannot satisfy them, such as building a game engine—then why not switch to a more efficient language, such as Rust, write it in that, and compile it to WASM?
Smaller code size? Sorry, but WASM really has to cry out: “I cannot do it, Your Majesty!”
More efficient interoperability with the DOM and browser events? Sorry, but to manipulate the DOM, WASM has to go through JavaScript… And look again at how vim.wasm does it in the figure above.
…
If we cannot answer this question, then what is the point of spending so much effort to support compiling TypeScript to WASM? Just to keep up with fashion and hop on the new-technology train?
Let’s look at Chrome’s support for JavaScript and WASM:

As you can see, the relationship between the two is not one of replacement but of coexistence. Behind them is the same execution engine, TurboFan. JavaScript code spends considerable time in parsing and JIT, but once the code has been optimized at runtime, its execution efficiency is not much different from WASM. And for a flexible language like JavaScript, runtime optimization can do more than optimization at AOT time. If you force it to be fully compiled at compile time, it may instead hurt runtime efficiency. So I can’t really imagine a web use case for compiling TypeScript to WASM.
Of course, there is another use case for WASM that cannot be ignored: the server side. Its security, inherited from the web, makes it an excellent platform to replace sandboxes like Docker on the backend. If WASI-related applications flourish on the backend, then for a TypeScript engineer, being able to write code that runs on server-side WASM runtimes such as wasmer / wasmtime might still have some appeal. This may be the only reason to compile TypeScript to WASM.
References
- AssemblyScript: https://docs.assemblyscript.org/
- WebAssembly Official Documentation: https://webassembly.org
- WebAssembly Wikipedia: https://en.wikipedia.org/wiki/WebAssembly
- WebAssembly Core Specification: https://www.w3.org/TR/wasm-core-1/
- Vim WASM: https://rhysd.github.io/vim.wasm/
- Emscripten: https://emscripten.org/
- Golang: Are we WebAssembly Yet?: https://medium.com/@KevinHoffman/golang-are-we-webassembly-yet-e0a2e180fc98
- Awesome WASM langs: https://github.com/appcypher/awesome-wasm-langs
- WebAssembly for Web Developers: https://www.youtube.com/watch?v=njt-Qzw0mVY
- AssemblyScript is not a subset of TypeScript: https://dev.to/jtenner/AssemblyScript-is-not-a-subset-of-TypeScript-4ka3