Skip to content

feat(tsconfig): parse strict and strictNullChecks compiler options#1166

Merged
Boshen merged 3 commits into
oxc-project:mainfrom
kylecannon:tsconfig-strict-null-checks
May 27, 2026
Merged

feat(tsconfig): parse strict and strictNullChecks compiler options#1166
Boshen merged 3 commits into
oxc-project:mainfrom
kylecannon:tsconfig-strict-null-checks

Conversation

@kylecannon

@kylecannon kylecannon commented May 27, 2026

Copy link
Copy Markdown
Contributor

Summary

CompilerOptions models only the options the resolver/transformer pipeline consumes (paths, baseUrl, jsx*, experimentalDecorators, emitDecoratorMetadata, useDefineForClassFields, verbatimModuleSyntax, target, module, and so on). It has no field for strict or strictNullChecks, so even when a user sets them in tsconfig.json the value is dropped at parse time, leaving a downstream consumer nothing to read.

This blocks consumers (for example rolldown's merge_transform_options_with_tsconfig) from inferring strictNullChecks from a tsconfig and forwarding it to the transformer, where it controls whether null/undefined are elided from union design:type annotations under legacy emitDecoratorMetadata. Today that can only be set via the explicit transform.decorator.strictNullChecks option.

This PR adds the two fields, inherits them through extends, and exposes a resolver that applies TypeScript's precedence rule.

TypeScript semantics

tsc resolves the flag as strictNullChecks ?? strict (see getStrictOptionValue: options[flag] === undefined ? !!options.strict : options[flag]):

  • an explicit strictNullChecks always wins over the value implied by strict, in either direction;
  • when strictNullChecks is absent it falls back to strict;
  • strict: false on its own therefore yields false.

effective_strict_null_checks() implements exactly this with self.strict_null_checks.or(self.strict). It returns None only when neither is set, deliberately leaving the final default to the consumer rather than baking one into the resolver (the common transformer default is true).

The two fields are merged independently in extend_tsconfig (mirroring emitDecoratorMetadata), so the precedence rule stays correct across an extends chain: the nearest explicit strictNullChecks still wins over a strict set in any ancestor, consistent with the later-wins merge from #1156.

Changes

  • src/tsconfig.rs
    • CompilerOptions: add strict: Option<bool> and strict_null_checks: Option<bool> (serde camelCase mapping, no per-field rename).
    • extend_tsconfig: inherit both fields independently from the extended config.
    • new effective_strict_null_checks() accessor (#[must_use]).
  • src/tests/tsconfig_extends.rs
    • test_effective_strict_null_checks_resolution: full truth table (strict:true alone, explicit strictNullChecks:false override, strict:false alone, neither).
    • test_extend_tsconfig_strict_null_checks: a child's explicit strictNullChecks:false survives inheriting a parent's strict:true; a child with neither inherits the parent's strict.

Purely additive. All existing tests pass (cargo test and cargo test --all-features, clippy -D warnings, cargo doc -D warnings, fmt, typos, and the NAPI JS suite all green locally).

Follow-up (separate, downstream)

This is the enabling resolver-side change. Inferring the value end to end is a downstream PR in rolldown: forward compiler_options.effective_strict_null_checks() in merge_transform_options_with_tsconfig into DecoratorOptions.strict_null_checks (mapping None to keep the transformer default), and add the field to its inline BindingTsconfigCompilerOptions for the non-disk config path.

🤖 Generated with Claude Code

Add `strict` and `strictNullChecks` to `CompilerOptions`, inherit them
independently through `extends`, and expose `effective_strict_null_checks()`
which applies TypeScript's rule that an explicit `strictNullChecks` overrides
the value implied by `strict` (returning `None` when neither is set, leaving
the default to the consumer).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kylecannon kylecannon marked this pull request as ready for review May 27, 2026 05:58
@Boshen Boshen merged commit 50e1aaa into oxc-project:main May 27, 2026
14 checks passed
@oxc-guard oxc-guard Bot mentioned this pull request May 27, 2026
@codspeed-hq

codspeed-hq Bot commented May 27, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 5.77%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
✅ 12 untouched benchmarks
⏩ 5 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
resolver_real[multi-thread] 411 µs 388.6 µs +5.77%

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing kylecannon:tsconfig-strict-null-checks (d4e53e3) with main (827b45a)

Open in CodSpeed

Footnotes

  1. 5 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@kylecannon kylecannon deleted the tsconfig-strict-null-checks branch May 27, 2026 06:57
shulaoda added a commit to rolldown/rolldown that referenced this pull request May 27, 2026
## Summary

Builds on #9563, which upgraded oxc to `0.133.0` and surfaced the `strictNullChecks` decorator-metadata option (from oxc-project/oxc#22266) in the TypeScript types, the validator, and the generated binding.

However, #9563's Rust `From<DecoratorOptions> for oxc::transformer::DecoratorOptions` hardcodes `strict_null_checks: true`, and the napi options normalizer doesn't carry the field, so `transform.decorator.strictNullChecks: false` is currently **silently ignored** through rolldown's transform/bundle pipeline (the option exists in the type surface but does nothing).

This PR wires it through so the option actually takes effect.

## What changed

- `rolldown_common::DecoratorOptions` gains a `strict_null_checks: Option<bool>` field; its `From` impl now uses `options.strict_null_checks.unwrap_or(true)` (mirroring oxc's own default) instead of the hardcoded `true`.
- `normalize_binding_transform_options` forwards `strict_null_checks` from the incoming napi options into `DecoratorOptions` (previously dropped).
- Docs (`transform.md`) + tests.

## Behavior

When `emitDecoratorMetadata` is enabled (legacy decorators):

| Source | `strictNullChecks: true` (default) | `strictNullChecks: false` |
| --- | --- | --- |
| `string \| null` | `Object` (matches `tsc` strict) | `String` (matches `tsc --strictNullChecks false` / `babel-plugin-transform-typescript-metadata`) |
| `number \| undefined` | `Object` | `Number` |

Defaults to `true`, so there is **no behavior change for existing users**; only an explicit `strictNullChecks: false` is newly honored.

> Note: this is **not** inferred from `tsconfig.json` *yet*. `oxc_resolver`'s `CompilerOptions` does not currently parse `strictNullChecks`, so (unlike `experimentalDecorators`/`emitDecoratorMetadata`) it must be set explicitly on `transform.decorator`. Documented in `transform.md`. See Follow-up below.

## Follow-up: inferring from `tsconfig.json`

[oxc-project/oxc-resolver#1166](oxc-project/oxc-resolver#1166) (`feat(tsconfig): parse strict and strictNullChecks compiler options`) is the enabling resolver-side change. It adds `strict`/`strictNullChecks` to `CompilerOptions`, inherits them through `extends`, and exposes `effective_strict_null_checks()` implementing tsc's `strictNullChecks ?? strict` precedence.

Once it lands and is released, a small downstream follow-up here can forward `compiler_options.effective_strict_null_checks()` in `merge_transform_options_with_tsconfig` into `DecoratorOptions.strict_null_checks` (mapping `None` to the transformer default), plus add the field to the inline `BindingTsconfigCompilerOptions` path, making `strictNullChecks` auto-inferred from tsconfig like the other decorator options. This PR is intentionally scoped to the explicit option so it's useful immediately and independent of that release.

## Test plan

- [x] Rebased/merged on top of the latest `main` (which already has oxc `0.133.0`); `cargo check --workspace` passes
- [x] New `transform.test.ts` cases assert `string | null` emits `Object` by default and `String` under `strictNullChecks: false` (verified locally; the full suite runs in CI)

## AI usage disclosure

Per the [AI Usage Policy](https://rolldown.rs/contribution-guide#ai-usage-policy): AI assistance (Claude Code) was used to implement and verify this change. I have reviewed and tested it locally.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: shulaoda <165626830+shulaoda@users.noreply.github.com>
Boshen pushed a commit that referenced this pull request May 27, 2026
## 🤖 New release

* `oxc_resolver`: 11.19.2 -> 11.20.0
* `oxc_resolver_napi`: 11.19.2 -> 11.20.0

<details><summary><i><b>Changelog</b></i></summary><p>

## `oxc_resolver`

<blockquote>

##
[11.20.0](v11.19.2...v11.20.0)
- 2026-05-27

### <!-- 0 -->🚀 Features

- *(tsconfig)* parse `strict` and `strictNullChecks` compiler options
([#1166](#1166)) (by
@kylecannon)

### <!-- 1 -->🐛 Bug Fixes

- *(tsconfig)* scope default `**/*` include to the tsconfig directory
([#1161](#1161)) (by
@Boshen)

### Contributors

* @kylecannon
* @Boshen
</blockquote>



</p></details>

---
This PR was generated with
[release-plz](https://github.com/release-plz/release-plz/).

Co-authored-by: oxc-guard[bot] <276638029+oxc-guard[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants