| | |

Biome 24 🧹 Type-Aware Linting — Inference without tsc

⏱️ Estimated read: 11 min | |

Type-aware lint rules have always been the most valuable and the most expensive rules in the JavaScript ecosystem. The no-floating-promises rule catches the unhandled Promise that silently swallows the error, the no-misused-promises catches the Promise passed where the boolean is expected. The typescript-eslint implements these rules by calling the TypeScript compiler’s tsc, and the cost is the separate process, the duplicate parsing, and the slow startup. Biome 2.0 introduced its own type inference engine written in Rust, and the engine is the mechanism that the type-aware rules run without the typescript dependency . The inference is not the full type checker; it is the subset that the lint rules need, and the subset is the reason the rules can run at the native speed.

The architecture is the module graph and the layered inference. The scanner indexes the project’s files, the module graph stores the relationships, and the inference resolves the types in the layers—the local inference, the module-level “thin” inference, and the full cross-module inference . The project domain is the mechanism that enables the scanning and the inference, and the domain is the opt-in because the full scan has the performance cost . The noFloatingPromises rule is the first type-aware rule, and the initial testing shows the roughly 75% of the typescript-eslint detection with the minimal performance impact . The rule is the proof of concept, and the coverage is the ongoing work.

Key point: Biome 2.0’s type inference is the Rust engine that runs the type-aware rules without the typescript dependency. The project domain enables the file scanner and the inference. The noFloatingPromises rule is the first type-aware rule, and the coverage is the roughly 75% of the typescript-eslint. The inference is the subset of the type checker, and the subset is the reason the rules are fast.


Why the inference exists without tsc

The tsc cost problem. The typescript-eslint runs the tsc to get the type information, and the tsc is the separate process with the separate parse, the separate program construction, and the separate type check. The cost is the seconds on the large project, and the cost is the reason the type-aware linting is the slow. The Biome’s inference is the native Rust, and the native is the mechanism that the type-aware rules run in the fraction of the time .

The false-positive problem. The Biome’s goal is not the full type checker; it is the lint rules that do not flag the false positives. The Biome blog states the design: the linter aims to detect the common mistakes, and the common mistakes do not require the 100% correctness. The false positives are the frustration, and the false negatives are the acceptable. The inference is the mechanism that the rules are the conservative, and the conservative is the reason the rules are the usable .

The module-graph problem. The type-aware rules need the cross-file information—the Promise returned from the function in the other file. The Biome 2.0 added the file scanner that indexes the project’s files, and the scanner is the mechanism that the cross-file queries are possible . The scanner is the opt-in because the full scan has the cost, and the project domain is the mechanism that enables it.

The layered-inference problem. The inference is not the single pass. The local inference derives the types from the expressions, the thin inference resolves the references within the module, and the full inference resolves the imports across the modules . The layering is the mechanism that the narrowest layer answers the question, and the narrowest is the performance.

The limitation problem. The inference is the subset, and the subset has the limits—the complex generics, the overloads, and the conditional types may miss the cases. The documentation states the limitation, and the limitation is the reason the rule is the nursery and the coverage is the ongoing. The noFloatingPromises detects the roughly 75% of the typescript-eslint, and the 25% is the work in progress .


a. The project domain and the type-aware rules

The project domain is the mechanism that enables the file scanner and the type inference. The domain is the opt-in, and the linter.domains.project is the configuration.

// biome.json — the project domain
{
  "linter": {
    "domains": {
      "project": "recommended"
    }
  }
}

The project: "recommended" is the mechanism that enables the domain. The domain is the opt-in because the full scan has the cost, and the cost is the reason the domain is not the default . The project domain’s rules are the type-aware, and the rules are the mechanism that the linting catches the errors the single-file rules cannot.

The noFloatingPromises is the first rule. The rule detects the Promise that is not the awaited, the returned, or the explicitly voided.

// The noFloatingPromises detection
async function loadData() {
  // ❌ The floating promise: the result is not handled.
  fetch("/api/data");
}

async function loadData() {
  // ✅ The awaited promise.
  await fetch("/api/data");
}

async function loadData() {
  // ✅ The explicitly voided promise (fire-and-forget).
  void fetch("/api/data");
}

The fetch("/api/data") without the await is the floating promise, and the rule is the mechanism that the diagnostic is the reported . The await fetch is the handled, and the void fetch is the explicit fire-and-forget. The rule is the mechanism that the three cases are distinguished.

The noMisusedPromises is the second rule. The rule detects the Promise passed where the boolean is expected—the if (fetch()) and the array callback that returns the Promise.

// The noMisusedPromises detection
// ❌ The Promise in the conditional.
if (fetch("/api/data")) {
  // ...
}

// ❌ The Promise in the array callback.
[1, 2, 3].forEach(async (n) => {
  await fetch(`/api/data/${n}`);
});

The if (fetch(...)) is the misuse because the Promise is always the truthy, and the forEach(async ...) is the misuse because the forEach does not await the callback . The rule is the mechanism that the misuse is the diagnostic.


b. The inference architecture and the limitations

The inference is the layered. The local inference derives the types from the expressions, and the TypeReference::Qualifier is the unresolved name.

// The type data structure
enum TypeData {
    // The unknown type: the inference is not implemented.
    Unknown,
    // The primitive types.
    String,
    Number,
    Boolean,
    // The function type with the parameters.
    Function(Box<Function>),
    // The object type with the properties.
    Object(Box<Object>),
    // The union type (A | B).
    Union(Box<Union>),
    // The reference to the other type.
    Reference(TypeReference),
}

The TypeData is the mechanism that the type is the tree. The Unknown is the inference not implemented, and the Reference is the mechanism that the module graph constraint is the respected . The TypeReference::Qualifier is the name-based, and the TypeReference::Import is the import-based, and the resolution is the mechanism that the imports are the followed.

The module graph constraint is the critical rule: the no module may copy the data from the other module, and the TypeReference is the mechanism that the stale reference is the avoided . The constraint is the reason the inference is the safe across the file changes.

The limitations are the separate concern. The complex generics, the overloads, and the conditional types may miss the cases. The noFloatingPromises detects the roughly 75% of the typescript-eslint, and the 25% is the ongoing work . The rule is the nursery, and the nursery is the mechanism that the rule is the opt-in.


Complete Example Session

// ============================================
// PART 1: THE PROJECT DOMAIN
// ============================================

// biome.json
{
  "linter": {
    "domains": { "project": "recommended" }
  }
}

// The project domain enables the scanner and the inference.

// ============================================
// PART 2: THE NO FLOATING PROMISES
// ============================================

// The floating promise.
async function loadData() {
  fetch("/api/data"); // ❌ the diagnostic
}

// The awaited promise.
async function loadData() {
  await fetch("/api/data"); // ✅
}

// The explicitly voided promise.
async function loadData() {
  void fetch("/api/data"); // ✅
}

// ============================================
// PART 3: THE NO MISUSED PROMISES
// ============================================

// The Promise in the conditional.
if (fetch("/api/data")) { // ❌ the diagnostic
  // ...
}

// The Promise in the array callback.
[1, 2, 3].forEach(async (n) => { // ❌ the diagnostic
  await fetch(`/api/data/${n}`);
});

// ============================================
// PART 4: THE TYPE DATA
// ============================================

// The TypeData enum is the type tree.
// The Unknown is the inference not implemented.
// The Reference is the module graph reference.

// ============================================
// PART 5: THE MODULE GRAPH CONSTRAINT
// ============================================

// The no module may copy the data from the other module.
// The TypeReference is the mechanism that the stale is the avoided.

// ============================================
// PART 6: THE LIMITATION
// ============================================

// The complex generics, the overloads, and the conditional types
// may miss the cases.
// The noFloatingPromises detects the roughly 75%.

// ============================================
// PART 7: THE NURSERY
// ============================================

// The type-aware rules are in the nursery.
// The nursery rules require the explicit opt-in.

// ============================================
// PART 8: THE PERFORMANCE
// ============================================

// The inference is the native Rust.
// The noFloatingPromises has the minimal performance impact.

// ============================================
// PART 9: THE TSC COMPARISON
// ============================================

// The typescript-eslint calls the tsc.
// The tsc is the separate process and the separate parse.
// The Biome inference is the native and the fast.

// ============================================
// PART 10: THE CHECKLIST
// ============================================

// [ ] The project domain is enabled.
// [ ] The file scanner is the opt-in.
// [ ] The noFloatingPromises is the type-aware rule.
// [ ] The limitation is the complex generics.
// [ ] The rule is the nursery.
// [ ] The inference is the native Rust.

The ten parts covered the project domain, the noFloatingPromises, the noMisusedPromises, the type data, the module graph constraint, the limitation, the nursery, the performance, the tsc comparison, and the checklist. The through-line is that the inference is the native, and the domain is the opt-in.


Quick Reference

The type-aware rules

RuleDetects
noFloatingPromisesThe unhandled Promise
noMisusedPromisesThe Promise in the boolean context
useAwaitThenableThe awaiting non-thenable
noUnnecessaryConditionsThe always-true/false condition

The inference layers

LayerPurpose
Local inferenceThe expressions without the context
Thin inferenceThe references within the module
Full inferenceThe imports across the modules

The limitations

LimitationDetail
The complex genericsMay miss the cases
The overloadsMay miss the cases
The conditional typesMay miss the cases
The coverageThe roughly 75% of the typescript-eslint

The performance

AspectDetail
The implementationThe Rust
The tscThe separate process
The project domainThe opt-in

Best Practices

✅ Do This:

// Enable the project domain
{ "linter": { "domains": { "project": "recommended" } } }         // ✅
// Use the await or the void for the Promise
await fetch("/api/data");                                          // ✅
void fetch("/api/data");                                           // ✅

❌ Don’t Do This:

// Leave the Promise unhandled
fetch("/api/data"); // the floating promise                       // ❌

// Use the Promise in the conditional
if (fetch("/api/data")) { } // the misused promise                // ❌

Common Pitfalls

PitfallWhy It HappensFix
The rule does not fireThe project domain is not enabledEnable the domain
The false negativeThe complex generic is not inferredAccept the limitation or use the tsc
The performance costThe full scan is enabledScope the files.includes

Real-World Examples

1. The project domain

{ "linter": { "domains": { "project": "recommended" } } }

2. The floating promise

fetch("/api/data"); // ❌

3. The awaited promise

await fetch("/api/data"); // ✅

4. The voided promise

void fetch("/api/data"); // ✅

5. The misused promise

if (fetch("/api/data")) { } // ❌

6. The array callback

[1, 2, 3].forEach(async (n) => { await fetch(n); }); // ❌

7. The module graph constraint

// The TypeReference is the mechanism that the stale is the avoided.

8. The coverage

The noFloatingPromises detects the roughly 75%.

9. The limitation

The complex generics may miss the cases.

10. The nursery

The type-aware rules are in the nursery.

Visual

The Inference Layers

┌──────────────────────────────────────────────────────────────┐
│  THE INFERENCE LAYERS                                        │
│                                                              │
│  The local inference                                         │
│  ┌─────────────────────┐                                    │
│  │ The expressions     │  The types without the context     │
│  └──────────┬──────────┘                                    │
│             │                                                │
│             ▼                                                    │
│  ┌─────────────────────┐                                    │
│  │ The thin inference  │  The references within the module  │
│  └──────────┬──────────┘                                    │
│             │                                                │
│             ▼                                                    │
│  ┌─────────────────────┐                                    │
│  │ The full inference  │  The imports across the modules    │
│  └─────────────────────┘                                    │
└──────────────────────────────────────────────────────────────┘

The Project Domain

┌──────────────────────────────────────────────────────────────┐
│  THE PROJECT DOMAIN                                          │
│                                                              │
│  The opt-in                                                  │
│         │                                                    │
│         ▼                                                    │
│  ┌─────────────────────┐                                    │
│  │ The file scanner    │  The project index                 │
│  └──────────┬──────────┘                                    │
│             │                                                │
│             ▼                                                    │
│  ┌─────────────────────┐                                    │
│  │ The type inference  │  The cross-file queries            │
│  └──────────┬──────────┘                                    │
│             │                                                │
│             ▼                                                    │
│  ┌─────────────────────┐                                    │
│  │ The type-aware rule │  The noFloatingPromises            │
│  └─────────────────────┘                                    │
└──────────────────────────────────────────────────────────────┘

The Coverage

┌──────────────────────────────────────────────────────────────┐
│  THE COVERAGE                                                │
│                                                              │
│  The typescript-eslint: the 100% (with the tsc)              │
│  The Biome: the roughly 75% (the native)                     │
│                                                              │
│  The false positive: the not allowed.                        │
│  The false negative: the acceptable.                         │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
The inferenceThe Rust, without the tsc
The domainThe project domain
The scannerThe file scanner, the opt-in
The first ruleThe noFloatingPromises
The coverageThe roughly 75% of the typescript-eslint
The limitationThe complex generics, the overloads
The statusThe nursery

Key takeaways:

  • The type inference is the Rust engine that runs the type-aware rules without the typescript dependency. The engine is the subset of the type checker, and the subset is the reason the rules are fast. The project domain is the opt-in that enables the scanner and the inference .
  • The noFloatingPromises is the first type-aware rule. The rule detects the Promise that is not the awaited, the returned, or the explicitly voided. The initial testing shows the roughly 75% of the typescript-eslint coverage with the minimal performance impact .
  • The inference is the layered. The local inference derives the types from the expressions, the thin inference resolves the references within the module, and the full inference resolves the imports across the modules . The layering is the mechanism that the narrowest layer answers the question.
  • The module graph constraint is the critical rule. The no module may copy the data from the other module, and the TypeReference is the mechanism that the stale reference is the avoided . The constraint is the reason the inference is the safe across the file changes.
  • The limitation is the subset. The complex generics, the overloads, and the conditional types may miss the cases, and the coverage is the ongoing work. The rule is the nursery, and the nursery is the mechanism that the rule is the opt-in .

Remember: The Biome type-aware linting is the inference without the tsc. The project domain is the opt-in, and the noFloatingPromises is the first rule. The inference is the layered, and the coverage is the roughly 75%. The limitation is the subset, and the rule is the nursery.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!