Skip to main content

tsz_solver/narrowing/
mod.rs

1//! Type narrowing for discriminated unions and type guards.
2//!
3//! Discriminated unions are unions where each member has a common "discriminant"
4//! property with a literal type that uniquely identifies that member.
5//!
6//! Example:
7//! ```typescript
8//! type Action =
9//!   | { type: "add", value: number }
10//!   | { type: "remove", id: string }
11//!   | { type: "clear" };
12//!
13//! function handle(action: Action) {
14//!   if (action.type === "add") {
15//!     // action is narrowed to { type: "add", value: number }
16//!   }
17//! }
18//! ```
19//!
20//! ## `TypeGuard` Abstraction
21//!
22//! The `TypeGuard` enum provides an AST-agnostic representation of narrowing
23//! conditions. This allows the Solver to perform pure type algebra without
24//! depending on AST nodes.
25//!
26//! Architecture:
27//! - **Checker**: Extracts `TypeGuard` from AST nodes (WHERE)
28//! - **Solver**: Applies `TypeGuard` to types (WHAT)
29
30mod compound;
31mod discriminants;
32mod instanceof;
33mod property;
34pub(crate) mod utils;
35
36// Re-export utility functions from the utils submodule
37pub use utils::{
38    find_discriminants, is_definitely_nullish, is_nullish_type, narrow_by_discriminant,
39    narrow_by_typeof, remove_definitely_falsy_types, remove_nullish, split_nullish_type,
40    type_contains_undefined,
41};
42
43use crate::relations::subtype::TypeResolver;
44use crate::type_queries::{UnionMembersKind, classify_for_union_members};
45#[cfg(test)]
46use crate::types::*;
47use crate::types::{FunctionShape, LiteralValue, ParamInfo, TypeData, TypeId};
48use crate::utils::{TypeIdExt, union_or_single};
49use crate::visitor::{
50    index_access_parts, intersection_list_id, is_function_type_db, is_object_like_type_db,
51    lazy_def_id, literal_value, object_shape_id, object_with_index_shape_id, template_literal_id,
52    type_param_info, union_list_id,
53};
54use crate::{QueryDatabase, TypeDatabase};
55use rustc_hash::FxHashMap;
56use std::cell::RefCell;
57use tracing::{Level, span, trace};
58use tsz_common::interner::Atom;
59
60/// The result of a `typeof` expression, restricted to the 8 standard JavaScript types.
61///
62/// Using an enum instead of `String` eliminates heap allocation per typeof guard.
63/// TypeScript's `typeof` operator only returns these 8 values.
64#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
65pub enum TypeofKind {
66    String,
67    Number,
68    Boolean,
69    BigInt,
70    Symbol,
71    Undefined,
72    Object,
73    Function,
74}
75
76impl TypeofKind {
77    /// Parse a typeof result string into a `TypeofKind`.
78    /// Returns None for non-standard typeof strings (which don't narrow).
79    pub fn parse(s: &str) -> Option<Self> {
80        match s {
81            "string" => Some(Self::String),
82            "number" => Some(Self::Number),
83            "boolean" => Some(Self::Boolean),
84            "bigint" => Some(Self::BigInt),
85            "symbol" => Some(Self::Symbol),
86            "undefined" => Some(Self::Undefined),
87            "object" => Some(Self::Object),
88            "function" => Some(Self::Function),
89            _ => None,
90        }
91    }
92
93    /// Get the string representation of this typeof kind.
94    pub const fn as_str(&self) -> &'static str {
95        match self {
96            Self::String => "string",
97            Self::Number => "number",
98            Self::Boolean => "boolean",
99            Self::BigInt => "bigint",
100            Self::Symbol => "symbol",
101            Self::Undefined => "undefined",
102            Self::Object => "object",
103            Self::Function => "function",
104        }
105    }
106}
107
108/// AST-agnostic representation of a type narrowing condition.
109///
110/// This enum represents various guards that can narrow a type, without
111/// depending on AST nodes like `NodeIndex` or `SyntaxKind`.
112///
113/// # Examples
114/// ```typescript
115/// typeof x === "string"     -> TypeGuard::Typeof(TypeofKind::String)
116/// x instanceof MyClass      -> TypeGuard::Instanceof(MyClass_type)
117/// x === null                -> TypeGuard::NullishEquality
118/// x                         -> TypeGuard::Truthy
119/// x.kind === "circle"       -> TypeGuard::Discriminant { property: "kind", value: "circle" }
120/// ```
121#[derive(Clone, Debug, PartialEq)]
122pub enum TypeGuard {
123    /// `typeof x === "typename"`
124    ///
125    /// Narrows a union to only members matching the typeof result.
126    /// For example, narrowing `string | number` with `Typeof(TypeofKind::String)` yields `string`.
127    Typeof(TypeofKind),
128
129    /// `x instanceof Class`
130    ///
131    /// Narrows to the class type or its subtypes.
132    Instanceof(TypeId),
133
134    /// `x === literal` or `x !== literal`
135    ///
136    /// Narrows to exactly that literal type (for equality) or excludes it (for inequality).
137    LiteralEquality(TypeId),
138
139    /// `x == null` or `x != null` (checks both null and undefined)
140    ///
141    /// JavaScript/TypeScript treats `== null` as matching both `null` and `undefined`.
142    NullishEquality,
143
144    /// `x` (truthiness check in a conditional)
145    ///
146    /// Removes falsy types from a union: `null`, `undefined`, `false`, `0`, `""`, `NaN`.
147    Truthy,
148
149    /// `x.prop === literal` or `x.payload.type === "value"` (Discriminated Union narrowing)
150    ///
151    /// Narrows a union of object types based on a discriminant property.
152    ///
153    /// # Examples
154    /// - Top-level: `{ kind: "A" } | { kind: "B" }` with `path: ["kind"]` yields `{ kind: "A" }`
155    /// - Nested: `{ payload: { type: "user" } } | { payload: { type: "product" } }`
156    ///   with `path: ["payload", "type"]` yields `{ payload: { type: "user" } }`
157    Discriminant {
158        /// Property path from base to discriminant (e.g., ["payload", "type"])
159        property_path: Vec<Atom>,
160        /// The literal value to match against
161        value_type: TypeId,
162    },
163
164    /// `prop in x`
165    ///
166    /// Narrows to types that have the specified property.
167    InProperty(Atom),
168
169    /// `x is T` or `asserts x is T` (User-Defined Type Guard)
170    ///
171    /// Narrows a type based on a user-defined type predicate function.
172    ///
173    /// # Examples
174    /// ```typescript
175    /// function isString(x: any): x is string { ... }
176    /// function assertDefined(x: any): asserts x is Date { ... }
177    ///
178    /// if (isString(x)) { x; // string }
179    /// assertDefined(x); x; // Date
180    /// ```
181    ///
182    /// - `type_id: Some(T)`: The type to narrow to (e.g., `string` or `Date`)
183    /// - `type_id: None`: Truthiness assertion (`asserts x`), behaves like `Truthy`
184    /// - `asserts: true`: This is an assertion (throws if false), affects control flow
185    Predicate {
186        type_id: Option<TypeId>,
187        asserts: bool,
188    },
189
190    /// `Array.isArray(x)`
191    ///
192    /// Narrows a type to only array-like types (arrays, tuples, readonly arrays).
193    ///
194    /// # Examples
195    /// ```typescript
196    /// function process(x: string[] | number | { length: number }) {
197    ///   if (Array.isArray(x)) {
198    ///     x; // string[] (not number or the object)
199    ///   }
200    /// }
201    /// ```
202    ///
203    /// This preserves element types - `string[] | number[]` stays as `string[] | number[]`,
204    /// it doesn't collapse to `any[]`.
205    Array,
206
207    /// `array.every(predicate)` where predicate has type predicate
208    ///
209    /// Narrows an array's element type based on a type predicate.
210    ///
211    /// # Examples
212    /// ```typescript
213    /// const arr: (number | string)[] = ['aaa'];
214    /// const isString = (x: unknown): x is string => typeof x === 'string';
215    /// if (arr.every(isString)) {
216    ///   arr; // string[] (element type narrowed from number | string to string)
217    /// }
218    /// ```
219    ///
220    /// This only applies to arrays. For non-array types, the type is unchanged.
221    ArrayElementPredicate {
222        /// The type to narrow array elements to
223        element_type: TypeId,
224    },
225}
226
227#[inline]
228pub(crate) fn union_or_single_preserve(db: &dyn TypeDatabase, types: Vec<TypeId>) -> TypeId {
229    match types.len() {
230        0 => TypeId::NEVER,
231        1 => types[0],
232        _ => db.union_from_sorted_vec(types),
233    }
234}
235
236/// Result of a narrowing operation.
237///
238/// Represents the types in both branches of a condition.
239#[derive(Clone, Debug)]
240pub struct NarrowingResult {
241    /// The type in the "true" branch of the condition
242    pub true_type: TypeId,
243    /// The type in the "false" branch of the condition
244    pub false_type: TypeId,
245}
246
247/// Result of finding discriminant properties in a union.
248#[derive(Clone, Debug)]
249pub struct DiscriminantInfo {
250    /// The name of the discriminant property
251    pub property_name: Atom,
252    /// Map from literal value to the union member type
253    pub variants: Vec<(TypeId, TypeId)>, // (literal_type, member_type)
254}
255
256/// Narrowing context for type guards and control flow analysis.
257/// Shared across multiple narrowing contexts to persist resolution results.
258#[derive(Default, Clone, Debug)]
259pub struct NarrowingCache {
260    /// Cache for type resolution (Lazy/App/Template -> Structural)
261    pub resolve_cache: RefCell<FxHashMap<TypeId, TypeId>>,
262    /// Cache for top-level property type lookups (TypeId, `PropName`) -> `PropType`
263    pub property_cache: RefCell<FxHashMap<(TypeId, Atom), Option<TypeId>>>,
264}
265
266impl NarrowingCache {
267    pub fn new() -> Self {
268        Self::default()
269    }
270}
271
272/// Narrowing context for type guards and control flow analysis.
273pub struct NarrowingContext<'a> {
274    pub(crate) db: &'a dyn QueryDatabase,
275    /// Optional `TypeResolver` for resolving Lazy types (e.g., type aliases).
276    /// When present, this enables proper narrowing of type aliases like `type Shape = Circle | Square`.
277    pub(crate) resolver: Option<&'a dyn TypeResolver>,
278    /// Cache for narrowing operations.
279    /// If provided, uses the shared cache; otherwise uses a local ephemeral cache.
280    pub(crate) cache: std::borrow::Cow<'a, NarrowingCache>,
281}
282
283impl<'a> NarrowingContext<'a> {
284    pub fn new(db: &'a dyn QueryDatabase) -> Self {
285        NarrowingContext {
286            db,
287            resolver: None,
288            cache: std::borrow::Cow::Owned(NarrowingCache::new()),
289        }
290    }
291
292    /// Create a new context with a shared cache.
293    pub fn with_cache(db: &'a dyn QueryDatabase, cache: &'a NarrowingCache) -> Self {
294        NarrowingContext {
295            db,
296            resolver: None,
297            cache: std::borrow::Cow::Borrowed(cache),
298        }
299    }
300
301    /// Set the `TypeResolver` for this context.
302    ///
303    /// This enables proper resolution of Lazy types (type aliases) during narrowing.
304    /// The resolver should be borrowed from the Checker's `TypeEnvironment`.
305    pub fn with_resolver(mut self, resolver: &'a dyn TypeResolver) -> Self {
306        self.resolver = Some(resolver);
307        self
308    }
309
310    /// Resolve a type to its structural representation.
311    ///
312    /// Unwraps:
313    /// - Lazy types (evaluates them using resolver if available, otherwise falls back to db)
314    /// - Application types (evaluates the generic instantiation)
315    ///
316    /// This ensures that type aliases, interfaces, and generics are resolved
317    /// to their actual structural types before performing narrowing operations.
318    pub(crate) fn resolve_type(&self, type_id: TypeId) -> TypeId {
319        if let Some(&cached) = self.cache.resolve_cache.borrow().get(&type_id) {
320            return cached;
321        }
322
323        let result = self.resolve_type_uncached(type_id);
324        self.cache
325            .resolve_cache
326            .borrow_mut()
327            .insert(type_id, result);
328        result
329    }
330
331    fn resolve_type_uncached(&self, mut type_id: TypeId) -> TypeId {
332        // Prevent infinite loops with a fuel counter
333        let mut fuel = 100;
334
335        while fuel > 0 {
336            fuel -= 1;
337
338            // 1. Handle Lazy types (DefId-based, not SymbolRef)
339            // If we have a TypeResolver, try to resolve Lazy types through it first
340            if let Some(def_id) = lazy_def_id(self.db, type_id) {
341                if let Some(resolver) = self.resolver
342                    && let Some(resolved) =
343                        resolver.resolve_lazy(def_id, self.db.as_type_database())
344                {
345                    type_id = resolved;
346                    continue;
347                }
348                // Fallback to database evaluation if no resolver or resolution failed
349                type_id = self.db.evaluate_type(type_id);
350                continue;
351            }
352
353            // 2. Handle Application types (Generics)
354            // CRITICAL: When a resolver is available (from the checker's TypeEnvironment),
355            // use it to resolve the Application's base type and instantiate with args.
356            // Without the resolver, generic type aliases like `Box<number>` can't resolve
357            // their DefId-based base types, causing narrowing to fail on discriminated
358            // unions wrapped in generics.
359            if let Some(TypeData::Application(app_id)) = self.db.lookup(type_id) {
360                if let Some(resolver) = self.resolver {
361                    let app = self.db.type_application(app_id);
362                    // Try to resolve the base type's DefId and instantiate manually
363                    if let Some(def_id) = lazy_def_id(self.db, app.base) {
364                        let resolved_body =
365                            resolver.resolve_lazy(def_id, self.db.as_type_database());
366                        let type_params = resolver.get_lazy_type_params(def_id);
367                        if let (Some(body), Some(params)) = (resolved_body, type_params) {
368                            let instantiated =
369                                crate::instantiation::instantiate::instantiate_generic(
370                                    self.db.as_type_database(),
371                                    body,
372                                    &params,
373                                    &app.args,
374                                );
375                            type_id = instantiated;
376                            continue;
377                        }
378                    }
379                }
380                // Fallback: use db.evaluate_type (works when resolver isn't needed)
381                type_id = self.db.evaluate_type(type_id);
382                continue;
383            }
384
385            // 3. Handle TemplateLiteral types that can be fully evaluated to string literals.
386            // Template literal spans may contain Lazy(DefId) types (e.g., `${EnumType.Member}`)
387            // that must be resolved before evaluation. We resolve all lazy spans first,
388            // rebuild the template literal, then let the evaluator handle it.
389            if let Some(TypeData::TemplateLiteral(spans_id)) = self.db.lookup(type_id) {
390                use crate::types::TemplateSpan;
391                let spans = self.db.template_list(spans_id);
392                let mut new_spans = Vec::with_capacity(spans.len());
393                let mut changed = false;
394                for span in spans.iter() {
395                    match span {
396                        TemplateSpan::Type(inner_id) => {
397                            let resolved = self.resolve_type(*inner_id);
398                            if resolved != *inner_id {
399                                changed = true;
400                            }
401                            new_spans.push(TemplateSpan::Type(resolved));
402                        }
403                        other => new_spans.push(other.clone()),
404                    }
405                }
406                let eval_input = if changed {
407                    self.db.template_literal(new_spans)
408                } else {
409                    type_id
410                };
411                let evaluated = self.db.evaluate_type(eval_input);
412                if evaluated != type_id {
413                    type_id = evaluated;
414                    continue;
415                }
416            }
417
418            // It's a structural type (Object, Union, Intersection, Primitive)
419            break;
420        }
421
422        type_id
423    }
424
425    /// Narrow a type based on a typeof check.
426    ///
427    /// Example: `typeof x === "string"` narrows `string | number` to `string`
428    pub fn narrow_by_typeof(&self, source_type: TypeId, typeof_result: &str) -> TypeId {
429        let _span =
430            span!(Level::TRACE, "narrow_by_typeof", source_type = source_type.0, %typeof_result)
431                .entered();
432
433        // CRITICAL FIX: Narrow `any` for typeof checks
434        // TypeScript narrows `any` for typeof/instanceof/Array.isArray/user-defined guards
435        // But NOT for equality/truthiness/in operator
436        if source_type == TypeId::UNKNOWN || source_type == TypeId::ANY {
437            return match typeof_result {
438                "string" => TypeId::STRING,
439                "number" => TypeId::NUMBER,
440                "boolean" => TypeId::BOOLEAN,
441                "bigint" => TypeId::BIGINT,
442                "symbol" => TypeId::SYMBOL,
443                "undefined" => TypeId::UNDEFINED,
444                "object" => self.db.union2(TypeId::OBJECT, TypeId::NULL),
445                "function" => self.function_type(),
446                _ => source_type,
447            };
448        }
449
450        let target_type = match typeof_result {
451            "string" => TypeId::STRING,
452            "number" => TypeId::NUMBER,
453            "boolean" => TypeId::BOOLEAN,
454            "bigint" => TypeId::BIGINT,
455            "symbol" => TypeId::SYMBOL,
456            "undefined" => TypeId::UNDEFINED,
457            "object" => TypeId::OBJECT, // includes null
458            "function" => return self.narrow_to_function(source_type),
459            _ => return source_type,
460        };
461
462        self.narrow_to_type(source_type, target_type)
463    }
464
465    /// Narrow a type to include only members assignable to target.
466    pub fn narrow_to_type(&self, source_type: TypeId, target_type: TypeId) -> TypeId {
467        let _span = span!(
468            Level::TRACE,
469            "narrow_to_type",
470            source_type = source_type.0,
471            target_type = target_type.0
472        )
473        .entered();
474
475        // CRITICAL FIX: Resolve Lazy/Ref types to inspect their structure.
476        // This fixes the "Missing type resolution" bug where type aliases and
477        // generics weren't being narrowed correctly.
478        let resolved_source = self.resolve_type(source_type);
479
480        // Gracefully handle resolution failures: if evaluation fails but the input
481        // wasn't ERROR, we can't narrow structurally. Return original source to
482        // avoid cascading ERRORs through the type system.
483        if resolved_source == TypeId::ERROR && source_type != TypeId::ERROR {
484            trace!("Source type resolution failed, returning original source");
485            return source_type;
486        }
487
488        // Resolve target for consistency
489        let resolved_target = self.resolve_type(target_type);
490        if resolved_target == TypeId::ERROR && target_type != TypeId::ERROR {
491            trace!("Target type resolution failed, returning original source");
492            return source_type;
493        }
494
495        // If source is the target, return it
496        if resolved_source == resolved_target {
497            trace!("Source type equals target type, returning unchanged");
498            return source_type;
499        }
500
501        // Special case: unknown can be narrowed to any type through type guards
502        // This handles cases like: if (typeof x === "string") where x: unknown
503        if resolved_source == TypeId::UNKNOWN {
504            trace!("Narrowing unknown to specific type via type guard");
505            return target_type;
506        }
507
508        // Special case: any can be narrowed to any type through type guards
509        // This handles cases like: if (x === null) where x: any
510        // CRITICAL: Unlike unknown, any MUST be narrowed to match target type
511        if resolved_source == TypeId::ANY {
512            trace!("Narrowing any to specific type via type guard");
513            return target_type;
514        }
515
516        // If source is a union, filter members
517        // Use resolved_source for structural inspection
518        if let Some(members) = union_list_id(self.db, resolved_source) {
519            let members = self.db.type_list(members);
520            trace!(
521                "Narrowing union with {} members to type {}",
522                members.len(),
523                target_type.0
524            );
525            let matching: Vec<TypeId> = members
526                .iter()
527                .filter_map(|&member| {
528                    if let Some(narrowed) = self.narrow_type_param(member, target_type) {
529                        return Some(narrowed);
530                    }
531                    if self.is_assignable_to(member, target_type) {
532                        return Some(member);
533                    }
534                    // CRITICAL FIX: Check if target_type is a subtype of member
535                    // This handles cases like narrowing string | number by "hello"
536                    // where "hello" is a subtype of string, so we should narrow to "hello"
537                    if crate::relations::subtype::is_subtype_of_with_db(self.db, target_type, member) {
538                        return Some(target_type);
539                    }
540                    // CRITICAL FIX: instanceof Array matching
541                    // When narrowing by `instanceof Array`, if the member is array-like and target
542                    // is a Lazy/Application type (which includes Array<T> interface references),
543                    // assume it's the global Array and match the member.
544                    // This handles: `x: Message | Message[]` with `instanceof Array` should keep `Message[]`.
545                    // At runtime, instanceof only checks prototype chain, not generic type arguments.
546                    if self.is_array_like(member) {
547                        use crate::type_queries;
548                        // Check if target is a type reference or generic application (Array<T>)
549                        let is_target_lazy_or_app = type_queries::is_type_reference(self.db, resolved_target)
550                            || type_queries::is_generic_type(self.db, resolved_target);
551
552                        trace!("Member is array-like: member={}, target={}, is_target_lazy_or_app={}",
553                            member.0, resolved_target.0, is_target_lazy_or_app);
554
555                        if is_target_lazy_or_app {
556                            trace!("Array member with lazy/app target (likely Array interface), keeping member");
557                            return Some(member);
558                        }
559                    }
560                    None
561                })
562                .collect();
563
564            if matching.is_empty() {
565                trace!("No matching members found, returning NEVER");
566                return TypeId::NEVER;
567            } else if matching.len() == 1 {
568                trace!("Found single matching member, returning {}", matching[0].0);
569                return matching[0];
570            }
571            trace!(
572                "Found {} matching members, creating new union",
573                matching.len()
574            );
575            return self.db.union(matching);
576        }
577
578        // Check if this is a type parameter that needs narrowing
579        // Use resolved_source to handle type parameters behind aliases
580        if let Some(narrowed) = self.narrow_type_param(resolved_source, target_type) {
581            trace!("Narrowed type parameter to {}", narrowed.0);
582            return narrowed;
583        }
584
585        // Task 13: Handle boolean -> literal narrowing
586        // When narrowing boolean to true or false, return the corresponding literal
587        if resolved_source == TypeId::BOOLEAN {
588            let is_target_true = if let Some(lit) = literal_value(self.db, resolved_target) {
589                matches!(lit, LiteralValue::Boolean(true))
590            } else {
591                resolved_target == TypeId::BOOLEAN_TRUE
592            };
593
594            if is_target_true {
595                trace!("Narrowing boolean to true");
596                return TypeId::BOOLEAN_TRUE;
597            }
598
599            let is_target_false = if let Some(lit) = literal_value(self.db, resolved_target) {
600                matches!(lit, LiteralValue::Boolean(false))
601            } else {
602                resolved_target == TypeId::BOOLEAN_FALSE
603            };
604
605            if is_target_false {
606                trace!("Narrowing boolean to false");
607                return TypeId::BOOLEAN_FALSE;
608            }
609        }
610
611        // Check if source is assignable to target using resolved types for comparison
612        if self.is_assignable_to(resolved_source, resolved_target) {
613            trace!("Source type is assignable to target, returning source");
614            source_type
615        } else if crate::relations::subtype::is_subtype_of_with_db(
616            self.db,
617            resolved_target,
618            resolved_source,
619        ) {
620            // CRITICAL FIX: Check if target is a subtype of source (reverse narrowing)
621            // This handles cases like narrowing string to "hello" where "hello" is a subtype of string
622            // The inference engine uses this to narrow upper bounds by lower bounds
623            trace!("Target is subtype of source, returning target");
624            target_type
625        } else {
626            trace!("Source type is not assignable to target, returning NEVER");
627            TypeId::NEVER
628        }
629    }
630
631    /// Check if a literal type is assignable to a target for narrowing purposes.
632    ///
633    /// Handles union decomposition: if the target is a union, checks each member.
634    /// Falls back to `narrow_to_type` to determine if the literal can narrow to the target.
635    pub fn literal_assignable_to(&self, literal: TypeId, target: TypeId) -> bool {
636        if literal == target || target == TypeId::ANY || target == TypeId::UNKNOWN {
637            return true;
638        }
639
640        if let UnionMembersKind::Union(members) = classify_for_union_members(self.db, target) {
641            return members
642                .iter()
643                .any(|&member| self.literal_assignable_to(literal, member));
644        }
645
646        self.narrow_to_type(literal, target) != TypeId::NEVER
647    }
648
649    /// Narrow a type to exclude members assignable to target.
650    pub fn narrow_excluding_type(&self, source_type: TypeId, excluded_type: TypeId) -> TypeId {
651        if let Some(members) = intersection_list_id(self.db, source_type) {
652            let members = self.db.type_list(members);
653            let mut narrowed_members = Vec::with_capacity(members.len());
654            let mut changed = false;
655            for &member in members.iter() {
656                let narrowed = self.narrow_excluding_type(member, excluded_type);
657                if narrowed == TypeId::NEVER {
658                    return TypeId::NEVER;
659                }
660                if narrowed != member {
661                    changed = true;
662                }
663                narrowed_members.push(narrowed);
664            }
665            if !changed {
666                return source_type;
667            }
668            return self.db.intersection(narrowed_members);
669        }
670
671        // If source is a union, filter out matching members
672        if let Some(members) = union_list_id(self.db, source_type) {
673            let members = self.db.type_list(members);
674            let remaining: Vec<TypeId> = members
675                .iter()
676                .filter_map(|&member| {
677                    if intersection_list_id(self.db, member).is_some() {
678                        return self
679                            .narrow_excluding_type(member, excluded_type)
680                            .non_never();
681                    }
682                    if let Some(narrowed) = self.narrow_type_param_excluding(member, excluded_type)
683                    {
684                        return narrowed.non_never();
685                    }
686                    if self.is_assignable_to(member, excluded_type) {
687                        None
688                    } else {
689                        Some(member)
690                    }
691                })
692                .collect();
693
694            tracing::trace!(
695                remaining_count = remaining.len(),
696                remaining = ?remaining.iter().map(|t| t.0).collect::<Vec<_>>(),
697                "narrow_excluding_type: union filter result"
698            );
699            if remaining.is_empty() {
700                return TypeId::NEVER;
701            } else if remaining.len() == 1 {
702                return remaining[0];
703            }
704            return self.db.union(remaining);
705        }
706
707        if let Some(narrowed) = self.narrow_type_param_excluding(source_type, excluded_type) {
708            return narrowed;
709        }
710
711        // Special case: boolean type (treat as true | false union)
712        // Task 13: Fix Boolean Narrowing Logic
713        // When excluding true or false from boolean, return the other literal
714        // When excluding both true and false from boolean, return never
715        if source_type == TypeId::BOOLEAN
716            || source_type == TypeId::BOOLEAN_TRUE
717            || source_type == TypeId::BOOLEAN_FALSE
718        {
719            // Check if excluded_type is a boolean literal
720            let is_excluding_true = if let Some(lit) = literal_value(self.db, excluded_type) {
721                matches!(lit, LiteralValue::Boolean(true))
722            } else {
723                excluded_type == TypeId::BOOLEAN_TRUE
724            };
725
726            let is_excluding_false = if let Some(lit) = literal_value(self.db, excluded_type) {
727                matches!(lit, LiteralValue::Boolean(false))
728            } else {
729                excluded_type == TypeId::BOOLEAN_FALSE
730            };
731
732            // Handle exclusion from boolean, true, or false
733            if source_type == TypeId::BOOLEAN {
734                if is_excluding_true {
735                    // Excluding true from boolean -> return false
736                    return TypeId::BOOLEAN_FALSE;
737                } else if is_excluding_false {
738                    // Excluding false from boolean -> return true
739                    return TypeId::BOOLEAN_TRUE;
740                }
741                // If excluding BOOLEAN, let the final is_assignable_to check handle it below
742            } else if source_type == TypeId::BOOLEAN_TRUE {
743                if is_excluding_true {
744                    // Excluding true from true -> return never
745                    return TypeId::NEVER;
746                }
747                // For other cases (e.g., excluding BOOLEAN from TRUE),
748                // let the final is_assignable_to check handle it below
749            } else if source_type == TypeId::BOOLEAN_FALSE && is_excluding_false {
750                // Excluding false from false -> return never
751                return TypeId::NEVER;
752            }
753            // For other cases, let the final is_assignable_to check handle it below
754            // CRITICAL: Do NOT return source_type here.
755            // Fall through to the standard is_assignable_to check below.
756            // This handles edge cases like narrow_excluding_type(TRUE, BOOLEAN) -> NEVER
757        }
758
759        // If source is assignable to excluded, return never
760        if self.is_assignable_to(source_type, excluded_type) {
761            TypeId::NEVER
762        } else {
763            source_type
764        }
765    }
766
767    /// Narrow a type by excluding multiple types at once (batched version).
768    ///
769    /// This is an optimized version of `narrow_excluding_type` for cases like
770    /// switch default clauses where we need to exclude many types at once.
771    /// It avoids creating intermediate union types and reduces complexity from O(N²) to O(N).
772    ///
773    /// # Arguments
774    /// * `source_type` - The type to narrow (typically a union)
775    /// * `excluded_types` - Types to exclude from the source
776    ///
777    /// # Returns
778    /// The narrowed type with all excluded types removed
779    pub fn narrow_excluding_types(&self, source_type: TypeId, excluded_types: &[TypeId]) -> TypeId {
780        if excluded_types.is_empty() {
781            return source_type;
782        }
783
784        // For small lists, use sequential narrowing (avoids HashSet overhead)
785        if excluded_types.len() <= 4 {
786            let mut result = source_type;
787            for &excluded in excluded_types {
788                result = self.narrow_excluding_type(result, excluded);
789                if result == TypeId::NEVER {
790                    return TypeId::NEVER;
791                }
792            }
793            return result;
794        }
795
796        // For larger lists, use HashSet for O(1) lookup
797        let excluded_set: rustc_hash::FxHashSet<TypeId> = excluded_types.iter().copied().collect();
798
799        // Handle union source type
800        if let Some(members) = union_list_id(self.db, source_type) {
801            let members = self.db.type_list(members);
802            let remaining: Vec<TypeId> = members
803                .iter()
804                .filter_map(|&member| {
805                    // Fast path: direct identity check against the set
806                    if excluded_set.contains(&member) {
807                        return None;
808                    }
809
810                    // Handle intersection members
811                    if intersection_list_id(self.db, member).is_some() {
812                        return self
813                            .narrow_excluding_types(member, excluded_types)
814                            .non_never();
815                    }
816
817                    // Handle type parameters
818                    if let Some(narrowed) =
819                        self.narrow_type_param_excluding_set(member, &excluded_set)
820                    {
821                        return narrowed.non_never();
822                    }
823
824                    // Slow path: check assignability for complex cases
825                    // This handles cases where the member isn't identical to an excluded type
826                    // but might still be assignable to one (e.g., literal subtypes)
827                    for &excluded in &excluded_set {
828                        if self.is_assignable_to(member, excluded) {
829                            return None;
830                        }
831                    }
832                    Some(member)
833                })
834                .collect();
835
836            if remaining.is_empty() {
837                return TypeId::NEVER;
838            } else if remaining.len() == 1 {
839                return remaining[0];
840            }
841            return self.db.union(remaining);
842        }
843
844        // Handle single type (not a union)
845        if excluded_set.contains(&source_type) {
846            return TypeId::NEVER;
847        }
848
849        // Check assignability for single type
850        for &excluded in &excluded_set {
851            if self.is_assignable_to(source_type, excluded) {
852                return TypeId::NEVER;
853            }
854        }
855
856        source_type
857    }
858
859    /// Helper for `narrow_excluding_types` with type parameters
860    fn narrow_type_param_excluding_set(
861        &self,
862        source: TypeId,
863        excluded_set: &rustc_hash::FxHashSet<TypeId>,
864    ) -> Option<TypeId> {
865        let info = type_param_info(self.db, source)?;
866
867        let constraint = info.constraint?;
868        if constraint == source || constraint == TypeId::UNKNOWN {
869            return None;
870        }
871
872        // Narrow the constraint by excluding all types in the set
873        let excluded_vec: Vec<TypeId> = excluded_set.iter().copied().collect();
874        let narrowed_constraint = self.narrow_excluding_types(constraint, &excluded_vec);
875
876        if narrowed_constraint == constraint {
877            return None;
878        }
879        if narrowed_constraint == TypeId::NEVER {
880            return Some(TypeId::NEVER);
881        }
882
883        Some(self.db.intersection2(source, narrowed_constraint))
884    }
885
886    /// Narrow to function types only.
887    fn narrow_to_function(&self, source_type: TypeId) -> TypeId {
888        if let Some(members) = union_list_id(self.db, source_type) {
889            let members = self.db.type_list(members);
890            let functions: Vec<TypeId> = members
891                .iter()
892                .filter_map(|&member| {
893                    if let Some(narrowed) = self.narrow_type_param_to_function(member) {
894                        return narrowed.non_never();
895                    }
896                    self.is_function_type(member).then_some(member)
897                })
898                .collect();
899
900            return union_or_single(self.db, functions);
901        }
902
903        if let Some(narrowed) = self.narrow_type_param_to_function(source_type) {
904            return narrowed;
905        }
906
907        if self.is_function_type(source_type) {
908            source_type
909        } else if source_type == TypeId::OBJECT {
910            self.function_type()
911        } else if let Some(shape_id) = object_shape_id(self.db, source_type) {
912            let shape = self.db.object_shape(shape_id);
913            if shape.properties.is_empty() {
914                self.function_type()
915            } else {
916                TypeId::NEVER
917            }
918        } else if let Some(shape_id) = object_with_index_shape_id(self.db, source_type) {
919            let shape = self.db.object_shape(shape_id);
920            if shape.properties.is_empty()
921                && shape.string_index.is_none()
922                && shape.number_index.is_none()
923            {
924                self.function_type()
925            } else {
926                TypeId::NEVER
927            }
928        } else if index_access_parts(self.db, source_type).is_some() {
929            // For indexed access types like T[K], narrow to T[K] & Function
930            // This handles cases like: typeof obj[key] === 'function'
931            let function_type = self.function_type();
932            self.db.intersection2(source_type, function_type)
933        } else {
934            TypeId::NEVER
935        }
936    }
937
938    /// Check if a type is a function type.
939    /// Uses the visitor pattern from `solver::visitor`.
940    fn is_function_type(&self, type_id: TypeId) -> bool {
941        is_function_type_db(self.db, type_id)
942    }
943
944    /// Narrow a type to exclude function-like members (typeof !== "function").
945    pub fn narrow_excluding_function(&self, source_type: TypeId) -> TypeId {
946        if let Some(members) = union_list_id(self.db, source_type) {
947            let members = self.db.type_list(members);
948            let remaining: Vec<TypeId> = members
949                .iter()
950                .filter_map(|&member| {
951                    if let Some(narrowed) = self.narrow_type_param_excluding_function(member) {
952                        return narrowed.non_never();
953                    }
954                    if self.is_function_type(member) {
955                        None
956                    } else {
957                        Some(member)
958                    }
959                })
960                .collect();
961
962            return union_or_single(self.db, remaining);
963        }
964
965        if let Some(narrowed) = self.narrow_type_param_excluding_function(source_type) {
966            return narrowed;
967        }
968
969        if self.is_function_type(source_type) {
970            TypeId::NEVER
971        } else {
972            source_type
973        }
974    }
975
976    /// Check if a type has typeof "object".
977    /// Uses the visitor pattern from `solver::visitor`.
978    fn is_object_typeof(&self, type_id: TypeId) -> bool {
979        is_object_like_type_db(self.db, type_id)
980    }
981
982    fn narrow_type_param(&self, source: TypeId, target: TypeId) -> Option<TypeId> {
983        let info = type_param_info(self.db, source)?;
984
985        let constraint = info.constraint.unwrap_or(TypeId::UNKNOWN);
986        if constraint == source {
987            return None;
988        }
989
990        let narrowed_constraint = if constraint == TypeId::UNKNOWN {
991            target
992        } else {
993            self.narrow_to_type(constraint, target)
994        };
995
996        if narrowed_constraint == TypeId::NEVER {
997            return None;
998        }
999
1000        Some(self.db.intersection2(source, narrowed_constraint))
1001    }
1002
1003    fn narrow_type_param_to_function(&self, source: TypeId) -> Option<TypeId> {
1004        let info = type_param_info(self.db, source)?;
1005
1006        let constraint = info.constraint.unwrap_or(TypeId::UNKNOWN);
1007        if constraint == source || constraint == TypeId::UNKNOWN {
1008            let function_type = self.function_type();
1009            return Some(self.db.intersection2(source, function_type));
1010        }
1011
1012        let narrowed_constraint = self.narrow_to_function(constraint);
1013        if narrowed_constraint == TypeId::NEVER {
1014            return None;
1015        }
1016
1017        Some(self.db.intersection2(source, narrowed_constraint))
1018    }
1019
1020    fn narrow_type_param_excluding(&self, source: TypeId, excluded: TypeId) -> Option<TypeId> {
1021        let info = type_param_info(self.db, source)?;
1022
1023        let constraint = info.constraint?;
1024        if constraint == source || constraint == TypeId::UNKNOWN {
1025            return None;
1026        }
1027
1028        let narrowed_constraint = self.narrow_excluding_type(constraint, excluded);
1029        if narrowed_constraint == constraint {
1030            return None;
1031        }
1032        if narrowed_constraint == TypeId::NEVER {
1033            return Some(TypeId::NEVER);
1034        }
1035
1036        Some(self.db.intersection2(source, narrowed_constraint))
1037    }
1038
1039    fn narrow_type_param_excluding_function(&self, source: TypeId) -> Option<TypeId> {
1040        let info = type_param_info(self.db, source)?;
1041
1042        let constraint = info.constraint.unwrap_or(TypeId::UNKNOWN);
1043        if constraint == source || constraint == TypeId::UNKNOWN {
1044            return Some(source);
1045        }
1046
1047        let narrowed_constraint = self.narrow_excluding_function(constraint);
1048        if narrowed_constraint == constraint {
1049            return Some(source);
1050        }
1051        if narrowed_constraint == TypeId::NEVER {
1052            return Some(TypeId::NEVER);
1053        }
1054
1055        Some(self.db.intersection2(source, narrowed_constraint))
1056    }
1057
1058    pub(crate) fn function_type(&self) -> TypeId {
1059        let rest_array = self.db.array(TypeId::ANY);
1060        let rest_param = ParamInfo {
1061            name: None,
1062            type_id: rest_array,
1063            optional: false,
1064            rest: true,
1065        };
1066        self.db.function(FunctionShape {
1067            params: vec![rest_param],
1068            this_type: None,
1069            return_type: TypeId::ANY,
1070            type_params: Vec::new(),
1071            type_predicate: None,
1072            is_constructor: false,
1073            is_method: false,
1074        })
1075    }
1076
1077    /// Check if a type is a JS primitive that can never pass `instanceof`.
1078    /// Includes string, number, boolean, bigint, symbol, undefined, null,
1079    /// void, never, and their literal forms.
1080    fn is_js_primitive(&self, type_id: TypeId) -> bool {
1081        matches!(
1082            type_id,
1083            TypeId::STRING
1084                | TypeId::NUMBER
1085                | TypeId::BOOLEAN
1086                | TypeId::BIGINT
1087                | TypeId::SYMBOL
1088                | TypeId::UNDEFINED
1089                | TypeId::NULL
1090                | TypeId::VOID
1091                | TypeId::NEVER
1092                | TypeId::BOOLEAN_TRUE
1093                | TypeId::BOOLEAN_FALSE
1094        ) || matches!(self.db.lookup(type_id), Some(TypeData::Literal(_)))
1095    }
1096
1097    /// Simple assignability check for narrowing purposes.
1098    fn is_assignable_to(&self, source: TypeId, target: TypeId) -> bool {
1099        if source == target {
1100            return true;
1101        }
1102
1103        // never is assignable to everything
1104        if source == TypeId::NEVER {
1105            return true;
1106        }
1107
1108        // everything is assignable to any/unknown
1109        if target.is_any_or_unknown() {
1110            return true;
1111        }
1112
1113        // Literal to base type
1114        if let Some(lit) = literal_value(self.db, source) {
1115            match (lit, target) {
1116                (LiteralValue::String(_), t) if t == TypeId::STRING => return true,
1117                (LiteralValue::Number(_), t) if t == TypeId::NUMBER => return true,
1118                (LiteralValue::Boolean(_), t) if t == TypeId::BOOLEAN => return true,
1119                (LiteralValue::BigInt(_), t) if t == TypeId::BIGINT => return true,
1120                _ => {}
1121            }
1122        }
1123
1124        // object/null for typeof "object"
1125        if target == TypeId::OBJECT {
1126            if source == TypeId::NULL {
1127                return true;
1128            }
1129            if self.is_object_typeof(source) {
1130                return true;
1131            }
1132            return false;
1133        }
1134
1135        if let Some(members) = intersection_list_id(self.db, source) {
1136            let members = self.db.type_list(members);
1137            if members
1138                .iter()
1139                .any(|member| self.is_assignable_to(*member, target))
1140            {
1141                return true;
1142            }
1143        }
1144
1145        if target == TypeId::STRING && template_literal_id(self.db, source).is_some() {
1146            return true;
1147        }
1148
1149        // Check if source is assignable to any member of a union target
1150        if let Some(members) = union_list_id(self.db, target) {
1151            let members = self.db.type_list(members);
1152            if members
1153                .iter()
1154                .any(|&member| self.is_assignable_to(source, member))
1155            {
1156                return true;
1157            }
1158        }
1159
1160        // Fallback: use full structural/nominal subtype check.
1161        // This handles class inheritance (Derived extends Base), interface
1162        // implementations, and other structural relationships that the
1163        // fast-path checks above don't cover.
1164        // CRITICAL: Resolve Lazy(DefId) types before the subtype check.
1165        // Without resolution, two unrelated interfaces (e.g., Cat and Dog)
1166        // remain as opaque Lazy types and the SubtypeChecker can't distinguish them.
1167        let source = self.resolve_type(source);
1168        let target = self.resolve_type(target);
1169        if source == target {
1170            return true;
1171        }
1172        crate::relations::subtype::is_subtype_of_with_db(self.db, source, target)
1173    }
1174
1175    /// Applies a type guard to narrow a type.
1176    ///
1177    /// This is the main entry point for AST-agnostic type narrowing.
1178    /// The Checker extracts a `TypeGuard` from AST nodes, and the Solver
1179    /// applies it to compute the narrowed type.
1180    ///
1181    /// # Arguments
1182    /// * `source_type` - The type to narrow
1183    /// * `guard` - The guard condition (extracted from AST by Checker)
1184    /// * `sense` - If true, narrow for the "true" branch; if false, narrow for the "false" branch
1185    ///
1186    /// # Returns
1187    /// The narrowed type after applying the guard.
1188    ///
1189    /// # Examples
1190    /// ```ignore
1191    /// // typeof x === "string"
1192    /// let guard = TypeGuard::Typeof(TypeofKind::String);
1193    /// let narrowed = narrowing.narrow_type(string_or_number, &guard, true);
1194    /// assert_eq!(narrowed, TypeId::STRING);
1195    ///
1196    /// // x !== null (negated sense)
1197    /// let guard = TypeGuard::NullishEquality;
1198    /// let narrowed = narrowing.narrow_type(string_or_null, &guard, false);
1199    /// // Result should exclude null and undefined
1200    /// ```
1201    pub fn narrow_type(&self, source_type: TypeId, guard: &TypeGuard, sense: bool) -> TypeId {
1202        match guard {
1203            TypeGuard::Typeof(typeof_kind) => {
1204                let type_name = typeof_kind.as_str();
1205                if sense {
1206                    self.narrow_by_typeof(source_type, type_name)
1207                } else {
1208                    // Negation: exclude typeof type
1209                    self.narrow_by_typeof_negation(source_type, type_name)
1210                }
1211            }
1212
1213            TypeGuard::Instanceof(instance_type) => {
1214                if sense {
1215                    // Positive: x instanceof Class
1216                    // Special case: `unknown` instanceof X narrows to X (or object if X unknown)
1217                    // This must be handled here in the solver, not in the checker.
1218                    if source_type == TypeId::UNKNOWN {
1219                        return *instance_type;
1220                    }
1221
1222                    // CRITICAL: The payload is already the Instance Type (extracted by Checker)
1223                    // Use narrow_by_instance_type for instanceof-specific semantics:
1224                    // type parameters with matching constraints are kept, but anonymous
1225                    // object types that happen to be structurally compatible are excluded.
1226                    // Primitive types are filtered out since they can never pass instanceof.
1227                    let narrowed = self.narrow_by_instance_type(source_type, *instance_type);
1228
1229                    if narrowed != TypeId::NEVER || source_type == TypeId::NEVER {
1230                        return narrowed;
1231                    }
1232
1233                    // Fallback 1: If standard narrowing returns NEVER but source wasn't NEVER,
1234                    // it might be an interface vs class check (which is allowed in TS).
1235                    // Use intersection in that case.
1236                    let intersection = self.db.intersection2(source_type, *instance_type);
1237                    if intersection != TypeId::NEVER {
1238                        return intersection;
1239                    }
1240
1241                    // Fallback 2: If even intersection fails, narrow to object-like types.
1242                    // On the true branch of instanceof, we know the value must be some
1243                    // kind of object (primitives can never pass instanceof).
1244                    self.narrow_to_objectish(source_type)
1245                } else {
1246                    // Negative: !(x instanceof Class)
1247                    // Keep primitives (they can never pass instanceof) and exclude
1248                    // non-primitive types assignable to the instance type.
1249                    if *instance_type == TypeId::OBJECT {
1250                        source_type
1251                    } else {
1252                        self.narrow_by_instanceof_false(source_type, *instance_type)
1253                    }
1254                }
1255            }
1256
1257            TypeGuard::LiteralEquality(literal_type) => {
1258                if sense {
1259                    // Equality: narrow to the literal type
1260                    self.narrow_to_type(source_type, *literal_type)
1261                } else {
1262                    // Inequality: exclude the literal type
1263                    self.narrow_excluding_type(source_type, *literal_type)
1264                }
1265            }
1266
1267            TypeGuard::NullishEquality => {
1268                if sense {
1269                    // Equality with null: narrow to null | undefined
1270                    self.db.union(vec![TypeId::NULL, TypeId::UNDEFINED])
1271                } else {
1272                    // Inequality: exclude null and undefined
1273                    let without_null = self.narrow_excluding_type(source_type, TypeId::NULL);
1274                    self.narrow_excluding_type(without_null, TypeId::UNDEFINED)
1275                }
1276            }
1277
1278            TypeGuard::Truthy => {
1279                if sense {
1280                    // Truthy: remove null and undefined (TypeScript doesn't narrow other falsy values)
1281                    self.narrow_by_truthiness(source_type)
1282                } else {
1283                    // Falsy: narrow to the falsy component(s)
1284                    // This handles cases like: if (!x) where x: string → "" in false branch
1285                    self.narrow_to_falsy(source_type)
1286                }
1287            }
1288
1289            TypeGuard::Discriminant {
1290                property_path,
1291                value_type,
1292            } => {
1293                // Use narrow_by_discriminant_for_type which handles type parameters
1294                // by narrowing the constraint and returning T & NarrowedConstraint
1295                self.narrow_by_discriminant_for_type(source_type, property_path, *value_type, sense)
1296            }
1297
1298            TypeGuard::InProperty(property_name) => {
1299                if sense {
1300                    // Positive: "prop" in x - narrow to types that have the property
1301                    self.narrow_by_property_presence(source_type, *property_name, true)
1302                } else {
1303                    // Negative: !("prop" in x) - narrow to types that don't have the property
1304                    self.narrow_by_property_presence(source_type, *property_name, false)
1305                }
1306            }
1307
1308            TypeGuard::Predicate { type_id, asserts } => {
1309                match type_id {
1310                    Some(target_type) => {
1311                        // Type guard with specific type: is T or asserts T
1312                        if sense {
1313                            // True branch: narrow source to the predicate type.
1314                            // Following TSC's narrowType logic:
1315                            // 1. For unions: filter members using narrow_to_type
1316                            // 2. For non-unions:
1317                            //    a. source <: target → return source
1318                            //    b. target <: source → return target
1319                            //    c. otherwise → return source & target
1320                            //
1321                            // Following TSC's narrowType logic which uses
1322                            // isTypeSubtypeOf (not isTypeAssignableTo) to decide
1323                            // whether source is already specific enough.
1324                            //
1325                            // If source is a strict subtype of the target, return
1326                            // source (it's already more specific). If target is a
1327                            // strict subtype of source, return target (narrowing
1328                            // down). Otherwise, return the intersection.
1329                            //
1330                            // narrow_to_type uses assignability internally, which is
1331                            // too loose for type predicates (e.g. {} is assignable to
1332                            // Record<string,unknown> but not a subtype).
1333                            let resolved_source = self.resolve_type(source_type);
1334
1335                            if resolved_source == self.resolve_type(*target_type) {
1336                                source_type
1337                            } else if resolved_source == TypeId::UNKNOWN
1338                                || resolved_source == TypeId::ANY
1339                            {
1340                                *target_type
1341                            } else if union_list_id(self.db, resolved_source).is_some() {
1342                                // For unions: filter members, fall back to
1343                                // intersection if nothing matches.
1344                                let narrowed = self.narrow_to_type(source_type, *target_type);
1345                                if narrowed == TypeId::NEVER && source_type != TypeId::NEVER {
1346                                    self.db.intersection2(source_type, *target_type)
1347                                } else {
1348                                    narrowed
1349                                }
1350                            } else {
1351                                // Non-union source: use narrow_to_type first.
1352                                // If it returns source unchanged (assignable but
1353                                // possibly losing structural info) or NEVER (no
1354                                // overlap), fall back to intersection.
1355                                let narrowed = self.narrow_to_type(source_type, *target_type);
1356                                if narrowed == source_type && narrowed != *target_type {
1357                                    // Source was unchanged — intersect to preserve
1358                                    // target's structural info (index sigs, etc.)
1359                                    self.db.intersection2(source_type, *target_type)
1360                                } else if narrowed == TypeId::NEVER && source_type != TypeId::NEVER
1361                                {
1362                                    self.db.intersection2(source_type, *target_type)
1363                                } else {
1364                                    narrowed
1365                                }
1366                            }
1367                        } else if *asserts {
1368                            // CRITICAL: For assertion functions, the false branch is unreachable
1369                            // (the function throws if the assertion fails), so we don't narrow
1370                            source_type
1371                        } else {
1372                            // False branch for regular type guards: exclude the target type
1373                            self.narrow_excluding_type(source_type, *target_type)
1374                        }
1375                    }
1376                    None => {
1377                        // Truthiness assertion: asserts x
1378                        // Behaves like TypeGuard::Truthy (narrows to truthy in true branch)
1379                        if *asserts {
1380                            self.narrow_by_truthiness(source_type)
1381                        } else {
1382                            source_type
1383                        }
1384                    }
1385                }
1386            }
1387
1388            TypeGuard::Array => {
1389                if sense {
1390                    // Positive: Array.isArray(x) - narrow to array-like types
1391                    self.narrow_to_array(source_type)
1392                } else {
1393                    // Negative: !Array.isArray(x) - exclude array-like types
1394                    self.narrow_excluding_array(source_type)
1395                }
1396            }
1397
1398            TypeGuard::ArrayElementPredicate { element_type } => {
1399                trace!(
1400                    ?element_type,
1401                    ?sense,
1402                    "Applying ArrayElementPredicate guard"
1403                );
1404                if sense {
1405                    // True branch: narrow array element type
1406                    let result = self.narrow_array_element_type(source_type, *element_type);
1407                    trace!(?result, "ArrayElementPredicate narrowing result");
1408                    result
1409                } else {
1410                    // False branch: we don't narrow (arr.every could be false for various reasons)
1411                    trace!("ArrayElementPredicate false branch, no narrowing");
1412                    source_type
1413                }
1414            }
1415        }
1416    }
1417}
1418
1419#[cfg(test)]
1420#[path = "../../tests/narrowing_tests.rs"]
1421mod tests;