Skip to main content

tsz_solver/narrowing/
discriminants.rs

1//! Discriminant-based narrowing for discriminated unions.
2//!
3//! Handles narrowing of types based on discriminant property checks.
4//! For example, `action.type === "add"` narrows `Action` to
5//! `{ type: "add", value: number }`.
6//!
7//! Key functions:
8//! - `find_discriminants`: Identifies discriminant properties in unions
9//! - `narrow_by_discriminant`: Narrows to matching union members
10//! - `narrow_by_excluding_discriminant`: Excludes matching union members
11
12use super::{DiscriminantInfo, NarrowingContext, union_or_single_preserve};
13use crate::operations::property::{PropertyAccessEvaluator, PropertyAccessResult};
14use crate::relations::subtype::is_subtype_of;
15use crate::type_queries::{
16    LiteralValueKind, UnionMembersKind, classify_for_literal_value, classify_for_union_members,
17};
18use crate::types::{PropertyLookup, TypeId};
19use crate::visitor::{
20    intersection_list_id, is_literal_type_db, object_shape_id, object_with_index_shape_id,
21    union_list_id,
22};
23use rustc_hash::FxHashSet;
24use tracing::{Level, span, trace};
25use tsz_common::interner::Atom;
26
27impl<'a> NarrowingContext<'a> {
28    /// Find discriminant properties in a union type.
29    ///
30    /// A discriminant property is one where:
31    /// 1. All union members have the property
32    /// 2. Each member has a unique literal type for that property
33    pub fn find_discriminants(&self, union_type: TypeId) -> Vec<DiscriminantInfo> {
34        let _span = span!(
35            Level::TRACE,
36            "find_discriminants",
37            union_type = union_type.0
38        )
39        .entered();
40
41        let members = match union_list_id(self.db, union_type) {
42            Some(members_id) => self.db.type_list(members_id),
43            None => return vec![],
44        };
45
46        if members.len() < 2 {
47            trace!("Union has fewer than 2 members, skipping discriminant search");
48            return vec![];
49        }
50
51        // Collect all property names from all members
52        let mut all_properties: Vec<Atom> = Vec::new();
53        let mut member_props: Vec<Vec<(Atom, TypeId)>> = Vec::new();
54
55        for &member in members.iter() {
56            if let Some(shape_id) = object_shape_id(self.db, member) {
57                let shape = self.db.object_shape(shape_id);
58                let props_vec: Vec<(Atom, TypeId)> = shape
59                    .properties
60                    .iter()
61                    .map(|p| (p.name, p.type_id))
62                    .collect();
63
64                // Track all property names
65                for (name, _) in &props_vec {
66                    if !all_properties.contains(name) {
67                        all_properties.push(*name);
68                    }
69                }
70                member_props.push(props_vec);
71            } else {
72                // Non-object member - can't have discriminants
73                return vec![];
74            }
75        }
76
77        // Check each property to see if it's a valid discriminant
78        let mut discriminants = Vec::new();
79
80        for prop_name in &all_properties {
81            let mut is_discriminant = true;
82            let mut variants: Vec<(TypeId, TypeId)> = Vec::new();
83            let mut seen_literals: Vec<TypeId> = Vec::new();
84
85            for (i, props) in member_props.iter().enumerate() {
86                // Find this property in the member
87                let prop_type = props
88                    .iter()
89                    .find(|(name, _)| name == prop_name)
90                    .map(|(_, ty)| *ty);
91
92                match prop_type {
93                    Some(ty) => {
94                        // Must be a literal type
95                        if is_literal_type_db(self.db, ty) {
96                            // Must be unique among members
97                            if seen_literals.contains(&ty) {
98                                is_discriminant = false;
99                                break;
100                            }
101                            seen_literals.push(ty);
102                            variants.push((ty, members[i]));
103                        } else {
104                            is_discriminant = false;
105                            break;
106                        }
107                    }
108                    None => {
109                        // Property doesn't exist in this member
110                        is_discriminant = false;
111                        break;
112                    }
113                }
114            }
115
116            if is_discriminant && !variants.is_empty() {
117                discriminants.push(DiscriminantInfo {
118                    property_name: *prop_name,
119                    variants,
120                });
121            }
122        }
123
124        discriminants
125    }
126
127    /// Get the type of a property at a nested path within a type.
128    ///
129    /// # Examples
130    /// - `get_type_at_path(type, ["payload"])` -> type of `payload` property
131    /// - `get_type_at_path(type, ["payload", "type"])` -> type of `payload.type`
132    ///
133    /// Returns `None` if:
134    /// - The type doesn't have the property at any level in the path
135    /// - An intermediate type in the path is not an object type
136    ///
137    /// **NOTE**: Uses `resolve_property_access` which correctly handles optional properties.
138    /// For optional properties that don't exist on a specific union member, returns
139    /// `TypeId::UNDEFINED` to indicate the property could be undefined (not a definitive mismatch).
140    fn get_type_at_path(
141        &self,
142        mut type_id: TypeId,
143        path: &[Atom],
144        evaluator: &PropertyAccessEvaluator<'_>,
145    ) -> Option<TypeId> {
146        for (i, &prop_name) in path.iter().enumerate() {
147            // Handle ANY - any property access on any returns any
148            if type_id == TypeId::ANY {
149                return Some(TypeId::ANY);
150            }
151
152            // Resolve Lazy types
153            type_id = self.resolve_type(type_id);
154
155            // Handle Union - return union of property types from all members
156            if let Some(members_id) = union_list_id(self.db, type_id) {
157                let members = self.db.type_list(members_id);
158                let remaining_path = &path[i..];
159                let prop_types: Vec<TypeId> = members
160                    .iter()
161                    .filter_map(|&member| self.get_type_at_path(member, remaining_path, evaluator))
162                    .collect();
163
164                if prop_types.is_empty() {
165                    return None;
166                } else if prop_types.len() == 1 {
167                    return Some(prop_types[0]);
168                }
169                return Some(self.db.union(prop_types));
170            }
171
172            // Use resolve_property_access for proper optional property handling
173            // This correctly handles properties that are optional (prop?: type)
174            let prop_name_arc = self.db.resolve_atom_ref(prop_name);
175            let prop_name_str = prop_name_arc.as_ref();
176            match evaluator.resolve_property_access(type_id, prop_name_str) {
177                PropertyAccessResult::Success {
178                    type_id: prop_type_id,
179                    ..
180                } => {
181                    // Property found - use its type
182                    // For optional properties, this already includes `undefined` in the union
183                    type_id = prop_type_id;
184                }
185                PropertyAccessResult::PropertyNotFound { .. } => {
186                    // Property truly doesn't exist on this type
187                    // This union member doesn't have the discriminant property, so filter it out
188                    return None;
189                }
190                PropertyAccessResult::PossiblyNullOrUndefined { property_type, .. } => {
191                    // CRITICAL FIX: For optional properties (prop?: type), we need to preserve
192                    // both the property type AND undefined in the union.
193                    // This ensures that is_subtype_of(circle, "circle" | undefined) works correctly.
194                    if let Some(prop_ty) = property_type {
195                        // Create union: property_type | undefined
196                        type_id = self.db.union2(prop_ty, TypeId::UNDEFINED);
197                    } else {
198                        // No property type, just undefined
199                        type_id = TypeId::UNDEFINED;
200                    }
201                }
202                PropertyAccessResult::IsUnknown => {
203                    return Some(TypeId::ANY);
204                }
205            }
206        }
207
208        Some(type_id)
209    }
210
211    /// Fast path for top-level property lookup on object members.
212    ///
213    /// This avoids `PropertyAccessEvaluator` for the common discriminant pattern
214    /// `x.kind === "..."` where we only need a direct property read from object-like
215    /// union members. Falls back to the general path for complex structures.
216    fn get_top_level_property_type_fast(&self, type_id: TypeId, property: Atom) -> Option<TypeId> {
217        let key = (type_id, property);
218        if let Some(&cached) = self.cache.property_cache.borrow().get(&key) {
219            return cached;
220        }
221
222        // Cache the resolved property type so hot paths avoid an extra resolve pass.
223        let result = self
224            .get_top_level_property_type_fast_uncached(type_id, property)
225            .map(|prop_type| self.resolve_type(prop_type));
226        self.cache.property_cache.borrow_mut().insert(key, result);
227        result
228    }
229
230    fn get_top_level_property_type_fast_uncached(
231        &self,
232        mut type_id: TypeId,
233        property: Atom,
234    ) -> Option<TypeId> {
235        type_id = self.resolve_type(type_id);
236
237        // Keep this fast path conservative: intersections and complex wrappers
238        // should use the full evaluator-based path for correctness.
239        if intersection_list_id(self.db, type_id).is_some() {
240            return None;
241        }
242
243        let shape_id = object_shape_id(self.db, type_id)
244            .or_else(|| object_with_index_shape_id(self.db, type_id))?;
245        let shape = self.db.object_shape(shape_id);
246
247        let prop = match self.db.object_property_index(shape_id, property) {
248            PropertyLookup::Found(idx) => shape.properties.get(idx),
249            PropertyLookup::NotFound => None,
250            PropertyLookup::Uncached => {
251                // Properties are sorted by Atom id.
252                shape
253                    .properties
254                    .binary_search_by_key(&property, |p| p.name)
255                    .ok()
256                    .and_then(|idx| shape.properties.get(idx))
257            }
258        }?;
259
260        Some(if prop.optional {
261            self.db.union2(prop.type_id, TypeId::UNDEFINED)
262        } else {
263            prop.type_id
264        })
265    }
266
267    /// Fast literal-only subtype check used by discriminant hot paths.
268    ///
269    /// Returns `None` when either side is non-literal (or not a string/number
270    /// literal) so callers can fall back to the full subtype relation.
271    #[inline]
272    fn literal_subtype_fast(&self, source: TypeId, target: TypeId) -> Option<bool> {
273        if source == target {
274            return Some(true);
275        }
276
277        match (
278            classify_for_literal_value(self.db, source),
279            classify_for_literal_value(self.db, target),
280        ) {
281            (LiteralValueKind::String(a), LiteralValueKind::String(b)) => Some(a == b),
282            (LiteralValueKind::Number(a), LiteralValueKind::Number(b)) => Some(a == b),
283            (LiteralValueKind::String(_), LiteralValueKind::Number(_))
284            | (LiteralValueKind::Number(_), LiteralValueKind::String(_)) => Some(false),
285            _ => None,
286        }
287    }
288
289    /// Fast narrowing for `x.<prop> === literal` / `!== literal` over union members.
290    ///
291    /// Returns `None` to request fallback to the general evaluator-based implementation
292    /// when the structure is too complex for this direct path.
293    fn fast_narrow_top_level_discriminant(
294        &self,
295        original_union_type: TypeId,
296        members: &[TypeId],
297        property: Atom,
298        literal_value: TypeId,
299        keep_matching: bool,
300    ) -> Option<TypeId> {
301        let mut kept = Vec::with_capacity(members.len());
302
303        for &member in members {
304            if member.is_any_or_unknown() {
305                kept.push(member);
306                continue;
307            }
308
309            let prop_type = self.get_top_level_property_type_fast(member, property)?;
310            let should_keep = if prop_type == literal_value {
311                keep_matching
312            } else if keep_matching {
313                // true branch: keep members where literal <: property_type
314                self.literal_subtype_fast(literal_value, prop_type)
315                    .unwrap_or_else(|| is_subtype_of(self.db, literal_value, prop_type))
316            } else {
317                // false branch: exclude members where property_type <: excluded_literal
318                !self
319                    .literal_subtype_fast(prop_type, literal_value)
320                    .unwrap_or_else(|| is_subtype_of(self.db, prop_type, literal_value))
321            };
322
323            if should_keep {
324                kept.push(member);
325            }
326        }
327
328        if keep_matching && kept.is_empty() {
329            return Some(TypeId::NEVER);
330        }
331        if keep_matching && kept.len() == members.len() {
332            return Some(original_union_type);
333        }
334
335        Some(union_or_single_preserve(self.db, kept))
336    }
337
338    /// Narrow a union type based on a discriminant property check.
339    ///
340    /// Example: `action.type === "add"` narrows `Action` to `{ type: "add", value: number }`
341    ///
342    /// Uses a filtering approach: checks each union member individually to see if
343    /// the property could match the literal value. This is more flexible than the
344    /// old `find_discriminants` approach which required ALL members to have the
345    /// property with unique literal values.
346    ///
347    /// # Arguments
348    /// Narrow a type by discriminant, handling type parameter constraints.
349    ///
350    /// If the type is a type parameter with a constraint, narrows the constraint
351    /// and intersects with the type parameter when the constraint is affected.
352    pub fn narrow_by_discriminant_for_type(
353        &self,
354        type_id: TypeId,
355        prop_path: &[Atom],
356        literal_type: TypeId,
357        is_true_branch: bool,
358    ) -> TypeId {
359        use crate::type_queries::{
360            TypeParameterConstraintKind, classify_for_type_parameter_constraint,
361        };
362
363        if let TypeParameterConstraintKind::TypeParameter {
364            constraint: Some(constraint),
365        } = classify_for_type_parameter_constraint(self.db, type_id)
366            && constraint != type_id
367        {
368            let narrowed_constraint = if is_true_branch {
369                self.narrow_by_discriminant(constraint, prop_path, literal_type)
370            } else {
371                self.narrow_by_excluding_discriminant(constraint, prop_path, literal_type)
372            };
373            if narrowed_constraint != constraint {
374                return self.db.intersection(vec![type_id, narrowed_constraint]);
375            }
376        }
377
378        if is_true_branch {
379            self.narrow_by_discriminant(type_id, prop_path, literal_type)
380        } else {
381            self.narrow_by_excluding_discriminant(type_id, prop_path, literal_type)
382        }
383    }
384
385    /// Narrows a union type based on whether a property is truthy or falsy.
386    ///
387    /// This is used for conditionals like `if (x.prop)` when `x` is a union.
388    pub fn narrow_by_property_truthiness(
389        &self,
390        union_type: TypeId,
391        property_path: &[Atom],
392        sense: bool,
393    ) -> TypeId {
394        let _span = span!(
395            Level::TRACE,
396            "narrow_by_property_truthiness",
397            union_type = union_type.0,
398            property_path_len = property_path.len(),
399            sense
400        )
401        .entered();
402
403        let resolved_type = self.resolve_type(union_type);
404
405        let single_member_storage: Vec<TypeId>;
406        let members: &[TypeId] = match classify_for_union_members(self.db, resolved_type) {
407            UnionMembersKind::Union(members_list) => {
408                single_member_storage = members_list.into_iter().collect::<Vec<_>>();
409                &single_member_storage
410            }
411            UnionMembersKind::NotUnion => {
412                single_member_storage = vec![resolved_type];
413                &single_member_storage
414            }
415        };
416
417        let mut matching: Vec<TypeId> = Vec::new();
418        let property_evaluator = match self.resolver {
419            Some(resolver) => PropertyAccessEvaluator::with_resolver(self.db, resolver),
420            None => PropertyAccessEvaluator::new(self.db),
421        };
422
423        for &member in members {
424            if member.is_any_or_unknown() {
425                matching.push(member);
426                continue;
427            }
428
429            let resolved_member = self.resolve_type(member);
430
431            let intersection_members = intersection_list_id(self.db, resolved_member)
432                .map(|members_id| self.db.type_list(members_id).to_vec());
433
434            let check_member_for_property = |check_type_id: TypeId| -> bool {
435                let prop_type = match self.get_type_at_path(
436                    check_type_id,
437                    property_path,
438                    &property_evaluator,
439                ) {
440                    Some(t) => t,
441                    None => {
442                        // Property doesn't exist -> undefined (falsy)
443                        return !sense;
444                    }
445                };
446
447                let resolved_prop_type = self.resolve_type(prop_type);
448
449                // If it's the true branch, check if the property can be truthy
450                // If it's the false branch, check if the property can be falsy
451                if sense {
452                    let narrowed = self.narrow_by_truthiness(resolved_prop_type);
453                    narrowed != TypeId::NEVER
454                } else {
455                    let narrowed = self.narrow_to_falsy(resolved_prop_type);
456                    narrowed != TypeId::NEVER
457                }
458            };
459
460            let matches = if let Some(ref intersection) = intersection_members {
461                intersection.iter().any(|&m| check_member_for_property(m))
462            } else {
463                check_member_for_property(resolved_member)
464            };
465
466            if matches {
467                matching.push(member);
468            }
469        }
470
471        if matching.is_empty() {
472            return TypeId::NEVER;
473        }
474
475        if matching.len() == members.len() {
476            return union_type;
477        }
478
479        union_or_single_preserve(self.db, matching)
480    }
481
482    /// - `union_type`: The union type to narrow
483    /// - `property_path`: Path to the discriminant property (e.g., ["payload", "type"])
484    /// - `literal_value`: The literal value to match
485    pub fn narrow_by_discriminant(
486        &self,
487        union_type: TypeId,
488        property_path: &[Atom],
489        literal_value: TypeId,
490    ) -> TypeId {
491        let _span = span!(
492            Level::TRACE,
493            "narrow_by_discriminant",
494            union_type = union_type.0,
495            property_path_len = property_path.len(),
496            literal_value = literal_value.0
497        )
498        .entered();
499
500        // CRITICAL: Resolve Lazy types before checking for union members
501        // This ensures type aliases are resolved to their actual union types
502        let resolved_type = self.resolve_type(union_type);
503
504        trace!(
505            "narrow_by_discriminant: union_type={}, resolved_type={}, property_path={:?}, literal_value={}",
506            union_type.0, resolved_type.0, property_path, literal_value.0
507        );
508
509        // CRITICAL FIX: Use classify_for_union_members instead of union_list_id
510        // This correctly handles intersections containing unions, nested unions, etc.
511        let single_member_storage: Vec<TypeId>;
512        let members: &[TypeId] = match classify_for_union_members(self.db, resolved_type) {
513            UnionMembersKind::Union(members_list) => {
514                // Convert Vec to slice for iteration
515                single_member_storage = members_list.into_iter().collect::<Vec<_>>();
516                &single_member_storage
517            }
518            UnionMembersKind::NotUnion => {
519                // Not a union at all - treat as single member
520                single_member_storage = vec![resolved_type];
521                &single_member_storage
522            }
523        };
524
525        trace!("narrow_by_discriminant: members={:?}", members);
526
527        trace!(
528            "Checking {} member(s) for discriminant match",
529            members.len()
530        );
531
532        trace!(
533            "Narrowing union with {} members by discriminant property",
534            members.len()
535        );
536
537        if property_path.len() == 1
538            && let Some(fast_result) = self.fast_narrow_top_level_discriminant(
539                union_type,
540                members,
541                property_path[0],
542                literal_value,
543                true,
544            )
545        {
546            return fast_result;
547        }
548
549        let mut matching: Vec<TypeId> = Vec::new();
550        let property_evaluator = match self.resolver {
551            Some(resolver) => PropertyAccessEvaluator::with_resolver(self.db, resolver),
552            None => PropertyAccessEvaluator::new(self.db),
553        };
554
555        for &member in members {
556            // Special case: any and unknown always match
557            if member.is_any_or_unknown() {
558                trace!("Member {} is any/unknown, keeping in true branch", member.0);
559                matching.push(member);
560                continue;
561            }
562
563            // CRITICAL: Resolve Lazy types before checking for object shape
564            // This ensures type aliases are resolved to their actual types
565            let resolved_member = self.resolve_type(member);
566
567            // Handle Intersection types: check all intersection members for the property
568            let intersection_members = intersection_list_id(self.db, resolved_member)
569                .map(|members_id| self.db.type_list(members_id).to_vec());
570
571            // Helper function to check if a type has a matching property at the path
572            let check_member_for_property = |check_type_id: TypeId| -> bool {
573                // Get the type at the property path
574                let prop_type = match self.get_type_at_path(
575                    check_type_id,
576                    property_path,
577                    &property_evaluator,
578                ) {
579                    Some(t) => t,
580                    None => {
581                        // Property doesn't exist on this member
582                        trace!(
583                            "Member {} does not have property path {:?}",
584                            check_type_id.0, property_path
585                        );
586                        return false;
587                    }
588                };
589
590                // CRITICAL: Resolve Lazy types in property type before comparison.
591                // Property types like `E.A` may be stored as Lazy(DefId) references
592                // that need to be resolved to their actual enum literal types.
593                let resolved_prop_type = self.resolve_type(prop_type);
594
595                // CRITICAL: Use is_subtype_of(literal_value, property_type)
596                // NOT the reverse! This was the bug in the reverted commit.
597                let matches = is_subtype_of(self.db, literal_value, resolved_prop_type);
598
599                if matches {
600                    trace!(
601                        "Member {} has property path {:?} with type {}, literal {} matches",
602                        check_type_id.0, property_path, prop_type.0, literal_value.0
603                    );
604                } else {
605                    trace!(
606                        "Member {} has property path {:?} with type {}, literal {} does not match",
607                        check_type_id.0, property_path, prop_type.0, literal_value.0
608                    );
609                }
610
611                matches
612            };
613
614            // Check for property match
615            let has_property_match = if let Some(ref intersection) = intersection_members {
616                // For Intersection: at least one member must have the property
617                intersection.iter().any(|&m| check_member_for_property(m))
618            } else {
619                // For non-Intersection: check the single member
620                check_member_for_property(resolved_member)
621            };
622
623            if has_property_match {
624                matching.push(member);
625            }
626        }
627
628        // Return result based on matches
629
630        if matching.is_empty() {
631            trace!("No members matched discriminant check, returning never");
632            TypeId::NEVER
633        } else if matching.len() == members.len() {
634            trace!("All members matched, returning original");
635            union_type
636        } else if matching.len() == 1 {
637            trace!("Narrowed to single member");
638            matching[0]
639        } else {
640            trace!(
641                "Narrowed to {} of {} members",
642                matching.len(),
643                members.len()
644            );
645            self.db.union(matching)
646        }
647    }
648
649    /// Narrow a union type by excluding variants with a specific discriminant value.
650    ///
651    /// Example: `action.type !== "add"` narrows to `{ type: "remove", ... } | { type: "clear" }`
652    ///
653    /// Uses the inverse logic of `narrow_by_discriminant`: we exclude a member
654    /// ONLY if its property is definitely and only the excluded value.
655    ///
656    /// For example:
657    /// - prop is "a", exclude "a" -> exclude (property is always "a")
658    /// - prop is "a" | "b", exclude "a" -> keep (could be "b")
659    /// - prop doesn't exist -> keep (property doesn't match excluded value)
660    ///
661    /// # Arguments
662    /// - `union_type`: The union type to narrow
663    /// - `property_path`: Path to the discriminant property (e.g., ["payload", "type"])
664    /// - `excluded_value`: The literal value to exclude
665    pub fn narrow_by_excluding_discriminant(
666        &self,
667        union_type: TypeId,
668        property_path: &[Atom],
669        excluded_value: TypeId,
670    ) -> TypeId {
671        let _span = span!(
672            Level::TRACE,
673            "narrow_by_excluding_discriminant",
674            union_type = union_type.0,
675            property_path_len = property_path.len(),
676            excluded_value = excluded_value.0
677        )
678        .entered();
679
680        // CRITICAL: Resolve Lazy types before checking for union members
681        // This ensures type aliases are resolved to their actual union types
682        let resolved_type = self.resolve_type(union_type);
683
684        // CRITICAL FIX: Use classify_for_union_members instead of union_list_id
685        // This correctly handles intersections containing unions, nested unions, etc.
686        // Consistent with narrow_by_discriminant.
687        let single_member_storage: Vec<TypeId>;
688        let members: &[TypeId] = match classify_for_union_members(self.db, resolved_type) {
689            UnionMembersKind::Union(members_list) => {
690                single_member_storage = members_list.into_iter().collect::<Vec<_>>();
691                &single_member_storage
692            }
693            UnionMembersKind::NotUnion => {
694                single_member_storage = vec![resolved_type];
695                &single_member_storage
696            }
697        };
698
699        trace!(
700            "Excluding discriminant value {} from union with {} members",
701            excluded_value.0,
702            members.len()
703        );
704
705        if property_path.len() == 1
706            && let Some(fast_result) = self.fast_narrow_top_level_discriminant(
707                union_type,
708                members,
709                property_path[0],
710                excluded_value,
711                false,
712            )
713        {
714            return fast_result;
715        }
716
717        let mut remaining: Vec<TypeId> = Vec::new();
718        let property_evaluator = match self.resolver {
719            Some(resolver) => PropertyAccessEvaluator::with_resolver(self.db, resolver),
720            None => PropertyAccessEvaluator::new(self.db),
721        };
722
723        for &member in members {
724            // Special case: any and unknown always kept (could have any property value)
725            if member.is_any_or_unknown() {
726                trace!(
727                    "Member {} is any/unknown, keeping in false branch",
728                    member.0
729                );
730                remaining.push(member);
731                continue;
732            }
733
734            // CRITICAL: Resolve Lazy types before checking for object shape
735            let resolved_member = self.resolve_type(member);
736
737            // Handle Intersection types: check all intersection members for the property
738            let intersection_members = intersection_list_id(self.db, resolved_member)
739                .map(|members_id| self.db.type_list(members_id).to_vec());
740
741            // Helper function to check if a member should be excluded
742            // Returns true if member should be KEPT (not excluded)
743            let should_keep_member = |check_type_id: TypeId| -> bool {
744                // Get the type at the property path
745                let prop_type = match self.get_type_at_path(
746                    check_type_id,
747                    property_path,
748                    &property_evaluator,
749                ) {
750                    Some(t) => t,
751                    None => {
752                        // Property doesn't exist - keep the member
753                        trace!(
754                            "Member {} does not have property path, keeping",
755                            check_type_id.0
756                        );
757                        return true;
758                    }
759                };
760
761                // CRITICAL: Resolve Lazy types in property type before comparison.
762                let resolved_prop_type = self.resolve_type(prop_type);
763
764                // Exclude member ONLY if property type is subtype of excluded value
765                // This means the property is ALWAYS the excluded value
766                // REVERSE of narrow_by_discriminant logic
767                let should_exclude = is_subtype_of(self.db, resolved_prop_type, excluded_value);
768
769                if should_exclude {
770                    trace!(
771                        "Member {} has property path type {} which is subtype of excluded {}, excluding",
772                        check_type_id.0, prop_type.0, excluded_value.0
773                    );
774                    false // Member should be excluded
775                } else {
776                    trace!(
777                        "Member {} has property path type {} which is not subtype of excluded {}, keeping",
778                        check_type_id.0, prop_type.0, excluded_value.0
779                    );
780                    true // Member should be kept
781                }
782            };
783
784            // Check if member should be kept
785            let keep_member = if let Some(ref intersection) = intersection_members {
786                // CRITICAL: For Intersection exclusion, use ALL not ANY
787                // If ANY intersection member has the excluded property value,
788                // the ENTIRE intersection must be excluded.
789                // Example: { kind: "A" } & { data: string } with x.kind !== "A"
790                //   -> { kind: "A" } has "A" (excluded) -> exclude entire intersection
791                intersection.iter().all(|&m| should_keep_member(m))
792            } else {
793                // For non-Intersection: check the single member
794                should_keep_member(resolved_member)
795            };
796
797            if keep_member {
798                remaining.push(member);
799            }
800        }
801
802        union_or_single_preserve(self.db, remaining)
803    }
804
805    /// Narrow a union type by excluding variants with any of the specified discriminant values.
806    ///
807    /// This is an optimized batch version of `narrow_by_excluding_discriminant` for switch statements.
808    pub fn narrow_by_excluding_discriminant_values(
809        &self,
810        union_type: TypeId,
811        property_path: &[Atom],
812        excluded_values: &[TypeId],
813    ) -> TypeId {
814        if excluded_values.is_empty() {
815            return union_type;
816        }
817
818        let _span = span!(
819            Level::TRACE,
820            "narrow_by_excluding_discriminant_values",
821            union_type = union_type.0,
822            property_path_len = property_path.len(),
823            excluded_count = excluded_values.len()
824        )
825        .entered();
826
827        let resolved_type = self.resolve_type(union_type);
828
829        let single_member_storage: Vec<TypeId>;
830        let members: &[TypeId] = match classify_for_union_members(self.db, resolved_type) {
831            UnionMembersKind::Union(members_list) => {
832                single_member_storage = members_list.into_iter().collect::<Vec<_>>();
833                &single_member_storage
834            }
835            UnionMembersKind::NotUnion => {
836                single_member_storage = vec![resolved_type];
837                &single_member_storage
838            }
839        };
840
841        // Put excluded values into a HashSet for O(1) lookup
842        let excluded_set: FxHashSet<TypeId> = excluded_values.iter().copied().collect();
843
844        let mut remaining: Vec<TypeId> = Vec::new();
845        let property_evaluator = match self.resolver {
846            Some(resolver) => PropertyAccessEvaluator::with_resolver(self.db, resolver),
847            None => PropertyAccessEvaluator::new(self.db),
848        };
849
850        for &member in members {
851            if member.is_any_or_unknown() {
852                remaining.push(member);
853                continue;
854            }
855
856            let resolved_member = self.resolve_type(member);
857            let intersection_members = intersection_list_id(self.db, resolved_member)
858                .map(|members_id| self.db.type_list(members_id).to_vec());
859
860            // Helper to check if member should be kept
861            let should_keep_member = |check_type_id: TypeId| -> bool {
862                let prop_type = match self.get_type_at_path(
863                    check_type_id,
864                    property_path,
865                    &property_evaluator,
866                ) {
867                    Some(t) => t,
868                    None => return true, // Keep if property missing
869                };
870
871                let resolved_prop_type = self.resolve_type(prop_type);
872
873                // Optimization: if property type is directly in excluded set (literal match)
874                if excluded_set.contains(&resolved_prop_type) {
875                    return false; // Exclude
876                }
877
878                // Subtype check for each excluded value
879                for &excluded in excluded_values {
880                    if is_subtype_of(self.db, resolved_prop_type, excluded) {
881                        return false; // Exclude
882                    }
883                }
884                true // Keep
885            };
886
887            let keep_member = if let Some(ref intersection) = intersection_members {
888                intersection.iter().all(|&m| should_keep_member(m))
889            } else {
890                should_keep_member(resolved_member)
891            };
892
893            if keep_member {
894                remaining.push(member);
895            }
896        }
897
898        union_or_single_preserve(self.db, remaining)
899    }
900}