#43097·bun

tsconfig.json `extends` array (TS 5.0 form) is silently ignored by the bundler

Author: kamontatCreated Sep 17, 2026Updated Sep 17, 2026

What version of Bun is running?

1.4.2+744846f84

What platform is your computer?

Darwin 27.0.0 arm64 (macOS 27.0)

What steps can reproduce the bug?

TypeScript 5.0 allows extends to be an array of config paths. Bun's tsconfig parser only accepts a string, so when extends is an array the whole extends field is dropped — no warning is emitted and every option inherited from the base config(s) is lost. The relative-path string form of extends works fine, so this is distinct from #6326 (package specifiers not resolved through node_modules).

Self-contained repro (no dependencies):

mkdir bun-extends-array && cd bun-extends-array

cat > base.json <<'EOF'
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "useDefineForClassFields": false
  }
}
EOF

cat > dec.ts <<'EOF'
function deco(_target: unknown, _key: string) {}
export class A {
  @deco x = 1;
}
EOF

# 1) string extends — works
echo '{ "extends": "./base.json" }' > tsconfig.json
bun build ./dec.ts | head -1

# 2) array extends with the same single entry — ignored
echo '{ "extends": ["./base.json"] }' > tsconfig.json
bun build ./dec.ts | head -1

# tsc resolves the array form correctly
bunx tsc -p tsconfig.json --showConfig | grep -E 'experimentalDecorators|useDefineForClassFields'

What is the expected behavior?

Both builds should emit the legacy TypeScript decorator helper, because experimentalDecorators: true is inherited from base.json in both cases:

var __legacyDecorateClassTS = function(decorators, target, key, desc) {

Per TypeScript semantics, { "extends": ["a", "b"] } is equivalent to b extending a; later entries override earlier ones.

What do you see instead?

Case 1 (string) emits the legacy helper as expected. Case 2 (array) emits the TC39 standard-decorator runtime instead, i.e. experimentalDecorators/useDefineForClassFields from base.json were never applied:

== string extends
var __legacyDecorateClassTS = function(decorators, target, key, desc) {
== array extends
var __create = Object.create;

tsc --showConfig on the array form shows both options set, so the config itself is valid:

"experimentalDecorators": true,
"useDefineForClassFields": false

No warning or debug log is produced for the array case — the field is just skipped.

The same happens when the array appears further down the chain (tsconfig.json → string extends → a file whose extends is an array): everything above the array is dropped.

Additional information

Cause is in src/resolver/tsconfig_json.rs, where extends is only accepted when it is a string:

if let Some(extends_value) = extends_value {
    if !source.path.is_node_module() {
        if let Some(str) = extends_value.as_str() {
            result.extends = Box::from(str);
        }
    }
}

An array value falls through as_str() and result.extends stays empty. Since the resolver walks current.extends as a single path, supporting the array would mean iterating the entries in order and merging each parent (TypeScript semantics: later entries win over earlier ones, and the child config wins over all of them).

Real-world impact: shared config packages commonly compose presets with "extends": ["./base.json", "./features/x.json"]. With Bun, options that affect emitted code (experimentalDecorators, useDefineForClassFields, emitDecoratorMetadata, paths, baseUrl, jsx*) must be duplicated into the leaf tsconfig.json to take effect. In my case a Lit project built with Bun.build() produced unusable output (standard decorators applied to @property() fields) until both flags were inlined, even though bun tsc --showConfig showed the identical merged config either way.

Related: #6326 (package specifier extends not resolved), #5524 (exports map), #29197 (accessor fields with decorators fail to parse, which blocks the standard-decorator path for Lit).