--- id: biomejs/biome/type-inference version: "db55f85e" license: Apache-2.0 install: manual updated: 2026-07-27 --- # type-inference — This skill teaches you how to work with Biome's type inference engine and module graph architecture for building type-aware lint rules. It covers the three-phase type resolution process—local inference, module-level resolution, and cross-module full inference—plus the TypeReference pattern that prevents stale data in an IDE environment. Use it when implementing new type-aware rules, understanding type resolution, or working on type inference features. Publisher: biomejs · Stars: 25418 · Updated: 2026-07-27 Install (manual): `git clone https://github.com/biomejs/biome` ## SKILL.md ## Purpose Use this skill when working with Biome's type inference system and module graph. Covers type references, resolution phases, and the architecture designed for IDE performance. ## Prerequisites 1. Read `crates/biome_js_type_info/CONTRIBUTING.md` for architecture details 2. Understand Biome's focus on IDE support and instant updates 3. Familiarity with TypeScript type system concepts ## Key Concepts ### Module Graph Constraint **Critical rule**: No module may copy or clone data from another module, not even behind `Arc`. **Why**: Any module can be updated at any time (IDE file changes). Copying data would create stale references that are hard to invalidate. **Solution**: Use `TypeReference` instead of direct type references. ### Type Data Structure Types are stored in `TypeData` enum with many variants: ```rust // Simplified — see crates/biome_js_type_info/src/type_data.rs for the full enum enum TypeData { Unknown, // Inference not implemented Global, // Global type reference BigInt, Boolean, Null, Number, // Primitive types String, Symbol, Undefined, Function(Box), // Function with parameters Object(Box), // Object with properties Class(Box), // Class definition Interface(Box), // Interface definition Union(Box), // Union type (A | B) Intersection(Box), // Intersection type (A & B) Tuple(Box), // Tuple type Literal(Box), // Literal type ("foo", 42) Reference(TypeReference), // Reference to another type TypeofExpression(Box), // typeof an expression // ... plus Conditional, Generic, TypeOperator, InstanceOf, // keyword variants (AnyKeyword, NeverKeyword, VoidKeyword, etc.) } ``` ### Type References Instead of direct type references, use `TypeReference`: ```rust enum TypeReference { Qualifier(Box), // Name-based reference Resolved(ResolvedTypeId), // Resolved to type ID Import(Box), // Import reference } ``` **Note:** There is no `Unknown` variant. Unknown types are represented as `TypeReference::Resolved(GLOBAL_UNKNOWN_ID)`. Use `TypeReference::unknown()` to create one. ## Type Resolution Phases ### 1. Local Inference **What**: Derives types from expressions without surrounding context. **Example**: For `a + b`, creates: ```rust TypeData::TypeofExpression(TypeofExpression::Addition { left: TypeReference::from(TypeReferenceQualifier::from_name("a")), right: TypeReference::from(TypeReferenceQualifier::from_name("b")) }) ``` **Where**: Implemented in `local_inference.rs` **Output**: Types with unresolved `TypeReference::Qualifier` references ### 2. Module-Level ("Thin") Inference **What**: Resolves references within a single module's scope. **Process**: 1. Takes results from local inference 2. Looks up qualifiers in local scopes 3. Converts to `TypeReference::Resolved` if found locally 4. Converts to `TypeReference::Import` if from import statement 5. Falls back to globals (like `Array`, `Promise`) 6. Uses `TypeReference::unknown()` if nothing is found **Where**: Implemented in `js_module_info/collector.rs` **Output**: Types with resolved local references, import markers, or unknown ### 3. Full Inference **What**: Resolves import references across module boundaries. **Process**: 1. Has access to entire module graph 2. Resolves `TypeReference::Import` by following imports 3. Converts to `TypeReference::Resolved` after following imports **Where**: The Salsa-backed implementation starts at `db/queries/type_inference.rs::infer_module_types` and uses helpers under `db/type_inference/`. `js_module_info/module_resolver.rs` contains the legacy `TypeResolver`-based path. **Caching**: `infer_module_types` is tracked by Salsa. Imported module results are dependencies, so Salsa invalidates affected importers after a change. ## Working with Type Resolvers ### Available Resolvers ```rust // 1. For tests HardcodedSymbolResolver // 2. For globals (Array, Promise, etc.) GlobalsResolver // 3. For thin inference (single module) JsModuleInfoCollector // 4. For full inference (across modules) ModuleResolver ``` ### Using a Resolver ```rust use biome_js_type_info::{TypeResolver, ResolvedTypeData}; fn analyze_type(resolver: &impl TypeResolver, type_ref: TypeReference) { // Resolve the reference let resolved_data: ResolvedTypeData = resolver.resolve_type(type_ref); // Get raw data for pattern matching match resolved_data.as_raw_data() { TypeData::String => { /* handle string */ }, TypeData::Number => { /* handle number */ }, TypeData::Function(func) => { /* handle function */ }, _ => { /* handle others */ } } // Resolve nested references if let TypeData::Reference(inner_ref) = resolved_data.as_raw_data() { let inner_data = resolver.resolve_type(*inner_ref); // Process inner type } } ``` ### Type Flattening **What**: Converts complex type expressions to concrete types. **Example**: After resolving `a + b`: - If both are `TypeData::Number` → Flatten to `TypeData::Number` - Otherwise → Usually flatten to `TypeData::String` **Where**: Implemented in `flattening.rs` ## Common Workflows ### Implement Type-Aware Lint Rule ```rust use biome_analyze::Semantic; use biome_js_type_info::{TypeResolver, TypeData}; impl Rule for MyTypeRule { type Query = Semantic; fn run(ctx: &RuleContext) -> Self::Signals { let node = ctx.query(); let model = ctx.model(); // Get type resolver from model let resolver = model.type_resolver(); // Get type of expression let expr_type = node.callee().ok()?.infer_type(resolver); // Check the type match expr_type.as_raw_data() { TypeData::Function(_) => { /* valid */ }, TypeData::Unknown => { /* might be valid, can't tell */ }, _ => { return Some(()); /* not callable */ } } None } } ``` ### Navigate Type References ```rust fn is_string_type(resolver: &impl TypeResolver, type_ref: TypeReference) -> bool { let resolved = resolver.resolve_type(type_ref); // Follow references let data = match resolved.as_raw_data() { TypeData::Reference(ref_to) => resolver.resolve_type(*ref_to), _other => resolved, }; // Check the resolved type matches!(data.as_raw_data(), TypeData::String) } ``` ### Work with Function Types ```rust fn analyze_function(resolver: &impl TypeResolver, type_ref: TypeReference) { let resolved = resolver.resolve_type(type_ref); if let TypeData::Function(func_type) = resolved.as_raw_data() { // Access parameters for param in func_type.parameters() { let param_type = resolver.resolve_type(param.type_ref()); // Analyze parameter type } // Access return type let return_type = resolver.resolve_type(func_type.return_type()); } } ``` ## Architecture Principles ### Why Type References? **Advantages**: 1. **No stale data**: Module updates don't leave old types in memory 2. **Better performance**: Types stored in vectors (data locality) 3. **Easier debugging**: Can inspect all types in vector 4. **Simpler algorithms**: Process vectors instead of traversing graphs **Trade-off**: Must explicitly resolve references (not automatic like `Arc`) ### ResolvedTypeId Structure ```rust struct ResolvedTypeId(ResolverId, TypeId) ``` - `TypeId` (u32): Index into a type vector - `ResolverId` (u32): Identifies which vector to use - Total: 64 bits (compact representation) ### ResolvedTypeData Always work with `ResolvedTypeData` from resolver, not raw `&TypeData`: ```rust // Good - tracks resolver context let resolved_data: ResolvedTypeData = resolver.resolve_type(type_ref); // Be careful - loses resolver context let raw_data: &TypeData = resolved_data.as_raw_data(); // Can't resolve nested TypeReferences without ResolverId! ``` ## Tips - **Unknown types**: `TypeData::Unknown` means inference not implemented, treat as "could be anything" - **Follow references**: Always follow `TypeData::Reference` to get actual type - **Resolver context**: Keep `ResolvedTypeData` when possible, don't extract raw `TypeData` early - **Performance**: Type vectors are fast - iterate directly instead of recursive traversal - **IDE focus**: All design decisions prioritize instant IDE updates over CLI performance - **Caching**: Salsa memoizes full-inference query results and invalidates them through tracked module dependencies - **Globals**: Currently hardcoded, eventually should use TypeScript's `.d.ts` files ## Common Patterns ```rust // Pattern 1: Resolve and flatten let type_ref = expr.infer_type(resolver); let flattened = type_ref.flatten(resolver); // Pattern 2: Check if type matches fn is_string_type(resolver: &impl TypeResolver, type_ref: TypeReference) -> bool { let resolved = resolver.resolve_type(type_ref); matches!(resolved.as_raw_data(), TypeData::String) } // Pattern 3: Handle unknown gracefully match resolved.as_raw_data() { TypeData::Unknown | TypeData::UnknownKeyword => { // Can't verify, assume valid return None; } TypeData::String => { /* handle */ } _ => { /* handle */ } } ``` ## CSS and HTML Module Graph The module graph tracks not only JS imports/exports but also CSS class names and HTML class references, used by cross-file lint rules like `noUnusedStyles` and `noUndeclaredStyles`. ### Key Types ``` CssModuleInfo — classes: IndexSet HtmlModuleInfo — style_classes: IndexSet (from