target
Sets which ECMAScript version TypeScript emits and which default library declarations come with it.
"target": "es3" | "es5" | "es6"/"es2015" | "es2016" | "es2017" | "es2018" | "es2019" | "es2020" | "es2021" | "es2022" | "es2023" | "es2024" | "es2025" | "esnext" // Default: es2023 if module is node20; esnext if module is nodenext; ES5 otherwise- Added in TypeScript 1.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$targetoptional | string= es2023 if module is node20; esnext if module is nodenext; ES5 otherwise | The ECMAScript language version for emitted JavaScript. Anything newer than the chosen version is downlevelled, and the default value of lib is derived from it. |
Errors
Nothing at runtime; syntax that cannot be downlevelled — such as BigInt literals below ES2020 — is reported as a compile error.
Changelog
- PHP 4.3useDefineForClassFields began defaulting to true for ES2022 and above, so raising target changes class field emit semantics.
- PHP 5.0target: ES3 was deprecated.
- PHP 5.5target: ES3 no longer has any effect; specifying it becomes an error in TypeScript 6.0.
target answers one question: what is the oldest runtime this output must work on? Everything newer than that version is rewritten. An arrow function () => this becomes a function expression with a captured _this under ES5 or lower; async/await becomes a state machine below ES2017; ** becomes Math.pow below ES2016.
It also picks your lib
Changing target changes the default value of lib, so target: "es5" means Promise, Map and Object.entries are not declared. You may mix and match — a high lib with a low target is the right combination when a polyfill supplies the runtime behaviour that the emitter cannot.
What trips people up
- Downlevelling rewrites syntax, never runtime APIs. Targeting ES5 does not give you a
Promise. esnextmeans "whatever this compiler version supports", so its meaning changes under you on upgrade.- Reaching ES2022 flips
useDefineForClassFieldson by default, which changes class semantics, not just syntax. - Iterating a
SetorMapunder ES5 needsdownlevelIterationto be correct.
Example
// "target": "ES5" -> everything below is rewritten
// "target": "ES2017" -> emitted almost verbatim
class Greeter {
constructor(private name: string) {}
greet = () => "hi " + this.name; // ES5: function expr + var _this
}
console.log(new Greeter("ts").greet()); // hi ts
const unique = new Set([1, 2, 2, 3]); // Set needs lib es2015 (implied)
console.log([...unique]); // [ 1, 2, 3 ] (spread downlevelled)
console.log(2 ** 10); // 1024 (** -> Math.pow below ES2016)When to use it
- Choosing the oldest browser or Node version your bundle must run on
- Pairing a low target with a high lib when polyfills provide the missing runtime APIs
- Explaining why an arrow function became a function expression in the build output
Frequently asked questions
What target should I use in tsconfig?
The oldest ECMAScript version your runtime supports: ES2022 or higher for modern Node and evergreen browsers, lower only if you ship to old environments. Note that ES2022 and above also flips useDefineForClassFields to true.
What is the difference between target and lib?
target controls which syntax gets downlevelled in the emitted JavaScript; lib controls which built-in API declarations the type checker knows about. target sets the default lib, but you can override lib to assume polyfills are present.
Why is target esnext risky?
Because esnext means whatever the installed TypeScript version supports, so upgrading the compiler can change your emitted output without any config change. Pin a numbered version when reproducible builds matter.
More examples
Mixing target and lib
Prints { a: 1, b: 2 } on any runtime that has Object.fromEntries. The point is the split: target controls syntax rewriting, lib controls which APIs the type checker believes exist, and neither one ships an implementation.
// tsconfig.json
// {
// "compilerOptions": {
// "target": "es5", // emit for old runtimes
// "lib": ["es2019", "dom"] // but assume polyfills exist
// }
// }
// Object.fromEntries is declared by lib es2019; targeting es5 does
// NOT provide it at runtime, so a polyfill must be loaded first.
const pairs: [string, number][] = [["a", 1], ["b", 2]];
console.log(Object.fromEntries(pairs)); // { a: 1, b: 2 }What downlevelling actually rewrites
Prints "ada" then "found accent". Under ES5 the await becomes helper code and the for...of over a string only iterates code points correctly with downlevelIteration enabled — a case where the emitted output is not merely longer but semantically different.
async function fetchName(): Promise<string> {
return "ada";
}
// target ES2017+: emitted as written.
// target ES5/ES2015: rewritten into a generator-and-state-machine
// helper (__awaiter/__generator), and Promise must be polyfilled.
const name = await fetchName();
console.log(name); // ada
for (const ch of "héllo") {
if (ch === "é") console.log("found accent"); // found accent
}
Discussion