/flags/typescript/tsc-target
tsc --target: which JavaScript version TypeScript emits (es2017, es2020, esnext)
- tool
- TypeScript
- command
- tsc
- flag
- --target, -t <version>
- default
- not set; TypeScript 7.0.2 then kept modern syntax unchanged (see the example)
tsc --target es2020 sets which edition of JavaScript the compiler writes. Syntax that the target does not support is rewritten into older equivalent code: with es2017 or es2019, optional chaining ?. and ?? become ternaries; with es2015, async functions become generator helpers. Newer targets keep the code almost as written. target also sets the default lib, the set of built-in APIs the type checker knows about. In TypeScript 7 the oldest accepted target is es2015; es5 has been removed.
One file, several targets
npm init -y >/dev/null
npm install --silent --no-audit --no-fund typescript@7.0.2 >/dev/null 2>&1
echo 'export const f = async (x?: { a: number }) => x?.a ?? 0;' > a.ts
for t in es2015 es2017 es2020 esnext; do
echo "== --target $t"
npx tsc a.ts --target $t --module esnext --outDir out-$t && cat out-$t/a.js
done
echo "== no --target"
npx tsc a.ts --module esnext --outDir out-default && cat out-default/a.js
echo "== --target es5"
npx tsc a.ts --target es5 --outDir out-es5; echo "exit: $?"
echo "== --target es3"
npx tsc a.ts --target es3; echo "exit: $?"Output
== --target es2015
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
export const f = (x) => __awaiter(void 0, void 0, void 0, function* () { var _a; return (_a = x === null || x === void 0 ? void 0 : x.a) !== null && _a !== void 0 ? _a : 0; });
== --target es2017
export const f = async (x) => { var _a; return (_a = x === null || x === void 0 ? void 0 : x.a) !== null && _a !== void 0 ? _a : 0; };
== --target es2020
export const f = async (x) => x?.a ?? 0;
== --target esnext
export const f = async (x) => x?.a ?? 0;
== no --target
export const f = async (x) => x?.a ?? 0;
== --target es5
error TS5108: Option 'target=ES5' has been removed. Please remove it from your configuration.
exit: 2
== --target es3
error TS6046: Argument for '--target' option must be: 'es6', 'es2015', 'es2016', 'es2017', 'es2018', 'es2019', 'es2020', 'es2021', 'es2022', 'es2023', 'es2024', 'es2025', 'esnext'.
exit: 1es2017 is the first target that keeps async, and es2020 the first that keeps ?. and ??. Without --target, TypeScript 7 left the code unchanged. Asking for es5 fails with TS5108 and nothing is emitted.
Target values and what they keep
| Target | First target that keeps this syntax as written |
|---|---|
| es2015 (es6) | classes, arrow functions, let/const, template literals, generators (older syntax than this cannot be emitted by TypeScript 7) |
| es2017 | async/await (lower targets use the __awaiter helper with generators) |
| es2018 | object rest/spread, for await...of |
| es2019 | optional catch binding: catch without (e) |
| es2020 | optional chaining ?., nullish coalescing ??; BigInt literals need at least this target |
| es2021 | logical assignment ||=, &&=, ??=, numeric separators |
| es2022 | class fields with define semantics, private #methods, static blocks |
| es2023 to es2025, esnext | later syntax; esnext follows the newest features TypeScript supports |
The table lists the main features only. An invalid value is rejected with TS6046 and the list of accepted values.
target, lib and module
- target changes the emitted syntax.
- lib decides which runtime APIs type check (
Array.prototype.findLast,Promise.withResolvers). It defaults fromtarget; set it explicitly if your runtime differs from that default, for example"lib": ["es2022", "dom"]in a browser project. - module decides the import/export format (
nodenext,esnext,commonjs) and is independent oftarget; set both. In TypeScript 6 and 7 several oldmoduleandmoduleResolutionvalues were removed, which fails with errors such as TS5108.
Check the effective value in a project with extends using tsc --showConfig.
Pitfalls
- Bundlers (Vite, esbuild, webpack with swc) usually ignore
targetin tsconfig for their own output and use their own build target setting; tsc'stargetthen matters only for type checking defaults. - A lower target makes code larger and slower (helpers such as
__awaiter), so do not lower it "to be safe" if every runtime you support is modern. useDefineForClassFieldsdefaults to true fores2022and later, which changes how class fields are initialised compared to older targets; libraries that rely on decorators or property setters may notice the difference.
FAQ
Does --target add polyfills for missing APIs?
No. target only rewrites syntax, such as async functions or optional chaining. Library APIs like Array.prototype.at or Object.groupBy are not added; if the runtime lacks them, include a polyfill. The lib option only controls which APIs the type checker accepts.
Which target should I use for Node.js?
Match the oldest Node.js version you run. Current LTS lines support everything up to recent ECMAScript editions, so es2022 or later is a common choice; the official @tsconfig/node bases on npm list a matching target per Node.js version.
Why does TypeScript say target ES5 has been removed?
TypeScript 7 no longer emits ES5 or ES3: the oldest accepted value is es2015 (es6). Older code that needs ES5 output must use a bundler or Babel for the last step, or stay on an older TypeScript version.