type-inference

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.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "type-inference" with this command: npx skills add biomejs/biome/biomejs-biome-type-inference

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

  • Read crates/biome_js_type_info/CONTRIBUTING.md for architecture details

  • Understand Biome's focus on IDE support and instant updates

  • 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:

// 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>), // Function with parameters Object(Box<Object>), // Object with properties Class(Box<Class>), // Class definition Interface(Box<Interface>), // Interface definition Union(Box<Union>), // Union type (A | B) Intersection(Box<Intersection>), // Intersection type (A & B) Tuple(Box<Tuple>), // Tuple type Literal(Box<Literal>), // Literal type ("foo", 42) Reference(TypeReference), // Reference to another type TypeofExpression(Box<TypeofExpression>), // typeof an expression // ... plus Conditional, Generic, TypeOperator, InstanceOf, // keyword variants (AnyKeyword, NeverKeyword, VoidKeyword, etc.) }

Type References

Instead of direct type references, use TypeReference :

enum TypeReference { Qualifier(Box<TypeReferenceQualifier>), // Name-based reference Resolved(ResolvedTypeId), // Resolved to type ID Import(Box<TypeImportQualifier>), // 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:

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

  1. Module-Level ("Thin") Inference

What: Resolves references within a single module's scope.

Process:

  • Takes results from local inference

  • Looks up qualifiers in local scopes

  • Converts to TypeReference::Resolved if found locally

  • Converts to TypeReference::Import if from import statement

  • Falls back to globals (like Array , Promise )

  • Uses TypeReference::Unknown if nothing found

Where: Implemented in js_module_info/collector.rs

Output: Types with resolved local references, import markers, or unknown

  1. Full Inference

What: Resolves import references across module boundaries.

Process:

  • Has access to entire module graph

  • Resolves TypeReference::Import by following imports

  • Converts to TypeReference::Resolved after following imports

Where: Implemented in js_module_info/module_resolver.rs

Limitation: Results cannot be cached (would become stale on file changes)

Working with Type Resolvers

Available Resolvers

// 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

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

use biome_analyze::Semantic; use biome_js_type_info::{TypeResolver, TypeData};

impl Rule for MyTypeRule { type Query = Semantic<JsCallExpression>;

fn run(ctx: &#x26;RuleContext&#x3C;Self>) -> 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

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

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:

  • No stale data: Module updates don't leave old types in memory

  • Better performance: Types stored in vectors (data locality)

  • Easier debugging: Can inspect all types in vector

  • Simpler algorithms: Process vectors instead of traversing graphs

Trade-off: Must explicitly resolve references (not automatic like Arc )

ResolvedTypeId Structure

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 :

// 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

  • No caching: Full inference results can't be cached (would become stale)

  • Globals: Currently hardcoded, eventually should use TypeScript's .d.ts files

Common Patterns

// 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 */ } }

References

  • Architecture guide: crates/biome_js_type_info/CONTRIBUTING.md

  • Module graph: crates/biome_module_graph/

  • Type resolver trait: crates/biome_js_type_info/src/resolver.rs

  • Flattening: crates/biome_js_type_info/src/flattening.rs

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

prettier-compare

No summary provided by upstream source.

Repository SourceNeeds Review
General

rule-options

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

biome-developer

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

parser-development

No summary provided by upstream source.

Repository SourceNeeds Review