Compile TypeScript to JavaScript

Learn how tsc converts .ts files to .js, what gets stripped during compilation, and the essential compiler flags every TypeScript developer uses.

5 min read

Compiling TypeScript means running the tsc command to convert .ts files into .js files. The compiler does two things at once: it checks all your type annotations for errors, and it strips the types to produce plain JavaScript.

The output is standard JavaScript that runs in any browser or Node.js version. TypeScript never adds runtime behavior. It only removes things.

The Basic Compile Command

Running the compiler starts with a single file. Given a small TypeScript file called app.ts that declares one typed variable:

typescripttypescript
const greeting: string = "Hello, TypeScript!";
console.log(greeting);

Compile it with a single command, which reads the file, checks its types, and writes a new JavaScript file next to it:

bashbash
tsc app.ts

Running that command produces a brand new file called app.js sitting right next to the original app.ts in the same folder:

javascriptjavascript
const greeting = "Hello, TypeScript!";
console.log(greeting);

Only one thing happened here: the string annotation was removed, because types do not exist at runtime. Nothing else changed, since the default target in modern TypeScript already emits a recent JavaScript version that supports const natively. Older syntax only appears when you set an older target yourself, which is explained below.

What Gets Stripped and What Gets Transformed

Compilation involves two separate operations:

OperationWhat happensExample
Type erasureAll type annotations, interfaces, type aliases, and generics are removedtype annotations, interfaces, and generic parameters all disappear
Syntax downlevelingModern JavaScript syntax is converted to older syntax based on targetconst to var, arrow functions to regular functions, template literals to string concatenation

Type erasure always happens. Syntax downleveling depends on your target setting. Here is a more complete example showing both effects together, using an interface, an arrow function, and a template literal in one small file called modern.ts:

typescripttypescript
interface User {
  name: string;
  age: number;
}
 
const getUser = (id: number): User => {
  return { name: `User ${id}`, age: 25 };
};
 
console.log(getUser(1).name);

Compiling this with no target set lets TypeScript float to the latest ECMAScript version it supports, so only the types disappear:

javascriptjavascript
const getUser = (id) => {
    return { name: `User ${id}`, age: 25 };
};
console.log(getUser(1).name);

The interface and its type annotations are gone, but the arrow function and template literal stay exactly as written, because the default target already supports them natively. Setting target to an older value like ES5 changes that, and downlevels the syntax on top of stripping the types:

javascriptjavascript
var getUser = function (id) {
    return { name: "User ".concat(id), age: 25 };
};
console.log(getUser(1).name);

Setting target to ES5 tells the compiler to downlevel syntax for older environments: the arrow function becomes a regular function, and the template literal becomes .concat(). Types are stripped either way. Only the syntax changes, and only when you ask for it.

Essential Compiler Flags

These flags control how tsc behaves. Use them on the command line or in your tsconfig.json.

FlagWhat it doesWhen to use
--target ES2022Emit modern JavaScriptProjects running on modern Node.js or recent browsers
--module NodeNextUse Node.js module systemNode.js projects
--outDir ./distPut compiled files in a separate folderKeeps source and output cleanly separated
--noEmitOnErrorDo not produce .js files if there are type errorsPrevents running broken code
--watchRecompile on every file saveDevelopment workflow
--strictEnable all strict type checksEvery new project

Combine flags directly on the command line when you want to override the config for a single run:

bashbash
tsc app.ts --target ES2022 --noEmitOnError

For anything beyond a one-off command, put the same options in a tsconfig.json file at the project root instead, so every teammate and every CI run uses the same settings:

jsonjson
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "outDir": "./dist",
    "strict": true,
    "noEmitOnError": true
  },
  "include": ["src"]
}

With this config, running tsc with no arguments compiles every .ts file in src/ and outputs the JavaScript to dist/.

Compiling with Errors

By default, tsc produces JavaScript output even when there are type errors. This is deliberate: it lets you run partial migrations where some files still have issues.

typescripttypescript
// error.ts
function add(a: number, b: number) {
  return a + b;
}
 
add("hello", 5);  // type error

Compile this file the normal way. The compiler reports the type mismatch, but by default it does not stop the JavaScript from being written:

bashbash
tsc error.ts

Compiling prints the type error straight to the terminal, with the file, line, and column pointing exactly at the mismatched argument:

texttext
error.ts:5:5 - error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

Despite the error, error.js is still created and will run, producing "hello5" at runtime with no warning at all. To prevent JavaScript from being written when there is an error, add the --noEmitOnError flag:

bashbash
tsc error.ts --noEmitOnError

Now no error.js file appears. The compiler refuses to output JavaScript until you fix the type error. This is the safer default for production builds.

Watch Mode for Development

The --watch flag keeps tsc running and recompiles whenever you save a .ts file:

bashbash
tsc --watch

The compiler starts, checks the project once, and then stays running in the terminal instead of exiting, printing a status line when it finds no problems:

texttext
[10:30:15 AM] Starting compilation in watch mode...
[10:30:15 AM] Found 0 errors. Watching for file changes.

Save a file with a type error while tsc --watch is still running, and the terminal updates immediately without you running any command:

texttext
[10:30:45 AM] File change detected. Starting incremental compilation...
error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
[10:30:45 AM] Found 1 error. Watching for file changes.

Fix the error and save the file again, and the terminal clears itself and confirms the project compiles cleanly right away:

texttext
[10:31:02 AM] File change detected. Starting incremental compilation...
[10:31:02 AM] Found 0 errors. Watching for file changes.

Watch mode gives you a fast feedback loop. Keep a terminal open with tsc --watch running, and you see type errors the moment you save.

Compiling an Entire Project

For projects with multiple files, you need a tsconfig.json. Create one at the project root:

bashbash
tsc --init

This generates a tsconfig.json with every option listed and commented. Edit it to set target, module, outDir, and strict. Then run tsc with no arguments:

bashbash
tsc

The compiler reads tsconfig.json, finds every .ts file in the project, and compiles them all. For more on project setup, see TypeScript project setup for beginners.

The Compiled Output is Just JavaScript

The most important thing to remember: after compilation, you have plain .js files. You can run them with Node.js, serve them to a browser, or bundle them with any JavaScript tool. TypeScript does not lock you into a special runtime. It produces standard JavaScript that works everywhere.

For help understanding what the compiler tells you when something is wrong, see read TypeScript compiler errors.

Rune AI

Rune AI

Key Insights

  • Compile a single file with tsc filename.ts. Compile a project with just tsc (requires tsconfig.json).
  • The compiler does two things: type checking and JavaScript output. Types are always stripped.
  • Use --noEmitOnError to prevent JavaScript output when there are type errors.
  • Use --watch to recompile automatically on every file save.
  • The target option controls which JavaScript version is emitted. module controls the module system.
RunePowered by Rune AI

Frequently Asked Questions

Does tsc only strip types, or does it also transform JavaScript syntax?

Both. tsc strips type annotations and transforms modern JavaScript syntax (arrow functions, async/await, template literals, etc.) based on the `target` and `module` settings in your tsconfig. If you target ES2015, modern syntax is kept. If you target ES5, everything is downleveled.

Can I compile TypeScript without a tsconfig.json file?

Yes. `tsc filename.ts` works without any configuration. The compiler uses its built-in defaults. A tsconfig.json gives you control over the output but is not required for single files.

Why does my compiled JavaScript look different from my TypeScript?

tsc may transform modern JavaScript features like template literals, arrow functions, and async/await into older syntax depending on your `target` setting. This is separate from type stripping and is the same transformation a JavaScript bundler would do.

Conclusion

Compiling TypeScript is a single command: tsc. The compiler does two things: checks your types for errors, then strips them and outputs plain JavaScript. The output is determined by your target and module settings. Use --watch during development, --noEmitOnError to prevent broken JavaScript, and --outDir to keep your source and output folders separate. The compiled JavaScript is standard JavaScript that runs anywhere Node.js or a browser runs.