tsz_solver/narrowing.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
30// Re-export utility functions that were extracted to narrowing_utils
31pub use crate::narrowing_utils::{
32 can_be_nullish, find_discriminants, is_definitely_nullish, is_nullish_type,
33 narrow_by_discriminant, narrow_by_typeof, remove_definitely_falsy_types, remove_nullish,
34 split_nullish_type, type_contains_nullish, type_contains_undefined,
35};
36
37use crate::subtype::{SubtypeChecker, TypeResolver};
38use crate::type_queries::{UnionMembersKind, classify_for_union_members};
39#[cfg(test)]
40use crate::types::*;
41use crate::types::{FunctionShape, LiteralValue, ParamInfo, TypeData, TypeId};
42use crate::utils::{TypeIdExt, intersection_or_single, union_or_single};
43use crate::visitor::{
44 index_access_parts, intersection_list_id, is_function_type_db, is_object_like_type_db,
45 lazy_def_id, literal_value, object_shape_id, object_with_index_shape_id, template_literal_id,
46 type_param_info, union_list_id,
47};
48use crate::{QueryDatabase, TypeDatabase};
49use rustc_hash::FxHashMap;
50use std::cell::RefCell;
51use tracing::{Level, span, trace};
52use tsz_common::interner::Atom;
53
54/// The result of a `typeof` expression, restricted to the 8 standard JavaScript types.
55///
56/// Using an enum instead of `String` eliminates heap allocation per typeof guard.
57/// TypeScript's `typeof` operator only returns these 8 values.
58#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
59pub enum TypeofKind {
60 String,
61 Number,
62 Boolean,
63 BigInt,
64 Symbol,
65 Undefined,
66 Object,
67 Function,
68}
69
70impl TypeofKind {
71 /// Parse a typeof result string into a `TypeofKind`.
72 /// Returns None for non-standard typeof strings (which don't narrow).
73 pub fn parse(s: &str) -> Option<Self> {
74 match s {
75 "string" => Some(Self::String),
76 "number" => Some(Self::Number),
77 "boolean" => Some(Self::Boolean),
78 "bigint" => Some(Self::BigInt),
79 "symbol" => Some(Self::Symbol),
80 "undefined" => Some(Self::Undefined),
81 "object" => Some(Self::Object),
82 "function" => Some(Self::Function),
83 _ => None,
84 }
85 }
86
87 /// Get the string representation of this typeof kind.
88 pub const fn as_str(&self) -> &'static str {
89 match self {
90 Self::String => "string",
91 Self::Number => "number",
92 Self::Boolean => "boolean",
93 Self::BigInt => "bigint",
94 Self::Symbol => "symbol",
95 Self::Undefined => "undefined",
96 Self::Object => "object",
97 Self::Function => "function",
98 }
99 }
100}
101
102/// AST-agnostic representation of a type narrowing condition.
103///
104/// This enum represents various guards that can narrow a type, without
105/// depending on AST nodes like `NodeIndex` or `SyntaxKind`.
106///
107/// # Examples
108/// ```typescript
109/// typeof x === "string" -> TypeGuard::Typeof(TypeofKind::String)
110/// x instanceof MyClass -> TypeGuard::Instanceof(MyClass_type)
111/// x === null -> TypeGuard::NullishEquality
112/// x -> TypeGuard::Truthy
113/// x.kind === "circle" -> TypeGuard::Discriminant { property: "kind", value: "circle" }
114/// ```
115#[derive(Clone, Debug, PartialEq)]
116pub enum TypeGuard {
117 /// `typeof x === "typename"`
118 ///
119 /// Narrows a union to only members matching the typeof result.
120 /// For example, narrowing `string | number` with `Typeof(TypeofKind::String)` yields `string`.
121 Typeof(TypeofKind),
122
123 /// `x instanceof Class`
124 ///
125 /// Narrows to the class type or its subtypes.
126 Instanceof(TypeId),
127
128 /// `x === literal` or `x !== literal`
129 ///
130 /// Narrows to exactly that literal type (for equality) or excludes it (for inequality).
131 LiteralEquality(TypeId),
132
133 /// `x == null` or `x != null` (checks both null and undefined)
134 ///
135 /// JavaScript/TypeScript treats `== null` as matching both `null` and `undefined`.
136 NullishEquality,
137
138 /// `x` (truthiness check in a conditional)
139 ///
140 /// Removes falsy types from a union: `null`, `undefined`, `false`, `0`, `""`, `NaN`.
141 Truthy,
142
143 /// `x.prop === literal` or `x.payload.type === "value"` (Discriminated Union narrowing)
144 ///
145 /// Narrows a union of object types based on a discriminant property.
146 ///
147 /// # Examples
148 /// - Top-level: `{ kind: "A" } | { kind: "B" }` with `path: ["kind"]` yields `{ kind: "A" }`
149 /// - Nested: `{ payload: { type: "user" } } | { payload: { type: "product" } }`
150 /// with `path: ["payload", "type"]` yields `{ payload: { type: "user" } }`
151 Discriminant {
152 /// Property path from base to discriminant (e.g., ["payload", "type"])
153 property_path: Vec<Atom>,
154 /// The literal value to match against
155 value_type: TypeId,
156 },
157
158 /// `prop in x`
159 ///
160 /// Narrows to types that have the specified property.
161 InProperty(Atom),
162
163 /// `x is T` or `asserts x is T` (User-Defined Type Guard)
164 ///
165 /// Narrows a type based on a user-defined type predicate function.
166 ///
167 /// # Examples
168 /// ```typescript
169 /// function isString(x: any): x is string { ... }
170 /// function assertDefined(x: any): asserts x is Date { ... }
171 ///
172 /// if (isString(x)) { x; // string }
173 /// assertDefined(x); x; // Date
174 /// ```
175 ///
176 /// - `type_id: Some(T)`: The type to narrow to (e.g., `string` or `Date`)
177 /// - `type_id: None`: Truthiness assertion (`asserts x`), behaves like `Truthy`
178 /// - `asserts: true`: This is an assertion (throws if false), affects control flow
179 Predicate {
180 type_id: Option<TypeId>,
181 asserts: bool,
182 },
183
184 /// `Array.isArray(x)`
185 ///
186 /// Narrows a type to only array-like types (arrays, tuples, readonly arrays).
187 ///
188 /// # Examples
189 /// ```typescript
190 /// function process(x: string[] | number | { length: number }) {
191 /// if (Array.isArray(x)) {
192 /// x; // string[] (not number or the object)
193 /// }
194 /// }
195 /// ```
196 ///
197 /// This preserves element types - `string[] | number[]` stays as `string[] | number[]`,
198 /// it doesn't collapse to `any[]`.
199 Array,
200
201 /// `array.every(predicate)` where predicate has type predicate
202 ///
203 /// Narrows an array's element type based on a type predicate.
204 ///
205 /// # Examples
206 /// ```typescript
207 /// const arr: (number | string)[] = ['aaa'];
208 /// const isString = (x: unknown): x is string => typeof x === 'string';
209 /// if (arr.every(isString)) {
210 /// arr; // string[] (element type narrowed from number | string to string)
211 /// }
212 /// ```
213 ///
214 /// This only applies to arrays. For non-array types, the type is unchanged.
215 ArrayElementPredicate {
216 /// The type to narrow array elements to
217 element_type: TypeId,
218 },
219}
220
221#[inline]
222pub(crate) fn union_or_single_preserve(db: &dyn TypeDatabase, types: Vec<TypeId>) -> TypeId {
223 match types.len() {
224 0 => TypeId::NEVER,
225 1 => types[0],
226 _ => db.union_from_sorted_vec(types),
227 }
228}
229
230/// Result of a narrowing operation.
231///
232/// Represents the types in both branches of a condition.
233#[derive(Clone, Debug)]
234pub struct NarrowingResult {
235 /// The type in the "true" branch of the condition
236 pub true_type: TypeId,
237 /// The type in the "false" branch of the condition
238 pub false_type: TypeId,
239}
240
241/// Result of finding discriminant properties in a union.
242#[derive(Clone, Debug)]
243pub struct DiscriminantInfo {
244 /// The name of the discriminant property
245 pub property_name: Atom,
246 /// Map from literal value to the union member type
247 pub variants: Vec<(TypeId, TypeId)>, // (literal_type, member_type)
248}
249
250/// Narrowing context for type guards and control flow analysis.
251/// Shared across multiple narrowing contexts to persist resolution results.
252#[derive(Default, Clone, Debug)]
253pub struct NarrowingCache {
254 /// Cache for type resolution (Lazy/App/Template -> Structural)
255 pub resolve_cache: RefCell<FxHashMap<TypeId, TypeId>>,
256 /// Cache for top-level property type lookups (TypeId, `PropName`) -> `PropType`
257 pub property_cache: RefCell<FxHashMap<(TypeId, Atom), Option<TypeId>>>,
258}
259
260impl NarrowingCache {
261 pub fn new() -> Self {
262 Self::default()
263 }
264}
265
266/// Narrowing context for type guards and control flow analysis.
267pub struct NarrowingContext<'a> {
268 pub(crate) db: &'a dyn QueryDatabase,
269 /// Optional `TypeResolver` for resolving Lazy types (e.g., type aliases).
270 /// When present, this enables proper narrowing of type aliases like `type Shape = Circle | Square`.
271 pub(crate) resolver: Option<&'a dyn TypeResolver>,
272 /// Cache for narrowing operations.
273 /// If provided, uses the shared cache; otherwise uses a local ephemeral cache.
274 pub(crate) cache: std::borrow::Cow<'a, NarrowingCache>,
275}
276
277impl<'a> NarrowingContext<'a> {
278 pub fn new(db: &'a dyn QueryDatabase) -> Self {
279 NarrowingContext {
280 db,
281 resolver: None,
282 cache: std::borrow::Cow::Owned(NarrowingCache::new()),
283 }
284 }
285
286 /// Create a new context with a shared cache.
287 pub fn with_cache(db: &'a dyn QueryDatabase, cache: &'a NarrowingCache) -> Self {
288 NarrowingContext {
289 db,
290 resolver: None,
291 cache: std::borrow::Cow::Borrowed(cache),
292 }
293 }
294
295 /// Set the `TypeResolver` for this context.
296 ///
297 /// This enables proper resolution of Lazy types (type aliases) during narrowing.
298 /// The resolver should be borrowed from the Checker's `TypeEnvironment`.
299 pub fn with_resolver(mut self, resolver: &'a dyn TypeResolver) -> Self {
300 self.resolver = Some(resolver);
301 self
302 }
303
304 /// Resolve a type to its structural representation.
305 ///
306 /// Unwraps:
307 /// - Lazy types (evaluates them using resolver if available, otherwise falls back to db)
308 /// - Application types (evaluates the generic instantiation)
309 ///
310 /// This ensures that type aliases, interfaces, and generics are resolved
311 /// to their actual structural types before performing narrowing operations.
312 pub(crate) fn resolve_type(&self, type_id: TypeId) -> TypeId {
313 if let Some(&cached) = self.cache.resolve_cache.borrow().get(&type_id) {
314 return cached;
315 }
316
317 let result = self.resolve_type_uncached(type_id);
318 self.cache
319 .resolve_cache
320 .borrow_mut()
321 .insert(type_id, result);
322 result
323 }
324
325 fn resolve_type_uncached(&self, mut type_id: TypeId) -> TypeId {
326 // Prevent infinite loops with a fuel counter
327 let mut fuel = 100;
328
329 while fuel > 0 {
330 fuel -= 1;
331
332 // 1. Handle Lazy types (DefId-based, not SymbolRef)
333 // If we have a TypeResolver, try to resolve Lazy types through it first
334 if let Some(def_id) = lazy_def_id(self.db, type_id) {
335 if let Some(resolver) = self.resolver
336 && let Some(resolved) =
337 resolver.resolve_lazy(def_id, self.db.as_type_database())
338 {
339 type_id = resolved;
340 continue;
341 }
342 // Fallback to database evaluation if no resolver or resolution failed
343 type_id = self.db.evaluate_type(type_id);
344 continue;
345 }
346
347 // 2. Handle Application types (Generics)
348 // CRITICAL: When a resolver is available (from the checker's TypeEnvironment),
349 // use it to resolve the Application's base type and instantiate with args.
350 // Without the resolver, generic type aliases like `Box<number>` can't resolve
351 // their DefId-based base types, causing narrowing to fail on discriminated
352 // unions wrapped in generics.
353 if let Some(TypeData::Application(app_id)) = self.db.lookup(type_id) {
354 if let Some(resolver) = self.resolver {
355 let app = self.db.type_application(app_id);
356 // Try to resolve the base type's DefId and instantiate manually
357 if let Some(def_id) = lazy_def_id(self.db, app.base) {
358 let resolved_body =
359 resolver.resolve_lazy(def_id, self.db.as_type_database());
360 let type_params = resolver.get_lazy_type_params(def_id);
361 if let (Some(body), Some(params)) = (resolved_body, type_params) {
362 let instantiated = crate::instantiate::instantiate_generic(
363 self.db.as_type_database(),
364 body,
365 ¶ms,
366 &app.args,
367 );
368 type_id = instantiated;
369 continue;
370 }
371 }
372 }
373 // Fallback: use db.evaluate_type (works when resolver isn't needed)
374 type_id = self.db.evaluate_type(type_id);
375 continue;
376 }
377
378 // 3. Handle TemplateLiteral types that can be fully evaluated to string literals.
379 // Template literal spans may contain Lazy(DefId) types (e.g., `${EnumType.Member}`)
380 // that must be resolved before evaluation. We resolve all lazy spans first,
381 // rebuild the template literal, then let the evaluator handle it.
382 if let Some(TypeData::TemplateLiteral(spans_id)) = self.db.lookup(type_id) {
383 use crate::types::TemplateSpan;
384 let spans = self.db.template_list(spans_id);
385 let mut new_spans = Vec::with_capacity(spans.len());
386 let mut changed = false;
387 for span in spans.iter() {
388 match span {
389 TemplateSpan::Type(inner_id) => {
390 let resolved = self.resolve_type(*inner_id);
391 if resolved != *inner_id {
392 changed = true;
393 }
394 new_spans.push(TemplateSpan::Type(resolved));
395 }
396 other => new_spans.push(other.clone()),
397 }
398 }
399 let eval_input = if changed {
400 self.db.template_literal(new_spans)
401 } else {
402 type_id
403 };
404 let evaluated = self.db.evaluate_type(eval_input);
405 if evaluated != type_id {
406 type_id = evaluated;
407 continue;
408 }
409 }
410
411 // It's a structural type (Object, Union, Intersection, Primitive)
412 break;
413 }
414
415 type_id
416 }
417
418 /// Narrow a type based on a typeof check.
419 ///
420 /// Example: `typeof x === "string"` narrows `string | number` to `string`
421 pub fn narrow_by_typeof(&self, source_type: TypeId, typeof_result: &str) -> TypeId {
422 let _span =
423 span!(Level::TRACE, "narrow_by_typeof", source_type = source_type.0, %typeof_result)
424 .entered();
425
426 // CRITICAL FIX: Narrow `any` for typeof checks
427 // TypeScript narrows `any` for typeof/instanceof/Array.isArray/user-defined guards
428 // But NOT for equality/truthiness/in operator
429 if source_type == TypeId::UNKNOWN || source_type == TypeId::ANY {
430 return match typeof_result {
431 "string" => TypeId::STRING,
432 "number" => TypeId::NUMBER,
433 "boolean" => TypeId::BOOLEAN,
434 "bigint" => TypeId::BIGINT,
435 "symbol" => TypeId::SYMBOL,
436 "undefined" => TypeId::UNDEFINED,
437 "object" => self.db.union2(TypeId::OBJECT, TypeId::NULL),
438 "function" => self.function_type(),
439 _ => source_type,
440 };
441 }
442
443 let target_type = match typeof_result {
444 "string" => TypeId::STRING,
445 "number" => TypeId::NUMBER,
446 "boolean" => TypeId::BOOLEAN,
447 "bigint" => TypeId::BIGINT,
448 "symbol" => TypeId::SYMBOL,
449 "undefined" => TypeId::UNDEFINED,
450 "object" => TypeId::OBJECT, // includes null
451 "function" => return self.narrow_to_function(source_type),
452 _ => return source_type,
453 };
454
455 self.narrow_to_type(source_type, target_type)
456 }
457
458 /// Narrow a type based on an instanceof check.
459 ///
460 /// Example: `x instanceof MyClass` narrows `A | B` to include only `A` where `A` is an instance of `MyClass`
461 pub fn narrow_by_instanceof(
462 &self,
463 source_type: TypeId,
464 constructor_type: TypeId,
465 sense: bool,
466 ) -> TypeId {
467 let _span = span!(
468 Level::TRACE,
469 "narrow_by_instanceof",
470 source_type = source_type.0,
471 constructor_type = constructor_type.0,
472 sense
473 )
474 .entered();
475
476 // TODO: Check for static [Symbol.hasInstance] method which overrides standard narrowing
477 // TypeScript allows classes to define custom instanceof behavior via:
478 // static [Symbol.hasInstance](value: any): boolean
479 // This would require evaluating method calls and type predicates, which is
480 // significantly more complex than the standard construct signature approach.
481
482 // CRITICAL: Resolve Lazy types for both source and constructor
483 // This ensures type aliases are resolved to their actual types
484 let resolved_source = self.resolve_type(source_type);
485 let resolved_constructor = self.resolve_type(constructor_type);
486
487 // Extract the instance type from the constructor
488 use crate::type_queries_extended::InstanceTypeKind;
489 use crate::type_queries_extended::classify_for_instance_type;
490
491 let instance_type = match classify_for_instance_type(self.db, resolved_constructor) {
492 InstanceTypeKind::Callable(shape_id) => {
493 // For callable types with construct signatures, get the return type of the construct signature
494 let shape = self.db.callable_shape(shape_id);
495 // Find a construct signature and get its return type (the instance type)
496 if let Some(construct_sig) = shape.construct_signatures.first() {
497 construct_sig.return_type
498 } else {
499 // No construct signature found, can't narrow
500 trace!("No construct signature found in callable type");
501 return source_type;
502 }
503 }
504 InstanceTypeKind::Function(shape_id) => {
505 // For function types, check if it's a constructor
506 let shape = self.db.function_shape(shape_id);
507 if shape.is_constructor {
508 // The return type is the instance type
509 shape.return_type
510 } else {
511 trace!("Function is not a constructor");
512 return source_type;
513 }
514 }
515 InstanceTypeKind::Intersection(members) => {
516 // For intersection types, we need to extract instance types from all members
517 // For now, create an intersection of the instance types
518 let instance_types: Vec<TypeId> = members
519 .iter()
520 .map(|&member| self.narrow_by_instanceof(source_type, member, sense))
521 .collect();
522
523 if sense {
524 intersection_or_single(self.db, instance_types)
525 } else {
526 // For negation with intersection, we can't easily exclude
527 // Fall back to returning the source type unchanged
528 source_type
529 }
530 }
531 InstanceTypeKind::Union(members) => {
532 // For union types, extract instance types from all members
533 let instance_types: Vec<TypeId> = members
534 .iter()
535 .filter_map(|&member| {
536 self.narrow_by_instanceof(source_type, member, sense)
537 .non_never()
538 })
539 .collect();
540
541 if sense {
542 union_or_single(self.db, instance_types)
543 } else {
544 // For negation with union, we can't easily exclude
545 // Fall back to returning the source type unchanged
546 source_type
547 }
548 }
549 InstanceTypeKind::Readonly(inner) => {
550 // Readonly wrapper - extract from inner type
551 return self.narrow_by_instanceof(source_type, inner, sense);
552 }
553 InstanceTypeKind::TypeParameter { constraint } => {
554 // Follow type parameter constraint
555 if let Some(constraint) = constraint {
556 return self.narrow_by_instanceof(source_type, constraint, sense);
557 }
558 trace!("Type parameter has no constraint");
559 return source_type;
560 }
561 InstanceTypeKind::SymbolRef(_) | InstanceTypeKind::NeedsEvaluation => {
562 // Complex cases that need further evaluation
563 // For now, return the source type unchanged
564 trace!("Complex instance type (SymbolRef or NeedsEvaluation), returning unchanged");
565 return source_type;
566 }
567 InstanceTypeKind::NotConstructor => {
568 trace!("Constructor type is not a valid constructor");
569 return source_type;
570 }
571 };
572
573 // Now narrow based on the sense (positive or negative)
574 if sense {
575 // CRITICAL: instanceof DOES narrow any/unknown (unlike equality checks)
576 if resolved_source == TypeId::ANY {
577 // any narrows to the instance type with instanceof
578 trace!("Narrowing any to instance type via instanceof");
579 return instance_type;
580 }
581
582 if resolved_source == TypeId::UNKNOWN {
583 // unknown narrows to the instance type with instanceof
584 trace!("Narrowing unknown to instance type via instanceof");
585 return instance_type;
586 }
587
588 // Handle Union: filter members based on instanceof relationship
589 if let Some(members_id) = union_list_id(self.db, resolved_source) {
590 let members = self.db.type_list(members_id);
591 // PERF: Reuse a single SubtypeChecker across all member checks
592 // instead of allocating 4 hash sets per is_subtype_of call.
593 let mut checker = SubtypeChecker::new(self.db.as_type_database());
594 let mut filtered_members: Vec<TypeId> = Vec::new();
595 for &member in &*members {
596 // Check if member is assignable to instance type
597 checker.reset();
598 if checker.is_subtype_of(member, instance_type) {
599 trace!(
600 "Union member {} is assignable to instance type {}, keeping",
601 member.0, instance_type.0
602 );
603 filtered_members.push(member);
604 continue;
605 }
606
607 // Check if instance type is assignable to member (subclass case)
608 // If we have a Dog and instanceof Animal, Dog is an instance of Animal
609 checker.reset();
610 if checker.is_subtype_of(instance_type, member) {
611 trace!(
612 "Instance type {} is assignable to union member {} (subclass), narrowing to instance type",
613 instance_type.0, member.0
614 );
615 filtered_members.push(instance_type);
616 continue;
617 }
618
619 // Interface overlap: both are object-like but not assignable
620 // Use intersection to preserve properties from both
621 if self.are_object_like(member) && self.are_object_like(instance_type) {
622 trace!(
623 "Interface overlap between {} and {}, using intersection",
624 member.0, instance_type.0
625 );
626 filtered_members.push(self.db.intersection2(member, instance_type));
627 continue;
628 }
629
630 trace!("Union member {} excluded by instanceof check", member.0);
631 }
632
633 union_or_single(self.db, filtered_members)
634 } else {
635 // Non-union type: use standard narrowing with intersection fallback
636 let narrowed = self.narrow_to_type(resolved_source, instance_type);
637
638 // If that returns NEVER, try intersection approach for interface vs class cases
639 // In TypeScript, instanceof on an interface narrows to intersection, not NEVER
640 if narrowed == TypeId::NEVER && resolved_source != TypeId::NEVER {
641 // Check for interface overlap before using intersection
642 if self.are_object_like(resolved_source) && self.are_object_like(instance_type)
643 {
644 trace!("Interface vs class detected, using intersection instead of NEVER");
645 self.db.intersection2(resolved_source, instance_type)
646 } else {
647 narrowed
648 }
649 } else {
650 narrowed
651 }
652 }
653 } else {
654 // Negative: !(x instanceof Constructor) - exclude the instance type
655 // For unions, exclude members that are subtypes of the instance type
656 if let Some(members_id) = union_list_id(self.db, resolved_source) {
657 let members = self.db.type_list(members_id);
658 // PERF: Reuse a single SubtypeChecker across all member checks
659 let mut checker = SubtypeChecker::new(self.db.as_type_database());
660 let mut filtered_members: Vec<TypeId> = Vec::new();
661 for &member in &*members {
662 // Exclude members that are definitely subtypes of the instance type
663 checker.reset();
664 if !checker.is_subtype_of(member, instance_type) {
665 filtered_members.push(member);
666 }
667 }
668
669 union_or_single(self.db, filtered_members)
670 } else {
671 // Non-union: use standard exclusion
672 self.narrow_excluding_type(resolved_source, instance_type)
673 }
674 }
675 }
676
677 /// Narrow a type to include only members assignable to target.
678 pub fn narrow_to_type(&self, source_type: TypeId, target_type: TypeId) -> TypeId {
679 let _span = span!(
680 Level::TRACE,
681 "narrow_to_type",
682 source_type = source_type.0,
683 target_type = target_type.0
684 )
685 .entered();
686
687 // CRITICAL FIX: Resolve Lazy/Ref types to inspect their structure.
688 // This fixes the "Missing type resolution" bug where type aliases and
689 // generics weren't being narrowed correctly.
690 let resolved_source = self.resolve_type(source_type);
691
692 // Gracefully handle resolution failures: if evaluation fails but the input
693 // wasn't ERROR, we can't narrow structurally. Return original source to
694 // avoid cascading ERRORs through the type system.
695 if resolved_source == TypeId::ERROR && source_type != TypeId::ERROR {
696 trace!("Source type resolution failed, returning original source");
697 return source_type;
698 }
699
700 // Resolve target for consistency
701 let resolved_target = self.resolve_type(target_type);
702 if resolved_target == TypeId::ERROR && target_type != TypeId::ERROR {
703 trace!("Target type resolution failed, returning original source");
704 return source_type;
705 }
706
707 // If source is the target, return it
708 if resolved_source == resolved_target {
709 trace!("Source type equals target type, returning unchanged");
710 return source_type;
711 }
712
713 // Special case: unknown can be narrowed to any type through type guards
714 // This handles cases like: if (typeof x === "string") where x: unknown
715 if resolved_source == TypeId::UNKNOWN {
716 trace!("Narrowing unknown to specific type via type guard");
717 return target_type;
718 }
719
720 // Special case: any can be narrowed to any type through type guards
721 // This handles cases like: if (x === null) where x: any
722 // CRITICAL: Unlike unknown, any MUST be narrowed to match target type
723 if resolved_source == TypeId::ANY {
724 trace!("Narrowing any to specific type via type guard");
725 return target_type;
726 }
727
728 // If source is a union, filter members
729 // Use resolved_source for structural inspection
730 if let Some(members) = union_list_id(self.db, resolved_source) {
731 let members = self.db.type_list(members);
732 trace!(
733 "Narrowing union with {} members to type {}",
734 members.len(),
735 target_type.0
736 );
737 let matching: Vec<TypeId> = members
738 .iter()
739 .filter_map(|&member| {
740 if let Some(narrowed) = self.narrow_type_param(member, target_type) {
741 return Some(narrowed);
742 }
743 if self.is_assignable_to(member, target_type) {
744 return Some(member);
745 }
746 // CRITICAL FIX: Check if target_type is a subtype of member
747 // This handles cases like narrowing string | number by "hello"
748 // where "hello" is a subtype of string, so we should narrow to "hello"
749 if crate::subtype::is_subtype_of_with_db(self.db, target_type, member) {
750 return Some(target_type);
751 }
752 // CRITICAL FIX: instanceof Array matching
753 // When narrowing by `instanceof Array`, if the member is array-like and target
754 // is a Lazy/Application type (which includes Array<T> interface references),
755 // assume it's the global Array and match the member.
756 // This handles: `x: Message | Message[]` with `instanceof Array` should keep `Message[]`.
757 // At runtime, instanceof only checks prototype chain, not generic type arguments.
758 if self.is_array_like(member) {
759 use crate::type_queries;
760 // Check if target is a type reference or generic application (Array<T>)
761 let is_target_lazy_or_app = type_queries::is_type_reference(self.db, resolved_target)
762 || type_queries::is_generic_type(self.db, resolved_target);
763
764 trace!("Member is array-like: member={}, target={}, is_target_lazy_or_app={}",
765 member.0, resolved_target.0, is_target_lazy_or_app);
766
767 if is_target_lazy_or_app {
768 trace!("Array member with lazy/app target (likely Array interface), keeping member");
769 return Some(member);
770 }
771 }
772 None
773 })
774 .collect();
775
776 if matching.is_empty() {
777 trace!("No matching members found, returning NEVER");
778 return TypeId::NEVER;
779 } else if matching.len() == 1 {
780 trace!("Found single matching member, returning {}", matching[0].0);
781 return matching[0];
782 }
783 trace!(
784 "Found {} matching members, creating new union",
785 matching.len()
786 );
787 return self.db.union(matching);
788 }
789
790 // Check if this is a type parameter that needs narrowing
791 // Use resolved_source to handle type parameters behind aliases
792 if let Some(narrowed) = self.narrow_type_param(resolved_source, target_type) {
793 trace!("Narrowed type parameter to {}", narrowed.0);
794 return narrowed;
795 }
796
797 // Task 13: Handle boolean -> literal narrowing
798 // When narrowing boolean to true or false, return the corresponding literal
799 if resolved_source == TypeId::BOOLEAN {
800 let is_target_true = if let Some(lit) = literal_value(self.db, resolved_target) {
801 matches!(lit, LiteralValue::Boolean(true))
802 } else {
803 resolved_target == TypeId::BOOLEAN_TRUE
804 };
805
806 if is_target_true {
807 trace!("Narrowing boolean to true");
808 return TypeId::BOOLEAN_TRUE;
809 }
810
811 let is_target_false = if let Some(lit) = literal_value(self.db, resolved_target) {
812 matches!(lit, LiteralValue::Boolean(false))
813 } else {
814 resolved_target == TypeId::BOOLEAN_FALSE
815 };
816
817 if is_target_false {
818 trace!("Narrowing boolean to false");
819 return TypeId::BOOLEAN_FALSE;
820 }
821 }
822
823 // Check if source is assignable to target using resolved types for comparison
824 if self.is_assignable_to(resolved_source, resolved_target) {
825 trace!("Source type is assignable to target, returning source");
826 source_type
827 } else if crate::subtype::is_subtype_of_with_db(self.db, resolved_target, resolved_source) {
828 // CRITICAL FIX: Check if target is a subtype of source (reverse narrowing)
829 // This handles cases like narrowing string to "hello" where "hello" is a subtype of string
830 // The inference engine uses this to narrow upper bounds by lower bounds
831 trace!("Target is subtype of source, returning target");
832 target_type
833 } else {
834 trace!("Source type is not assignable to target, returning NEVER");
835 TypeId::NEVER
836 }
837 }
838
839 /// Narrow a type by instanceof check using the instance type.
840 ///
841 /// Unlike `narrow_to_type` which uses structural assignability to filter union members,
842 /// this method uses instanceof-specific semantics:
843 /// - Type parameters with constraints assignable to the target are kept (intersected)
844 /// - When a type parameter absorbs the target, anonymous object types are excluded
845 /// since they cannot be class instances at runtime
846 ///
847 /// This prevents anonymous object types like `{ x: string }` from surviving instanceof
848 /// narrowing when they happen to be structurally compatible with the class type.
849 pub fn narrow_by_instance_type(&self, source_type: TypeId, instance_type: TypeId) -> TypeId {
850 let resolved_source = self.resolve_type(source_type);
851
852 if resolved_source == TypeId::ERROR && source_type != TypeId::ERROR {
853 return source_type;
854 }
855
856 let resolved_target = self.resolve_type(instance_type);
857 if resolved_target == TypeId::ERROR && instance_type != TypeId::ERROR {
858 return source_type;
859 }
860
861 if resolved_source == resolved_target {
862 return source_type;
863 }
864
865 // any/unknown narrow to instance type with instanceof
866 if resolved_source == TypeId::ANY || resolved_source == TypeId::UNKNOWN {
867 return instance_type;
868 }
869
870 // If source is a union, filter members using instanceof semantics
871 if let Some(members) = union_list_id(self.db, resolved_source) {
872 let members = self.db.type_list(members);
873 trace!(
874 "instanceof: narrowing union with {} members {:?} to instance type {}",
875 members.len(),
876 members.iter().map(|m| m.0).collect::<Vec<_>>(),
877 instance_type.0
878 );
879
880 // First pass: check if any type parameter matches the instance type.
881 let mut type_param_results: Vec<(usize, TypeId)> = Vec::new();
882 for (i, &member) in members.iter().enumerate() {
883 if let Some(narrowed) = self.narrow_type_param(member, instance_type) {
884 type_param_results.push((i, narrowed));
885 }
886 }
887
888 let matching: Vec<TypeId> = if !type_param_results.is_empty() {
889 // Type parameter(s) matched: keep type params and exclude anonymous
890 // object types that can't be class instances at runtime.
891 let mut result = Vec::new();
892 let tp_indices: Vec<usize> = type_param_results.iter().map(|(i, _)| *i).collect();
893 for &(_, narrowed) in &type_param_results {
894 result.push(narrowed);
895 }
896 for (i, &member) in members.iter().enumerate() {
897 if tp_indices.contains(&i) {
898 continue;
899 }
900 if crate::type_queries::is_object_type(self.db, member) {
901 trace!(
902 "instanceof: excluding anonymous object {} (type param absorbs)",
903 member.0
904 );
905 continue;
906 }
907 if crate::subtype::is_subtype_of_with_db(self.db, member, instance_type) {
908 result.push(member);
909 } else if crate::subtype::is_subtype_of_with_db(self.db, instance_type, member)
910 {
911 result.push(instance_type);
912 }
913 }
914 result
915 } else {
916 // No type parameter match: filter by instanceof semantics.
917 // Primitives can never pass instanceof; non-primitives are
918 // checked for assignability with the instance type.
919 members
920 .iter()
921 .filter_map(|&member| {
922 // Primitive types can never pass `instanceof` at runtime.
923 if self.is_js_primitive(member) {
924 return None;
925 }
926 if let Some(narrowed) = self.narrow_type_param(member, instance_type) {
927 return Some(narrowed);
928 }
929 // Member assignable to instance type → keep member
930 if self.is_assignable_to(member, instance_type) {
931 return Some(member);
932 }
933 // Instance type assignable to member → narrow to instance
934 // (e.g., member=Animal, instance=Dog → Dog)
935 if self.is_assignable_to(instance_type, member) {
936 return Some(instance_type);
937 }
938 // Neither direction holds — create intersection per tsc
939 // semantics. This handles cases like Date instanceof Object
940 // where assignability checks may miss the relationship.
941 // The intersection preserves the member's shape while
942 // constraining it to the instance type.
943 Some(self.db.intersection2(member, instance_type))
944 })
945 .collect()
946 };
947
948 if matching.is_empty() {
949 return self.narrow_to_type(source_type, instance_type);
950 } else if matching.len() == 1 {
951 return matching[0];
952 }
953 return self.db.union(matching);
954 }
955
956 // Non-union: use instanceof-specific semantics
957 trace!(
958 "instanceof: non-union path for source_type={}",
959 source_type.0
960 );
961
962 // Try type parameter narrowing first (produces T & InstanceType)
963 if let Some(narrowed) = self.narrow_type_param(resolved_source, instance_type) {
964 return narrowed;
965 }
966
967 // For non-primitive, non-type-param source types, instanceof narrowing
968 // should keep them when there's a potential runtime relationship.
969 // This handles cases like `readonly number[]` narrowed by `instanceof Array`:
970 // - readonly number[] is NOT a subtype of Array<T> (missing mutating methods)
971 // - Array<T> is NOT a subtype of readonly number[] (unbound T)
972 // - But at runtime, a readonly array IS an Array instance
973 if !self.is_js_primitive(resolved_source) {
974 if self.is_assignable_to(resolved_source, instance_type) {
975 return source_type;
976 }
977 if self.is_assignable_to(instance_type, resolved_source) {
978 return instance_type;
979 }
980 // Non-primitive types may still be instances at runtime.
981 // Neither direction holds — create intersection per tsc semantics.
982 // This handles cases like `interface I {}` narrowed by `instanceof RegExp`.
983 return self.db.intersection2(source_type, instance_type);
984 }
985 // Primitives can never pass instanceof
986 TypeId::NEVER
987 }
988
989 /// Narrow a type for the false branch of `instanceof`.
990 ///
991 /// Keeps primitive types (which can never pass instanceof) and excludes
992 /// non-primitive members that are subtypes of the instance type.
993 /// For example, `string | number | Date` with `instanceof Object` false
994 /// branch gives `string | number` (Date is excluded as it's an Object instance).
995 pub fn narrow_by_instanceof_false(&self, source_type: TypeId, instance_type: TypeId) -> TypeId {
996 let resolved_source = self.resolve_type(source_type);
997
998 if let Some(members) = union_list_id(self.db, resolved_source) {
999 let members = self.db.type_list(members);
1000 let remaining: Vec<TypeId> = members
1001 .iter()
1002 .filter(|&&member| {
1003 // Primitives always survive the false branch of instanceof
1004 if self.is_js_primitive(member) {
1005 return true;
1006 }
1007 // A member only fails to reach the false branch if it is GUARANTEED
1008 // to pass the true branch. In TypeScript, this means the member
1009 // is assignable to the instance type.
1010 // If it is NOT assignable, it MIGHT fail at runtime, so we MUST keep it.
1011 !self.is_assignable_to(member, instance_type)
1012 })
1013 .copied()
1014 .collect();
1015
1016 if remaining.is_empty() {
1017 return TypeId::NEVER;
1018 } else if remaining.len() == 1 {
1019 return remaining[0];
1020 }
1021 return self.db.union(remaining);
1022 }
1023
1024 // Non-union: if it's guaranteed to be an instance, it will never reach the false branch.
1025 if self.is_assignable_to(resolved_source, instance_type) {
1026 return TypeId::NEVER;
1027 }
1028
1029 // Otherwise, it might reach the false branch, so we keep the original type.
1030 source_type
1031 }
1032
1033 /// Check if a literal type is assignable to a target for narrowing purposes.
1034 ///
1035 /// Handles union decomposition: if the target is a union, checks each member.
1036 /// Falls back to `narrow_to_type` to determine if the literal can narrow to the target.
1037 pub fn literal_assignable_to(&self, literal: TypeId, target: TypeId) -> bool {
1038 if literal == target || target == TypeId::ANY || target == TypeId::UNKNOWN {
1039 return true;
1040 }
1041
1042 if let UnionMembersKind::Union(members) = classify_for_union_members(self.db, target) {
1043 return members
1044 .iter()
1045 .any(|&member| self.literal_assignable_to(literal, member));
1046 }
1047
1048 self.narrow_to_type(literal, target) != TypeId::NEVER
1049 }
1050
1051 /// Narrow a type to exclude members assignable to target.
1052 pub fn narrow_excluding_type(&self, source_type: TypeId, excluded_type: TypeId) -> TypeId {
1053 if let Some(members) = intersection_list_id(self.db, source_type) {
1054 let members = self.db.type_list(members);
1055 let mut narrowed_members = Vec::with_capacity(members.len());
1056 let mut changed = false;
1057 for &member in members.iter() {
1058 let narrowed = self.narrow_excluding_type(member, excluded_type);
1059 if narrowed == TypeId::NEVER {
1060 return TypeId::NEVER;
1061 }
1062 if narrowed != member {
1063 changed = true;
1064 }
1065 narrowed_members.push(narrowed);
1066 }
1067 if !changed {
1068 return source_type;
1069 }
1070 return self.db.intersection(narrowed_members);
1071 }
1072
1073 // If source is a union, filter out matching members
1074 if let Some(members) = union_list_id(self.db, source_type) {
1075 let members = self.db.type_list(members);
1076 let remaining: Vec<TypeId> = members
1077 .iter()
1078 .filter_map(|&member| {
1079 if intersection_list_id(self.db, member).is_some() {
1080 return self
1081 .narrow_excluding_type(member, excluded_type)
1082 .non_never();
1083 }
1084 if let Some(narrowed) = self.narrow_type_param_excluding(member, excluded_type)
1085 {
1086 return narrowed.non_never();
1087 }
1088 if self.is_assignable_to(member, excluded_type) {
1089 None
1090 } else {
1091 Some(member)
1092 }
1093 })
1094 .collect();
1095
1096 tracing::trace!(
1097 remaining_count = remaining.len(),
1098 remaining = ?remaining.iter().map(|t| t.0).collect::<Vec<_>>(),
1099 "narrow_excluding_type: union filter result"
1100 );
1101 if remaining.is_empty() {
1102 return TypeId::NEVER;
1103 } else if remaining.len() == 1 {
1104 return remaining[0];
1105 }
1106 return self.db.union(remaining);
1107 }
1108
1109 if let Some(narrowed) = self.narrow_type_param_excluding(source_type, excluded_type) {
1110 return narrowed;
1111 }
1112
1113 // Special case: boolean type (treat as true | false union)
1114 // Task 13: Fix Boolean Narrowing Logic
1115 // When excluding true or false from boolean, return the other literal
1116 // When excluding both true and false from boolean, return never
1117 if source_type == TypeId::BOOLEAN
1118 || source_type == TypeId::BOOLEAN_TRUE
1119 || source_type == TypeId::BOOLEAN_FALSE
1120 {
1121 // Check if excluded_type is a boolean literal
1122 let is_excluding_true = if let Some(lit) = literal_value(self.db, excluded_type) {
1123 matches!(lit, LiteralValue::Boolean(true))
1124 } else {
1125 excluded_type == TypeId::BOOLEAN_TRUE
1126 };
1127
1128 let is_excluding_false = if let Some(lit) = literal_value(self.db, excluded_type) {
1129 matches!(lit, LiteralValue::Boolean(false))
1130 } else {
1131 excluded_type == TypeId::BOOLEAN_FALSE
1132 };
1133
1134 // Handle exclusion from boolean, true, or false
1135 if source_type == TypeId::BOOLEAN {
1136 if is_excluding_true {
1137 // Excluding true from boolean -> return false
1138 return TypeId::BOOLEAN_FALSE;
1139 } else if is_excluding_false {
1140 // Excluding false from boolean -> return true
1141 return TypeId::BOOLEAN_TRUE;
1142 }
1143 // If excluding BOOLEAN, let the final is_assignable_to check handle it below
1144 } else if source_type == TypeId::BOOLEAN_TRUE {
1145 if is_excluding_true {
1146 // Excluding true from true -> return never
1147 return TypeId::NEVER;
1148 }
1149 // For other cases (e.g., excluding BOOLEAN from TRUE),
1150 // let the final is_assignable_to check handle it below
1151 } else if source_type == TypeId::BOOLEAN_FALSE && is_excluding_false {
1152 // Excluding false from false -> return never
1153 return TypeId::NEVER;
1154 }
1155 // For other cases, let the final is_assignable_to check handle it below
1156 // CRITICAL: Do NOT return source_type here.
1157 // Fall through to the standard is_assignable_to check below.
1158 // This handles edge cases like narrow_excluding_type(TRUE, BOOLEAN) -> NEVER
1159 }
1160
1161 // If source is assignable to excluded, return never
1162 if self.is_assignable_to(source_type, excluded_type) {
1163 TypeId::NEVER
1164 } else {
1165 source_type
1166 }
1167 }
1168
1169 /// Narrow a type by excluding multiple types at once (batched version).
1170 ///
1171 /// This is an optimized version of `narrow_excluding_type` for cases like
1172 /// switch default clauses where we need to exclude many types at once.
1173 /// It avoids creating intermediate union types and reduces complexity from O(N²) to O(N).
1174 ///
1175 /// # Arguments
1176 /// * `source_type` - The type to narrow (typically a union)
1177 /// * `excluded_types` - Types to exclude from the source
1178 ///
1179 /// # Returns
1180 /// The narrowed type with all excluded types removed
1181 pub fn narrow_excluding_types(&self, source_type: TypeId, excluded_types: &[TypeId]) -> TypeId {
1182 if excluded_types.is_empty() {
1183 return source_type;
1184 }
1185
1186 // For small lists, use sequential narrowing (avoids HashSet overhead)
1187 if excluded_types.len() <= 4 {
1188 let mut result = source_type;
1189 for &excluded in excluded_types {
1190 result = self.narrow_excluding_type(result, excluded);
1191 if result == TypeId::NEVER {
1192 return TypeId::NEVER;
1193 }
1194 }
1195 return result;
1196 }
1197
1198 // For larger lists, use HashSet for O(1) lookup
1199 let excluded_set: rustc_hash::FxHashSet<TypeId> = excluded_types.iter().copied().collect();
1200
1201 // Handle union source type
1202 if let Some(members) = union_list_id(self.db, source_type) {
1203 let members = self.db.type_list(members);
1204 let remaining: Vec<TypeId> = members
1205 .iter()
1206 .filter_map(|&member| {
1207 // Fast path: direct identity check against the set
1208 if excluded_set.contains(&member) {
1209 return None;
1210 }
1211
1212 // Handle intersection members
1213 if intersection_list_id(self.db, member).is_some() {
1214 return self
1215 .narrow_excluding_types(member, excluded_types)
1216 .non_never();
1217 }
1218
1219 // Handle type parameters
1220 if let Some(narrowed) =
1221 self.narrow_type_param_excluding_set(member, &excluded_set)
1222 {
1223 return narrowed.non_never();
1224 }
1225
1226 // Slow path: check assignability for complex cases
1227 // This handles cases where the member isn't identical to an excluded type
1228 // but might still be assignable to one (e.g., literal subtypes)
1229 for &excluded in &excluded_set {
1230 if self.is_assignable_to(member, excluded) {
1231 return None;
1232 }
1233 }
1234 Some(member)
1235 })
1236 .collect();
1237
1238 if remaining.is_empty() {
1239 return TypeId::NEVER;
1240 } else if remaining.len() == 1 {
1241 return remaining[0];
1242 }
1243 return self.db.union(remaining);
1244 }
1245
1246 // Handle single type (not a union)
1247 if excluded_set.contains(&source_type) {
1248 return TypeId::NEVER;
1249 }
1250
1251 // Check assignability for single type
1252 for &excluded in &excluded_set {
1253 if self.is_assignable_to(source_type, excluded) {
1254 return TypeId::NEVER;
1255 }
1256 }
1257
1258 source_type
1259 }
1260
1261 /// Helper for `narrow_excluding_types` with type parameters
1262 fn narrow_type_param_excluding_set(
1263 &self,
1264 source: TypeId,
1265 excluded_set: &rustc_hash::FxHashSet<TypeId>,
1266 ) -> Option<TypeId> {
1267 let info = type_param_info(self.db, source)?;
1268
1269 let constraint = info.constraint?;
1270 if constraint == source || constraint == TypeId::UNKNOWN {
1271 return None;
1272 }
1273
1274 // Narrow the constraint by excluding all types in the set
1275 let excluded_vec: Vec<TypeId> = excluded_set.iter().copied().collect();
1276 let narrowed_constraint = self.narrow_excluding_types(constraint, &excluded_vec);
1277
1278 if narrowed_constraint == constraint {
1279 return None;
1280 }
1281 if narrowed_constraint == TypeId::NEVER {
1282 return Some(TypeId::NEVER);
1283 }
1284
1285 Some(self.db.intersection2(source, narrowed_constraint))
1286 }
1287
1288 /// Narrow to function types only.
1289 fn narrow_to_function(&self, source_type: TypeId) -> TypeId {
1290 if let Some(members) = union_list_id(self.db, source_type) {
1291 let members = self.db.type_list(members);
1292 let functions: Vec<TypeId> = members
1293 .iter()
1294 .filter_map(|&member| {
1295 if let Some(narrowed) = self.narrow_type_param_to_function(member) {
1296 return narrowed.non_never();
1297 }
1298 self.is_function_type(member).then_some(member)
1299 })
1300 .collect();
1301
1302 return union_or_single(self.db, functions);
1303 }
1304
1305 if let Some(narrowed) = self.narrow_type_param_to_function(source_type) {
1306 return narrowed;
1307 }
1308
1309 if self.is_function_type(source_type) {
1310 source_type
1311 } else if source_type == TypeId::OBJECT {
1312 self.function_type()
1313 } else if let Some(shape_id) = object_shape_id(self.db, source_type) {
1314 let shape = self.db.object_shape(shape_id);
1315 if shape.properties.is_empty() {
1316 self.function_type()
1317 } else {
1318 TypeId::NEVER
1319 }
1320 } else if let Some(shape_id) = object_with_index_shape_id(self.db, source_type) {
1321 let shape = self.db.object_shape(shape_id);
1322 if shape.properties.is_empty()
1323 && shape.string_index.is_none()
1324 && shape.number_index.is_none()
1325 {
1326 self.function_type()
1327 } else {
1328 TypeId::NEVER
1329 }
1330 } else if index_access_parts(self.db, source_type).is_some() {
1331 // For indexed access types like T[K], narrow to T[K] & Function
1332 // This handles cases like: typeof obj[key] === 'function'
1333 let function_type = self.function_type();
1334 self.db.intersection2(source_type, function_type)
1335 } else {
1336 TypeId::NEVER
1337 }
1338 }
1339
1340 /// Check if a type is a function type.
1341 /// Uses the visitor pattern from `solver::visitor`.
1342 fn is_function_type(&self, type_id: TypeId) -> bool {
1343 is_function_type_db(self.db, type_id)
1344 }
1345
1346 /// Narrow a type to exclude function-like members (typeof !== "function").
1347 pub fn narrow_excluding_function(&self, source_type: TypeId) -> TypeId {
1348 if let Some(members) = union_list_id(self.db, source_type) {
1349 let members = self.db.type_list(members);
1350 let remaining: Vec<TypeId> = members
1351 .iter()
1352 .filter_map(|&member| {
1353 if let Some(narrowed) = self.narrow_type_param_excluding_function(member) {
1354 return narrowed.non_never();
1355 }
1356 if self.is_function_type(member) {
1357 None
1358 } else {
1359 Some(member)
1360 }
1361 })
1362 .collect();
1363
1364 return union_or_single(self.db, remaining);
1365 }
1366
1367 if let Some(narrowed) = self.narrow_type_param_excluding_function(source_type) {
1368 return narrowed;
1369 }
1370
1371 if self.is_function_type(source_type) {
1372 TypeId::NEVER
1373 } else {
1374 source_type
1375 }
1376 }
1377
1378 /// Check if a type has typeof "object".
1379 /// Uses the visitor pattern from `solver::visitor`.
1380 fn is_object_typeof(&self, type_id: TypeId) -> bool {
1381 is_object_like_type_db(self.db, type_id)
1382 }
1383
1384 fn narrow_type_param(&self, source: TypeId, target: TypeId) -> Option<TypeId> {
1385 let info = type_param_info(self.db, source)?;
1386
1387 let constraint = info.constraint.unwrap_or(TypeId::UNKNOWN);
1388 if constraint == source {
1389 return None;
1390 }
1391
1392 let narrowed_constraint = if constraint == TypeId::UNKNOWN {
1393 target
1394 } else {
1395 self.narrow_to_type(constraint, target)
1396 };
1397
1398 if narrowed_constraint == TypeId::NEVER {
1399 return None;
1400 }
1401
1402 Some(self.db.intersection2(source, narrowed_constraint))
1403 }
1404
1405 fn narrow_type_param_to_function(&self, source: TypeId) -> Option<TypeId> {
1406 let info = type_param_info(self.db, source)?;
1407
1408 let constraint = info.constraint.unwrap_or(TypeId::UNKNOWN);
1409 if constraint == source || constraint == TypeId::UNKNOWN {
1410 let function_type = self.function_type();
1411 return Some(self.db.intersection2(source, function_type));
1412 }
1413
1414 let narrowed_constraint = self.narrow_to_function(constraint);
1415 if narrowed_constraint == TypeId::NEVER {
1416 return None;
1417 }
1418
1419 Some(self.db.intersection2(source, narrowed_constraint))
1420 }
1421
1422 fn narrow_type_param_excluding(&self, source: TypeId, excluded: TypeId) -> Option<TypeId> {
1423 let info = type_param_info(self.db, source)?;
1424
1425 let constraint = info.constraint?;
1426 if constraint == source || constraint == TypeId::UNKNOWN {
1427 return None;
1428 }
1429
1430 let narrowed_constraint = self.narrow_excluding_type(constraint, excluded);
1431 if narrowed_constraint == constraint {
1432 return None;
1433 }
1434 if narrowed_constraint == TypeId::NEVER {
1435 return Some(TypeId::NEVER);
1436 }
1437
1438 Some(self.db.intersection2(source, narrowed_constraint))
1439 }
1440
1441 fn narrow_type_param_excluding_function(&self, source: TypeId) -> Option<TypeId> {
1442 let info = type_param_info(self.db, source)?;
1443
1444 let constraint = info.constraint.unwrap_or(TypeId::UNKNOWN);
1445 if constraint == source || constraint == TypeId::UNKNOWN {
1446 return Some(source);
1447 }
1448
1449 let narrowed_constraint = self.narrow_excluding_function(constraint);
1450 if narrowed_constraint == constraint {
1451 return Some(source);
1452 }
1453 if narrowed_constraint == TypeId::NEVER {
1454 return Some(TypeId::NEVER);
1455 }
1456
1457 Some(self.db.intersection2(source, narrowed_constraint))
1458 }
1459
1460 pub(crate) fn function_type(&self) -> TypeId {
1461 let rest_array = self.db.array(TypeId::ANY);
1462 let rest_param = ParamInfo {
1463 name: None,
1464 type_id: rest_array,
1465 optional: false,
1466 rest: true,
1467 };
1468 self.db.function(FunctionShape {
1469 params: vec![rest_param],
1470 this_type: None,
1471 return_type: TypeId::ANY,
1472 type_params: Vec::new(),
1473 type_predicate: None,
1474 is_constructor: false,
1475 is_method: false,
1476 })
1477 }
1478
1479 /// Check if a type is a JS primitive that can never pass `instanceof`.
1480 /// Includes string, number, boolean, bigint, symbol, undefined, null,
1481 /// void, never, and their literal forms.
1482 fn is_js_primitive(&self, type_id: TypeId) -> bool {
1483 matches!(
1484 type_id,
1485 TypeId::STRING
1486 | TypeId::NUMBER
1487 | TypeId::BOOLEAN
1488 | TypeId::BIGINT
1489 | TypeId::SYMBOL
1490 | TypeId::UNDEFINED
1491 | TypeId::NULL
1492 | TypeId::VOID
1493 | TypeId::NEVER
1494 | TypeId::BOOLEAN_TRUE
1495 | TypeId::BOOLEAN_FALSE
1496 ) || matches!(self.db.lookup(type_id), Some(TypeData::Literal(_)))
1497 }
1498
1499 /// Simple assignability check for narrowing purposes.
1500 fn is_assignable_to(&self, source: TypeId, target: TypeId) -> bool {
1501 if source == target {
1502 return true;
1503 }
1504
1505 // never is assignable to everything
1506 if source == TypeId::NEVER {
1507 return true;
1508 }
1509
1510 // everything is assignable to any/unknown
1511 if target.is_any_or_unknown() {
1512 return true;
1513 }
1514
1515 // Literal to base type
1516 if let Some(lit) = literal_value(self.db, source) {
1517 match (lit, target) {
1518 (LiteralValue::String(_), t) if t == TypeId::STRING => return true,
1519 (LiteralValue::Number(_), t) if t == TypeId::NUMBER => return true,
1520 (LiteralValue::Boolean(_), t) if t == TypeId::BOOLEAN => return true,
1521 (LiteralValue::BigInt(_), t) if t == TypeId::BIGINT => return true,
1522 _ => {}
1523 }
1524 }
1525
1526 // object/null for typeof "object"
1527 if target == TypeId::OBJECT {
1528 if source == TypeId::NULL {
1529 return true;
1530 }
1531 if self.is_object_typeof(source) {
1532 return true;
1533 }
1534 return false;
1535 }
1536
1537 if let Some(members) = intersection_list_id(self.db, source) {
1538 let members = self.db.type_list(members);
1539 if members
1540 .iter()
1541 .any(|member| self.is_assignable_to(*member, target))
1542 {
1543 return true;
1544 }
1545 }
1546
1547 if target == TypeId::STRING && template_literal_id(self.db, source).is_some() {
1548 return true;
1549 }
1550
1551 // Check if source is assignable to any member of a union target
1552 if let Some(members) = union_list_id(self.db, target) {
1553 let members = self.db.type_list(members);
1554 if members
1555 .iter()
1556 .any(|&member| self.is_assignable_to(source, member))
1557 {
1558 return true;
1559 }
1560 }
1561
1562 // Fallback: use full structural/nominal subtype check.
1563 // This handles class inheritance (Derived extends Base), interface
1564 // implementations, and other structural relationships that the
1565 // fast-path checks above don't cover.
1566 // CRITICAL: Resolve Lazy(DefId) types before the subtype check.
1567 // Without resolution, two unrelated interfaces (e.g., Cat and Dog)
1568 // remain as opaque Lazy types and the SubtypeChecker can't distinguish them.
1569 let source = self.resolve_type(source);
1570 let target = self.resolve_type(target);
1571 if source == target {
1572 return true;
1573 }
1574 crate::subtype::is_subtype_of_with_db(self.db, source, target)
1575 }
1576
1577 /// Applies a type guard to narrow a type.
1578 ///
1579 /// This is the main entry point for AST-agnostic type narrowing.
1580 /// The Checker extracts a `TypeGuard` from AST nodes, and the Solver
1581 /// applies it to compute the narrowed type.
1582 ///
1583 /// # Arguments
1584 /// * `source_type` - The type to narrow
1585 /// * `guard` - The guard condition (extracted from AST by Checker)
1586 /// * `sense` - If true, narrow for the "true" branch; if false, narrow for the "false" branch
1587 ///
1588 /// # Returns
1589 /// The narrowed type after applying the guard.
1590 ///
1591 /// # Examples
1592 /// ```ignore
1593 /// // typeof x === "string"
1594 /// let guard = TypeGuard::Typeof(TypeofKind::String);
1595 /// let narrowed = narrowing.narrow_type(string_or_number, &guard, true);
1596 /// assert_eq!(narrowed, TypeId::STRING);
1597 ///
1598 /// // x !== null (negated sense)
1599 /// let guard = TypeGuard::NullishEquality;
1600 /// let narrowed = narrowing.narrow_type(string_or_null, &guard, false);
1601 /// // Result should exclude null and undefined
1602 /// ```
1603 pub fn narrow_type(&self, source_type: TypeId, guard: &TypeGuard, sense: bool) -> TypeId {
1604 match guard {
1605 TypeGuard::Typeof(typeof_kind) => {
1606 let type_name = typeof_kind.as_str();
1607 if sense {
1608 self.narrow_by_typeof(source_type, type_name)
1609 } else {
1610 // Negation: exclude typeof type
1611 self.narrow_by_typeof_negation(source_type, type_name)
1612 }
1613 }
1614
1615 TypeGuard::Instanceof(instance_type) => {
1616 if sense {
1617 // Positive: x instanceof Class
1618 // Special case: `unknown` instanceof X narrows to X (or object if X unknown)
1619 // This must be handled here in the solver, not in the checker.
1620 if source_type == TypeId::UNKNOWN {
1621 return *instance_type;
1622 }
1623
1624 // CRITICAL: The payload is already the Instance Type (extracted by Checker)
1625 // Use narrow_by_instance_type for instanceof-specific semantics:
1626 // type parameters with matching constraints are kept, but anonymous
1627 // object types that happen to be structurally compatible are excluded.
1628 // Primitive types are filtered out since they can never pass instanceof.
1629 let narrowed = self.narrow_by_instance_type(source_type, *instance_type);
1630
1631 if narrowed != TypeId::NEVER || source_type == TypeId::NEVER {
1632 return narrowed;
1633 }
1634
1635 // Fallback 1: If standard narrowing returns NEVER but source wasn't NEVER,
1636 // it might be an interface vs class check (which is allowed in TS).
1637 // Use intersection in that case.
1638 let intersection = self.db.intersection2(source_type, *instance_type);
1639 if intersection != TypeId::NEVER {
1640 return intersection;
1641 }
1642
1643 // Fallback 2: If even intersection fails, narrow to object-like types.
1644 // On the true branch of instanceof, we know the value must be some
1645 // kind of object (primitives can never pass instanceof).
1646 self.narrow_to_objectish(source_type)
1647 } else {
1648 // Negative: !(x instanceof Class)
1649 // Keep primitives (they can never pass instanceof) and exclude
1650 // non-primitive types assignable to the instance type.
1651 if *instance_type == TypeId::OBJECT {
1652 source_type
1653 } else {
1654 self.narrow_by_instanceof_false(source_type, *instance_type)
1655 }
1656 }
1657 }
1658
1659 TypeGuard::LiteralEquality(literal_type) => {
1660 if sense {
1661 // Equality: narrow to the literal type
1662 self.narrow_to_type(source_type, *literal_type)
1663 } else {
1664 // Inequality: exclude the literal type
1665 self.narrow_excluding_type(source_type, *literal_type)
1666 }
1667 }
1668
1669 TypeGuard::NullishEquality => {
1670 if sense {
1671 // Equality with null: narrow to null | undefined
1672 self.db.union(vec![TypeId::NULL, TypeId::UNDEFINED])
1673 } else {
1674 // Inequality: exclude null and undefined
1675 let without_null = self.narrow_excluding_type(source_type, TypeId::NULL);
1676 self.narrow_excluding_type(without_null, TypeId::UNDEFINED)
1677 }
1678 }
1679
1680 TypeGuard::Truthy => {
1681 if sense {
1682 // Truthy: remove null and undefined (TypeScript doesn't narrow other falsy values)
1683 self.narrow_by_truthiness(source_type)
1684 } else {
1685 // Falsy: narrow to the falsy component(s)
1686 // This handles cases like: if (!x) where x: string → "" in false branch
1687 self.narrow_to_falsy(source_type)
1688 }
1689 }
1690
1691 TypeGuard::Discriminant {
1692 property_path,
1693 value_type,
1694 } => {
1695 // Use narrow_by_discriminant_for_type which handles type parameters
1696 // by narrowing the constraint and returning T & NarrowedConstraint
1697 self.narrow_by_discriminant_for_type(source_type, property_path, *value_type, sense)
1698 }
1699
1700 TypeGuard::InProperty(property_name) => {
1701 if sense {
1702 // Positive: "prop" in x - narrow to types that have the property
1703 self.narrow_by_property_presence(source_type, *property_name, true)
1704 } else {
1705 // Negative: !("prop" in x) - narrow to types that don't have the property
1706 self.narrow_by_property_presence(source_type, *property_name, false)
1707 }
1708 }
1709
1710 TypeGuard::Predicate { type_id, asserts } => {
1711 match type_id {
1712 Some(target_type) => {
1713 // Type guard with specific type: is T or asserts T
1714 if sense {
1715 // True branch: narrow source to the predicate type.
1716 // Following TSC's narrowType logic:
1717 // 1. For unions: filter members using narrow_to_type
1718 // 2. For non-unions:
1719 // a. source <: target → return source
1720 // b. target <: source → return target
1721 // c. otherwise → return source & target
1722 //
1723 // Following TSC's narrowType logic which uses
1724 // isTypeSubtypeOf (not isTypeAssignableTo) to decide
1725 // whether source is already specific enough.
1726 //
1727 // If source is a strict subtype of the target, return
1728 // source (it's already more specific). If target is a
1729 // strict subtype of source, return target (narrowing
1730 // down). Otherwise, return the intersection.
1731 //
1732 // narrow_to_type uses assignability internally, which is
1733 // too loose for type predicates (e.g. {} is assignable to
1734 // Record<string,unknown> but not a subtype).
1735 let resolved_source = self.resolve_type(source_type);
1736
1737 if resolved_source == self.resolve_type(*target_type) {
1738 source_type
1739 } else if resolved_source == TypeId::UNKNOWN
1740 || resolved_source == TypeId::ANY
1741 {
1742 *target_type
1743 } else if union_list_id(self.db, resolved_source).is_some() {
1744 // For unions: filter members, fall back to
1745 // intersection if nothing matches.
1746 let narrowed = self.narrow_to_type(source_type, *target_type);
1747 if narrowed == TypeId::NEVER && source_type != TypeId::NEVER {
1748 self.db.intersection2(source_type, *target_type)
1749 } else {
1750 narrowed
1751 }
1752 } else {
1753 // Non-union source: use narrow_to_type first.
1754 // If it returns source unchanged (assignable but
1755 // possibly losing structural info) or NEVER (no
1756 // overlap), fall back to intersection.
1757 let narrowed = self.narrow_to_type(source_type, *target_type);
1758 if narrowed == source_type && narrowed != *target_type {
1759 // Source was unchanged — intersect to preserve
1760 // target's structural info (index sigs, etc.)
1761 self.db.intersection2(source_type, *target_type)
1762 } else if narrowed == TypeId::NEVER && source_type != TypeId::NEVER
1763 {
1764 self.db.intersection2(source_type, *target_type)
1765 } else {
1766 narrowed
1767 }
1768 }
1769 } else if *asserts {
1770 // CRITICAL: For assertion functions, the false branch is unreachable
1771 // (the function throws if the assertion fails), so we don't narrow
1772 source_type
1773 } else {
1774 // False branch for regular type guards: exclude the target type
1775 self.narrow_excluding_type(source_type, *target_type)
1776 }
1777 }
1778 None => {
1779 // Truthiness assertion: asserts x
1780 // Behaves like TypeGuard::Truthy (narrows to truthy in true branch)
1781 if *asserts {
1782 self.narrow_by_truthiness(source_type)
1783 } else {
1784 source_type
1785 }
1786 }
1787 }
1788 }
1789
1790 TypeGuard::Array => {
1791 if sense {
1792 // Positive: Array.isArray(x) - narrow to array-like types
1793 self.narrow_to_array(source_type)
1794 } else {
1795 // Negative: !Array.isArray(x) - exclude array-like types
1796 self.narrow_excluding_array(source_type)
1797 }
1798 }
1799
1800 TypeGuard::ArrayElementPredicate { element_type } => {
1801 trace!(
1802 ?element_type,
1803 ?sense,
1804 "Applying ArrayElementPredicate guard"
1805 );
1806 if sense {
1807 // True branch: narrow array element type
1808 let result = self.narrow_array_element_type(source_type, *element_type);
1809 trace!(?result, "ArrayElementPredicate narrowing result");
1810 result
1811 } else {
1812 // False branch: we don't narrow (arr.every could be false for various reasons)
1813 trace!("ArrayElementPredicate false branch, no narrowing");
1814 source_type
1815 }
1816 }
1817 }
1818 }
1819}
1820
1821#[cfg(test)]
1822#[path = "../tests/narrowing_tests.rs"]
1823mod tests;