Compilers are cool, they take code you write and make code the machine can understand. Transpilers are cooler, they take code you write and make code in another programming language that machines can't understand (yet). But why?
Let's take a quick tour around the world of transpilers!
JavaScript Clones
JavaScript is one of the top use cases for transpilers. JavaScript can run natively in any major browser. So, instead of making a new language and asking browsers to support that natively in the browser, many want to reuse what's already there.
The problem is, JavaScript is crazy. There are many more examples of that. Even with perfect knowledge of JavaScript internals, bugs will happen because of just how dynamic the language is. So, to get around that, we have...
TypeScript
TypeScript is a superset of JavaScript that allows explicit type information. TypeScript solves many of JavaScript's biggest woes with dynamic typing. I will not provide examples so I don't go insane.
So, how does TypeScript do it? It's a transpiler! You can just transpile your TypeScript to JavaScript then run that in the web browser.
So take this example in TypeScript, ruthlessly stolen from the handbook on everyday types:
// test.ts
// Parameter type annotation
function greet(name: string) {
console.log("Hello, " + name.toUpperCase() + "!!");
}
So we can see our name
parameter has the type string
. We can then compile this with tsc test.ts
to get...
// test.js
// Parameter type annotation
function greet(name) {
console.log("Hello, " + name.toUpperCase() + "!!");
}
... The same thing without the type in name
! Okay that's not interesting, but try adding a call like greet(1);
and you'll see an error:
test.ts:6:7 - error TS2345: Argument of type '1' is not assignable to parameter of type 'string'.
6 greet(1);
~
Found 1 error.
The type guarantee lets you call toUpperCase()
and declines anything that's not a string
type. You may be able to see the bugs that could be caught from this. That's why TypeScript has made such a splash.
CoffeeScript
CoffeeScript tries to solve a slightly different problem. Instead of providing a superset that makes your code less prone to bugs, CoffeeScript makes JavaScript prettier.
However, I've always considered CoffeeScript its own language. Once learned, it's a pleasure to write, which is a nice break from JavaScript, in my opinion. But, it seems to be in some middle ground between JavaScript and a standalone language.
In fact, as stated on the CoffeeScript site, NodeJS can natively run CoffeeScript. It doesn't need to transpile down to JavaScript; it is its own language. What if we went more pure, with no intent to replace the syntax of some other language?
Nim
Nim can transpile to C, C++, or JavaScript. That would make Nim our lone example of a language whose purpose is to transpile to multiple other languages. For systems programming, use its C support with dependency-free executables. For frontend development, target JavaScript.
Let's try an example:
// test.nim
echo "Hello World!"
If we compile this to C with nim cc --nimcache:nimcache test.nim
, we get.. an executable. So did it compile? Or transpile?
Well, if we check the nimcache directory I specifically included, we'll find a fair few C files. These C files are what get compiled to create the executable we see. I won't include these, since they're a fair bit of code.
But, we see a pretty major difference from the JavaScript-like transpilers we looked at: Nim only uses the transpilation as a means to an end.
C2Rust
But what if you want to permanently change your codebase? That's the idea behind C2Rust. There's value in updating legacy codebases to safer, more secure languages like Rust. So, if you want to do it, try a transpiler!
The value in transpilers stretches far beyond just writing simpler code, or code that targets some other language for backend work. Transpilers can change your codebase and make modernizing the code far easier. In the process, your code becomes safer.
Many frontends, one backend
Imagine you have a tool that you want to support many languages, like for static analysis. But, you don't want to write different analyses for every language's internal representation. What can you do? Introduce a new language, then transpile to that!
That's what I currently work on every day. I make translators that go from many source languages and produce one proprietary language as its output. Then, we run checks based on that internal language.
This can have a pretty high barrier to entry because you have to design a new language and your transpiler. But, depending on the use case, it may be an interesting solution.
Those familiar with LLVM may wonder why this is different from compilation? Many compilers, from Rust to Clang, generate LLVM from your source code. Then, the LLVM generates the machine code. But, that's a compiler, not a transpiler. Don't be silly.
Why?
I don't really have profound conclusions about transpilers. I just think they're cool and underappreciated. And, when you work with them, you get to see the internals of two languages at once, rather than just one!
Two is better than one!
So write more transpilers... And compilers.
Top comments (3)
JavaScript-targeting languages are interesting, because they're technically transpilers, but ultimately build to a technically-interpreted language. Thus, Kotlin and Dart are also, technically, transpiled.
And since there's assembly dialects that are directly machine code, things become really complicated at some point, when you want to draw the line between compiled and transpiled languages. That's not even mentioning VM bytecode which could also be considered a language unto its own, making both PHP and again Kotlin transpiled languages.
Great read
Long time ago before Typescript even exists, we had Java with one of the best ever transpilers we ever had. GWT which still works flawlessly until now, and also go the new future proofed one J2CL.