Skip to content

/flags/typescript/tsc-target

tsc --target: which JavaScript version TypeScript emits (es2017, es2020, esnext)

verified onenvironmentlocal · windows 10.0.26200 arm64 · GNU bash 5.3.15 (Git Bash) · v24.20.0updated
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

bash
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: 1

es2017 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

TargetFirst 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)
es2017async/await (lower targets use the __awaiter helper with generators)
es2018object rest/spread, for await...of
es2019optional catch binding: catch without (e)
es2020optional chaining ?., nullish coalescing ??; BigInt literals need at least this target
es2021logical assignment ||=, &&=, ??=, numeric separators
es2022class fields with define semantics, private #methods, static blocks
es2023 to es2025, esnextlater 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 from target; 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 of target; set both. In TypeScript 6 and 7 several old module and moduleResolution values 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 target in tsconfig for their own output and use their own build target setting; tsc's target then 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.
  • useDefineForClassFields defaults to true for es2022 and 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.

Sources