Three independent settings, each answering a different question.
target: which JavaScript syntax may appear in the output. Newer syntax is rewritten (??,**, optional chaining, class fields) and anything the target already supports is left alone. It also sets the defaultliband the default foruseDefineForClassFields(on from ES2022).lib: which built-in declarations the type checker knows about:Array.prototype.at,Object.hasOwn,Promise, and thedomtypes. It produces no output at all. Settinglibreplaces the default list, so"lib": ["es2022"]also dropsdom.module: which module format the emittedimport/exporttakes (commonjs,esnext,preserve,nodenext). It also steers module resolution defaults. It has no say over syntax such as??.
tsc never polyfills. Syntax is transformed; APIs are not. [1].at(0) is emitted unchanged even for target: es2015. If lib says an API exists but the runtime lacks it, you get a runtime TypeError, not a compile error. So set lib to what your real runtime provides (or what your polyfills add), and target to the oldest syntax that runtime can parse.
Defaults, by version: in 5.9 target defaulted to ES5 and module to CommonJS. TS 6.0 changed these to a floating latest target (es2025 on 6.0 and 7.0) and module: esnext. target: es5 is deprecated in 6.0 and an error in 7.0.
Practical picks: Node runs the output, so module: nodenext and a target matching the Node version. A bundler consumes it, so module: preserve, noEmit, and a target as modern as the bundler's input. Community baselines live in tsconfig/bases.