Transpiler vs Compiler: The Difference, with TypeScript

A transpiler outputs source code at the same level as its input; a classic compiler outputs bytecode or machine code. Real javac, tsc and Node.js output shows the difference.

angular

A transpiler is a compiler whose output is source code in another high-level language, at about the same level of abstraction as the input. A compiler, in the narrower sense most developers mean, translates source code into a lower-level form such as machine code or JVM bytecode. So the difference between a transpiler and a compiler is the level of the output, because javac turns Hello.java into bytecode for the JVM, whereas the TypeScript compiler (tsc) turns hello.ts into readable JavaScript.

We use a transpiler when the platform runs only one language, such as JavaScript in the browser, but we want to write our code in another language or in a newer version of the same language. For example, a team writes the frontend of its web shop in TypeScript, and the browsers of its customers get plain JavaScript.

The following example sends one TypeScript line through tsc with two different targets. All examples in the article use TypeScript 7.0.2, Node.js 22 and JDK 21.

// 1. TypeScript input
const total = (price: number, qty?: number) => price * (qty ?? 1);
const cost = total(5, 3);                     // cost = 15

// 2. tsc output with target es2024: only the types are removed
// const total = (price, qty) => price * (qty ?? 1);

// 3. tsc output with target es2015: ?? is rewritten for older engines
// const total = (price, qty) => price * (qty !== null && qty !== void 0 ? qty : 1);

// 4. A compiler in the classic sense: Hello.java -> Hello.class (JVM bytecode)

Notice that both outputs are JavaScript source code that a person can read and edit, and only the es2015 output rewrites the ?? operator.

Next, we compare both terms with real output from javac and tsc, and we see what tsc changes when it targets an older JavaScript version and why the TypeScript team still calls tsc a compiler. After that, we cover the type-stripping tools (esbuild, SWC, Babel, Node.js) that transpile TypeScript without checking types.

1. Compiler and Transpiler Side by Side

Both tools read a program and write an equivalent program in another form. What they produce is the only real difference between the two terms, so we compare them by their output.

AspectCompiler (classic sense)Transpiler (source-to-source compiler)
OutputMachine code, bytecode or assemblySource code in a high-level language
Level of abstractionLower than the inputAbout the same as the input
Who reads the outputA CPU or a virtual machineAnother tool, an engine, or a developer
Output is readableNo, or only with a disassemblerYes
Typical examplesjavac (Java to bytecode), GCC and Clang (C to machine code)tsc (TypeScript to JavaScript), Babel (new JavaScript to older JavaScript), Nim to C
Main reason to use itRun the program on hardware or a VMUse a language or language version that the target platform does not support

A Java developer already knows the classic case. The javac output is a .class file with bytecode instructions, which we can only read with a tool such as javap.

javac Hello.java
javap -c Hello
  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #13                 // String Hello
       5: invokevirtual #15                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return

The TypeScript compiler, by contrast, writes JavaScript that looks almost like the input. The .js file is still a program in a high-level language, which a JavaScript engine then parses and compiles again.

2. Why the TypeScript Team Calls tsc a Compiler

The official name of tsc is “the TypeScript compiler”, and the TypeScript handbook uses the word compiler throughout. The name is correct, because “compiler” in computer science means any program that translates one language into another. A transpiler is one kind of compiler, not a separate kind of tool.

The line between the two terms is also less clear than the comparison table suggests.

  • The javac compiler does not produce machine code either. The JVM’s just-in-time (JIT) compiler turns the bytecode into machine code at runtime.
  • GCC first produces assembly language, which an assembler then turns into machine code.
  • The tsc compiler does far more than copy text. It parses the program and builds a type for every expression, and it reports errors before it writes any JavaScript.

In practice, developers say “transpile” when they mean “convert source code to source code”, and the word is useful in the JavaScript world because almost every tool outputs JavaScript. When we read “compile TypeScript” and “transpile TypeScript”, both mean running tsc or a similar tool on .ts files.

3. What tsc Does in One Run

A single npx tsc run does three jobs, and knowing them separately helps when we choose build tools in section 5.

  1. It parses every .ts file in the project into a syntax tree.
  2. It type-checks the program and reports errors such as a string passed where a number is expected.
  3. It emits JavaScript. It removes the types and rewrites syntax that the chosen target does not support, and it writes the import and export statements in the chosen module format.

The jobs can also run alone, so the command npx tsc –noEmit runs only steps 1 and 2, which is the usual type check in CI. By default, tsc still emits JavaScript when step 2 finds errors, and the noEmitOnError option stops the emit in that case. We set the target and module options in tsconfig.json.

4. Downleveling: Compiling to an Older JavaScript Version

The target option tells tsc which JavaScript version the output must run on. When the target is older than the syntax we use, tsc rewrites that syntax with older constructs. Rewriting newer syntax for an older version is called downleveling, and downleveling is the original reason JavaScript transpilers exist.

Say an online shop must keep working for customers whose browser supports ES2015 but not ES2020. The developers still write ?? and ?. in their TypeScript code, and tsc writes the older equivalent into the JavaScript that the shop serves. Our cart.ts file uses several newer features, such as these two operators, the ** operator and a private class field.

export const total = (price: number, qty?: number) => price * (qty ?? 1);

export class Cart {
  items: string[] = [];
  #count = 0;
  add(item: string): void {
    this.items.push(item);
    this.#count++;
  }
  get size(): number {
    return this.#count;
  }
}

export const firstLetter = (name?: string) => name?.[0];
export const square = (n: number) => n ** 2;

With –target es2024, every feature in this file already exists in the target version, so tsc only removes the types.

export const total = (price, qty) => price * (qty ?? 1);
export class Cart {
    items = [];
    #count = 0;
    add(item) {
        this.items.push(item);
        this.#count++;
    }
    get size() {
        return this.#count;
    }
}
export const firstLetter = (name) => name?.[0];
export const square = (n) => n ** 2;

With –target es2015, the output changes a lot. The ?? and ?. operators (ES2020), the ** operator (ES2016), class fields and the private #count field (ES2022) all get rewritten.

var _Cart_count;
export const total = (price, qty) => price * (qty !== null && qty !== void 0 ? qty : 1);
export class Cart {
    constructor() {
        this.items = [];
        _Cart_count.set(this, 0);
    }
    add(item) {
        var _a;
        this.items.push(item);
        __classPrivateFieldSet(this, _Cart_count, (_a = __classPrivateFieldGet(this, _Cart_count, "f"), _a++, _a), "f");
    }
    get size() {
        return __classPrivateFieldGet(this, _Cart_count, "f");
    }
}
_Cart_count = new WeakMap();
export const firstLetter = (name) => name === null || name === void 0 ? void 0 : name[0];
export const square = (n) => Math.pow(n, 2);

The private field became a WeakMap with two helper functions (__classPrivateFieldGet and __classPrivateFieldSet) at the top of the file, which we trimmed here. Both versions return the same results. A cart with one item has size 1, total(5) is 5, firstLetter(“Lokesh”) is “L”, and square(4) is 16.

Downleveling has two limits that often cause confusion.

  • Downleveling rewrites syntax only. Newer built-in methods such as Array.prototype.findLast() (ES2023) are not added to old engines, so the app needs a polyfill library such as core-js for them. With target es2015 and the default lib, the compiler reports error TS2550 with the message “Property ‘findLast’ does not exist on type ‘number[]’. Do you need to change your target library?”
  • TypeScript 7 no longer compiles to ES5. The oldest target is es2015, and the downlevelIteration option was removed with it. Every browser in current use and every supported Node.js version runs ES2015, so for most projects the right target today is a recent version such as es2024.

Many older tutorials describe ES6 as the version that “brings types to JavaScript”, but ES6 never had types. ES6 (ES2015) added classes, modules, let and const, arrow functions and promises, but JavaScript has no static types in any version. The types come only from TypeScript.

5. Transpiling Without Type Checking

Type checking is the slow part of tsc, so many build tools skip it. Tools such as esbuild, SWC and Babel remove the TypeScript types file by file and write JavaScript, without building a type for every expression. These tools are pure transpilers, and skipping the type check makes them much faster than tsc. Bundlers and dev servers use them for .ts files and leave type checking to a separate tsc run.

For example, a developer saves a component in a large frontend project, and the dev server must show the change in the browser right after the save. The dev server transpiles only the saved file, whereas a full type check would look at the whole project.

Node.js 22 has the same kind of transpiler built in. Since Node.js 22.18, the command node file.ts runs a TypeScript file by replacing the type annotations with spaces. The node:module package exposes the same step as the experimental function stripTypeScriptTypes().

import { stripTypeScriptTypes } from "node:module";

const source = 'const total = (price: number, qty?: number): number => price * (qty ?? 1);';
const output = stripTypeScriptTypes(source);
const total = (price        , qty         )         => price * (qty ?? 1);
(node:7817) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time

The types turned into spaces, so every remaining character stays at its original column and error stack traces still point to the right place. A line with a type error such as const n: number = “hi” passes through the same function without any error, because type stripping never checks a type.

Because these tools look at one file at a time, they cannot handle TypeScript features that need information from other files or that generate code, such as enum in Node.js. Two compiler options make tsc report single-file problems during the build.

  • The isolatedModules option reports code that a single-file transpiler cannot convert safely, for example re-exporting a type without export type.
  • The erasableSyntaxOnly option reports every construct that is more than removable types, such as enum, a namespace with code, constructor parameter properties and import x = require() aliases. Code that passes the erasableSyntaxOnly check runs with Node.js type stripping.

Current projects combine both kinds of tools. A fast transpiler (esbuild, SWC or Node.js) produces the JavaScript, and tsc –noEmit runs as a separate type check in the editor and in CI. With the native compiler in TypeScript 7, the tsc type check is several times faster than before, but its job is still checking, not producing the output.

6. Comparing the Outputs Yourself

To see the downleveling on our own machine, we clone the transpiler-vs-compiler folder. It holds the quick-reference code, the cart.ts sample, two scripts that compile it for different targets, and the stripTypeScriptTypes() demo, with TypeScript 7.0.2 as a dev dependency and Node.js 22 as the runtime.

npm install
npm start               # compile src with tsc and run it
npm run emit:es2024     # write out/es2024/cart.js
npm run emit:es2015     # write out/es2015/cart.js with downleveled syntax
npm run strip           # strip types with Node.js

The two emit scripts compile one file with the –ignoreConfig flag, which TypeScript 7 requires when a tsconfig.json exists in the folder. Comparing out/es2024/cart.js and out/es2015/cart.js side by side shows the downleveling from section 4.

7. Conclusion

A transpiler is a compiler that outputs source code at the same level as its input, and a compiler in the classic sense outputs lower-level code such as bytecode or machine code. By that definition, tsc is a transpiler when it writes JavaScript and a type checker before that, and the TypeScript team calls the whole tool a compiler.

In daily work, a different distinction is more useful. The tsc compiler checks types and emits code, whereas esbuild, SWC, Babel and Node.js type stripping only remove types. Most projects use one of the fast tools for the output and tsc –noEmit for the checks, because the type checks are the main thing TypeScript adds to JavaScript.

8. References

The TSConfig reference documents the options used here, and the Node.js and esbuild documentation describe type stripping.

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.