A TypeScript build that downlevels to ES5 emits the same helper functions — `__extends`, `__awaiter`, `__spreadArray` — at the top of many output files. What does the `importHelpers` compiler option change, what must the package declare, and when is it the wrong choice?
answer
- one copy instead of per-file copies
- the helpers have a package
- runtime dependency, not a dev one
- gain scales with downleveling
- a third flag emits none
basics
~20 sBy default the compiler inlines its downleveling helpers into every file that needs them. importHelpers makes it import them from the tslib package instead, so they exist once. tslib then becomes a real runtime dependency, which a published library must list under dependencies.
solid answer
~50 sDownleveling needs runtime support code: `__extends` for `extends` at ES5, `__awaiter`/`__generator` for `async`/`await` below ES2017, `__spreadArray` for spread, `__importDefault` for interop. By default `tsc` **inlines a private copy into every emitted file** that uses one, which duplicates the same bytes across a large program. Setting `"importHelpers": true` replaces each copy with an import from **`tslib`**, the package Microsoft publishes containing exactly those helpers, so the program carries one copy. The obligation that comes with it: `tslib` is now a genuine runtime dependency and belongs in `dependencies` — not `devDependencies` — of anything you publish, or consumers get a module-not-found at run time. It is the wrong choice when you would rather not impose a dependency on consumers at all, when the build barely downlevels anything (a modern target emits almost no helpers, so the win is nil), or when a bundler already dedupes effectively. `noEmitHelpers` is the third option: emit no helpers and supply them yourself.
code
json · 7 lines{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"importHelpers": true
}
}go deeper
Know that downleveling generates small helper functions and that this option imports them from the tslib package instead of copying them into each file.
Name real helpers and their triggers, and explain that enabling the flag creates a runtime dependency that must be declared in dependencies.
Diagnose both symptoms — duplicated helpers in dist and a consumer's missing-tslib error — and weigh the flag against simply raising the target, which removes most helpers outright.
Decide the policy across a monorepo or a published surface: uniform adoption versus a zero-dependency promise, and how tslib versioning is kept in step with compiler upgrades.
## Where the helpers come from Some TypeScript features cannot be expressed in older JavaScript with syntax alone; they need a small function at runtime. The compiler owns a fixed catalogue of these, and which ones appear depends on your `target` and other flags: - `__extends` — prototype wiring for `class B extends A` below an ES2015 target; - `__awaiter` and `__generator` — the state machine that implements `async`/`await` below ES2017; - `__spreadArray` and `__read` — array spread and iterable destructuring at low targets; - `__importDefault` and `__importStar` — CommonJS interop shims; - `__decorate` and `__metadata` — decorator support. By default the compiler **inlines** whichever helpers a file needs at the top of that file's output. Each file gets its own private copy, so a 400-file program that uses `async` everywhere ships `__awaiter` 400 times. ## What `importHelpers` changes With `"importHelpers": true`, the compiler emits an import from `tslib` instead of a definition: ```javascript var tslib_1 = require("tslib"); // ... tslib_1.__awaiter(this, void 0, void 0, function () { ... }) ``` `tslib` is the official runtime package holding exactly these helper implementations. One copy exists in the module graph, and every consumer of it shares that copy. The effect is proportional to how much you downlevel: on an ES5 build of a large codebase it is a meaningful size reduction; on a modern target that emits few or no helpers, it is close to nothing. ## The obligation you take on `importHelpers` converts a self-contained output into one with a **runtime dependency**. Two consequences follow. First, packaging. `tslib` must appear in `dependencies` of any package you publish — putting it in `devDependencies` is the classic mistake, because the build passes locally and consumers get `Cannot find module 'tslib'` in production. (For an application that bundles everything before deploying, `devDependencies` is defensible, but the safe default is `dependencies`.) Second, version coupling. `tslib` tracks the compiler's helper set, so a `tsc` upgrade that starts emitting a newer helper requires a `tslib` new enough to contain it. Keep the two in step; a stale `tslib` shows up as an undefined helper at runtime rather than a build error. ## When it is the wrong choice - **A modern target.** With `target: es2022` there is barely any downleveling: no `__extends`, no `__awaiter`. Taking on a dependency to deduplicate helpers you no longer emit is pure cost. Raising the target is the better version of this optimisation. - **A dependency-free published library.** Some packages advertise zero runtime dependencies as a feature. Inlined helpers are a few hundred bytes of duplication in exchange for keeping that promise; that can be the right trade. - **Bundled applications where the bundler already helps.** A bundler that dedupes and minifies may collapse much of the duplication anyway, so measure the actual bundle before adding the dependency. - **When you want no helpers at all.** `noEmitHelpers` tells the compiler to emit none and assume they exist globally. That suits a build where another tool provides them, and it fails loudly if nothing does. ## Diagnosing it in the wild Two symptoms bring this option up in real work. Seeing the same `__awaiter` block repeated across dozens of files in `dist/` is the size symptom. A consumer reporting `Cannot find module 'tslib'` is the packaging symptom, and it means the flag was enabled without moving `tslib` into `dependencies`. There is also a subtler failure: mixing the two settings across a monorepo means some packages carry inlined helpers and others import `tslib`, so the deduplication is only partial. If you adopt `importHelpers`, adopt it uniformly. ## What a strong answer covers Name a couple of real helpers and what triggers them; state that the default is inlining per file; identify `tslib` by name and put it in `dependencies`; and close with the judgment — that a modern target is the version of this optimisation that costs nothing, so the flag matters mainly for builds that genuinely still downlevel.
- What breaks if tslib is listed only in devDependencies of a published package?Consumers install the package without `tslib`, and the first emitted helper call fails at runtime with a module-not-found error. Nothing catches it at build time in your repo, because your own `devDependencies` are installed there — which is exactly why it reaches production.
- What does noEmitHelpers do differently from importHelpers?`importHelpers` emits imports from `tslib`; `noEmitHelpers` emits neither definitions nor imports, assuming the helpers are already available as globals. It suits a build where another tool injects them, and it fails at runtime — loudly — if nothing does.
- Does importHelpers help a project that targets es2022?Barely. At that target classes, async/await and spread are emitted natively, so few or no helpers are generated and there is almost nothing to deduplicate. Interop helpers may still appear under CommonJS emit, but the size argument that motivates the flag largely disappears.
- Why must tslib's version stay in step with the compiler version?Because the helper set evolves with the compiler. A newer `tsc` can emit a call to a helper that an older `tslib` does not export, producing an undefined-function error at runtime rather than a build failure. Upgrading them together avoids a class of bug that build-time checks will not catch.
saying these in an interview costs you the question
- Says tslib belongs in devDependencies of a published package
- Thinks importHelpers reduces size at any target
- Believes the helpers are polyfills for missing built-ins
- Assumes bundlers always dedupe inlined helpers anyway
- Confuses importHelpers with noEmitHelpers