Skip to main content

tsz_solver/evaluation/evaluate_rules/
conditional.rs

1//! Conditional type evaluation.
2//!
3//! Handles TypeScript's conditional types: `T extends U ? X : Y`
4//! Including distributive conditional types over union types.
5
6use crate::instantiation::instantiate::{TypeSubstitution, instantiate_type_with_infer};
7use crate::relations::subtype::{SubtypeChecker, TypeResolver};
8use crate::types::{
9    ConditionalType, ObjectShapeId, PropertyInfo, TupleElement, TypeData, TypeId, TypeParamInfo,
10};
11use rustc_hash::{FxHashMap, FxHashSet};
12use tracing::trace;
13
14use super::super::evaluate::TypeEvaluator;
15
16impl<'a, R: TypeResolver> TypeEvaluator<'a, R> {
17    /// Maximum depth for tail-recursive conditional evaluation.
18    /// This allows patterns like `type Loop<T> = T extends [...infer R] ? Loop<R> : never`
19    /// to work with up to 1000 recursive calls instead of being limited to `MAX_EVALUATE_DEPTH`.
20    const MAX_TAIL_RECURSION_DEPTH: usize = 1000;
21
22    /// Evaluate a conditional type: T extends U ? X : Y
23    ///
24    /// Algorithm:
25    /// 1. If `check_type` is a union and the conditional is distributive, distribute
26    /// 2. Otherwise, check if `check_type` <: `extends_type`
27    /// 3. If true -> return `true_type`
28    /// 4. If false (disjoint) -> return `false_type`
29    /// 5. If ambiguous (unresolved type param) -> return deferred conditional
30    ///
31    /// ## Tail-Recursion Elimination
32    /// If the chosen branch (true/false) evaluates to another `ConditionalType`,
33    /// we immediately evaluate it in the current stack frame instead of recursing.
34    /// This allows tail-recursive patterns to work with up to `MAX_TAIL_RECURSION_DEPTH`
35    /// iterations instead of being limited by `MAX_EVALUATE_DEPTH`.
36    pub fn evaluate_conditional(&mut self, initial_cond: &ConditionalType) -> TypeId {
37        // Setup loop state for tail-recursion elimination
38        let mut current_cond = initial_cond.clone();
39        let mut tail_recursion_count = 0;
40
41        loop {
42            let cond = &current_cond;
43
44            // Pre-evaluation Application-level infer matching.
45            // When both check and extends are Applications (e.g., Promise<string> vs
46            // Promise<infer U>), match type arguments directly before expanding.
47            // After evaluation, Application types become structural Object/Callable types,
48            // which may fail structural infer matching for complex interfaces like Promise.
49            if let Some(result) = self.try_application_infer_match(cond) {
50                return result;
51            }
52
53            let check_type = self.evaluate(cond.check_type);
54            let extends_type = self.evaluate(cond.extends_type);
55
56            trace!(
57                check_raw = cond.check_type.0,
58                check_eval = check_type.0,
59                check_key = ?self.interner().lookup(check_type),
60                extends_raw = cond.extends_type.0,
61                extends_eval = extends_type.0,
62                extends_key = ?self.interner().lookup(extends_type),
63                "evaluate_conditional"
64            );
65
66            if cond.is_distributive && check_type == TypeId::NEVER {
67                return TypeId::NEVER;
68            }
69
70            if check_type == TypeId::ANY {
71                // For `any extends X ? T : F`, return union of both branches.
72                // When X contains infer patterns, perform infer pattern matching
73                // so the infer variables get bound to `any` and properly substituted.
74                // e.g., `any extends infer U ? U : never` → union(any, never) → any
75                if self.type_contains_infer(extends_type) {
76                    let mut bindings = FxHashMap::default();
77                    let mut visited = FxHashSet::default();
78                    let mut checker =
79                        SubtypeChecker::with_resolver(self.interner(), self.resolver());
80                    checker.allow_bivariant_rest = true;
81                    self.match_infer_pattern(
82                        check_type,
83                        extends_type,
84                        &mut bindings,
85                        &mut visited,
86                        &mut checker,
87                    );
88                    let true_sub = self.substitute_infer(cond.true_type, &bindings);
89                    let false_sub = self.substitute_infer(cond.false_type, &bindings);
90                    let true_eval = self.evaluate(true_sub);
91                    let false_eval = self.evaluate(false_sub);
92                    return self.interner().union2(true_eval, false_eval);
93                }
94                let true_eval = self.evaluate(cond.true_type);
95                let false_eval = self.evaluate(cond.false_type);
96                return self.interner().union2(true_eval, false_eval);
97            }
98
99            // Step 1: Check for distributivity
100            // Only distribute for naked type parameters (recorded at lowering time).
101            if cond.is_distributive
102                && let Some(TypeData::Union(members)) = self.interner().lookup(check_type)
103            {
104                let members = self.interner().type_list(members);
105                return self.distribute_conditional(
106                    members.as_ref(),
107                    check_type, // Pass original check_type for substitution
108                    extends_type,
109                    cond.true_type,
110                    cond.false_type,
111                );
112            }
113
114            if let Some(TypeData::Infer(info)) = self.interner().lookup(extends_type) {
115                if matches!(
116                    self.interner().lookup(check_type),
117                    Some(TypeData::TypeParameter(_) | TypeData::Infer(_))
118                ) {
119                    return self.interner().conditional(cond.clone());
120                }
121
122                if check_type == TypeId::ANY {
123                    let mut subst = TypeSubstitution::new();
124                    subst.insert(info.name, check_type);
125                    let true_eval = self.evaluate(instantiate_type_with_infer(
126                        self.interner(),
127                        cond.true_type,
128                        &subst,
129                    ));
130                    let false_eval = self.evaluate(instantiate_type_with_infer(
131                        self.interner(),
132                        cond.false_type,
133                        &subst,
134                    ));
135                    return self.interner().union2(true_eval, false_eval);
136                }
137
138                let mut subst = TypeSubstitution::new();
139                subst.insert(info.name, check_type);
140                let mut inferred = check_type;
141                if let Some(constraint) = info.constraint {
142                    let mut checker =
143                        SubtypeChecker::with_resolver(self.interner(), self.resolver());
144                    checker.allow_bivariant_rest = true;
145                    let Some(filtered) =
146                        self.filter_inferred_by_constraint(inferred, constraint, &mut checker)
147                    else {
148                        let false_inst =
149                            instantiate_type_with_infer(self.interner(), cond.false_type, &subst);
150                        return self.evaluate(false_inst);
151                    };
152                    inferred = filtered;
153                }
154
155                subst.insert(info.name, inferred);
156
157                let true_inst =
158                    instantiate_type_with_infer(self.interner(), cond.true_type, &subst);
159                return self.evaluate(true_inst);
160            }
161
162            let extends_unwrapped = match self.interner().lookup(extends_type) {
163                Some(TypeData::ReadonlyType(inner)) => inner,
164                _ => extends_type,
165            };
166            let check_unwrapped = match self.interner().lookup(check_type) {
167                Some(TypeData::ReadonlyType(inner)) => inner,
168                _ => check_type,
169            };
170
171            // Handle array extends pattern with infer
172            if let Some(TypeData::Array(ext_elem)) = self.interner().lookup(extends_unwrapped)
173                && let Some(TypeData::Infer(info)) = self.interner().lookup(ext_elem)
174            {
175                return self.eval_conditional_array_infer(cond, check_unwrapped, info);
176            }
177
178            // Handle tuple extends pattern with infer
179            if let Some(TypeData::Tuple(extends_elements)) =
180                self.interner().lookup(extends_unwrapped)
181            {
182                let extends_elements = self.interner().tuple_list(extends_elements);
183                if extends_elements.len() == 1
184                    && !extends_elements[0].rest
185                    && let Some(TypeData::Infer(info)) =
186                        self.interner().lookup(extends_elements[0].type_id)
187                {
188                    return self.eval_conditional_tuple_infer(
189                        cond,
190                        check_unwrapped,
191                        &extends_elements[0],
192                        info,
193                    );
194                }
195            }
196
197            // Handle object extends pattern with infer
198            if let Some(extends_shape_id) = match self.interner().lookup(extends_unwrapped) {
199                Some(TypeData::Object(shape_id) | TypeData::ObjectWithIndex(shape_id)) => {
200                    Some(shape_id)
201                }
202                _ => None,
203            } && let Some(result) =
204                self.eval_conditional_object_infer(cond, check_unwrapped, extends_shape_id)
205            {
206                return result;
207            }
208
209            // Step 2: Check for naked type parameter
210            if let Some(TypeData::TypeParameter(param)) = self.interner().lookup(check_type) {
211                // Simplification: T extends never ? X : Y → Y
212                // A type parameter T cannot extend `never` (only `never` extends `never`),
213                // so the conditional always takes the false branch.
214                if extends_type == TypeId::NEVER {
215                    return self.evaluate(cond.false_type);
216                }
217
218                // Simplification: T extends T ? X : Y → X
219                // A type parameter always extends itself, so the conditional always takes
220                // the true branch.
221                if check_type == extends_type {
222                    return self.evaluate(cond.true_type);
223                }
224
225                // If extends_type contains infer patterns and the type parameter has a constraint,
226                // try to infer from the constraint. This handles cases like:
227                // R extends Reducer<infer S, any> ? S : never
228                // where R is constrained to Reducer<any, any>
229                if self.type_contains_infer(extends_type)
230                    && let Some(constraint) = param.constraint
231                {
232                    let mut checker =
233                        SubtypeChecker::with_resolver(self.interner(), self.resolver());
234                    checker.allow_bivariant_rest = true;
235                    let mut bindings = FxHashMap::default();
236                    let mut visited = FxHashSet::default();
237                    if self.match_infer_pattern(
238                        constraint,
239                        extends_type,
240                        &mut bindings,
241                        &mut visited,
242                        &mut checker,
243                    ) {
244                        let substituted_true = self.substitute_infer(cond.true_type, &bindings);
245                        return self.evaluate(substituted_true);
246                    }
247                }
248                // When the type parameter has a constraint, use it to determine
249                // which branch to take. This matches tsc's "restrictive instantiation"
250                // approach: if the constraint definitely doesn't extend the extends type,
251                // evaluate to the false branch. This is critical for types like
252                // `Awaited<T>` where T extends Record<string, unknown> — since Record
253                // doesn't have a `then` method, Awaited<T> evaluates to T.
254                if let Some(constraint) = param.constraint {
255                    let evaluated_constraint = self.evaluate(constraint);
256                    if !self.type_contains_infer(extends_type) {
257                        let mut checker =
258                            SubtypeChecker::with_resolver(self.interner(), self.resolver());
259                        if !checker.is_subtype_of(evaluated_constraint, extends_type) {
260                            // Constraint doesn't satisfy the condition → false branch
261                            // For tail-recursion, check if false branch is another conditional
262                            if tail_recursion_count < Self::MAX_TAIL_RECURSION_DEPTH
263                                && let Some(TypeData::Conditional(next_cond_id)) =
264                                    self.interner().lookup(cond.false_type)
265                            {
266                                let next_cond = self.interner().conditional_type(next_cond_id);
267                                current_cond = (*next_cond).clone();
268                                tail_recursion_count += 1;
269                                continue;
270                            }
271                            return self.evaluate(cond.false_type);
272                        }
273                        // Constraint satisfies the condition → true branch.
274                        // When T's constraint is assignable to extends_type, every
275                        // possible value of T satisfies the condition, so the conditional
276                        // always takes the true branch. For example:
277                        // type Foo<T extends string> = T extends string ? string : number
278                        // Foo<T> resolves to string because string <: string.
279                        //
280                        // For distributive conditionals, only resolve if the constraint
281                        // is not a union — a union constraint means T could be different
282                        // union members and we need to distribute individually.
283                        let constraint_is_union = matches!(
284                            self.interner().lookup(evaluated_constraint),
285                            Some(TypeData::Union(_))
286                        );
287                        if !constraint_is_union || !cond.is_distributive {
288                            // For tail-recursion on the true branch
289                            if tail_recursion_count < Self::MAX_TAIL_RECURSION_DEPTH
290                                && let Some(TypeData::Conditional(next_cond_id)) =
291                                    self.interner().lookup(cond.true_type)
292                            {
293                                let next_cond = self.interner().conditional_type(next_cond_id);
294                                current_cond = (*next_cond).clone();
295                                tail_recursion_count += 1;
296                                continue;
297                            }
298                            return self.evaluate(cond.true_type);
299                        }
300                    }
301                }
302
303                // Type parameter hasn't been substituted - defer evaluation
304                return self.interner().conditional(cond.clone());
305            }
306
307            // Step 3: Perform subtype check or infer pattern matching
308            let mut checker = SubtypeChecker::with_resolver(self.interner(), self.resolver());
309            checker.allow_bivariant_rest = true;
310
311            if self.type_contains_infer(extends_type) {
312                let mut bindings = FxHashMap::default();
313                let mut visited = FxHashSet::default();
314                if self.match_infer_pattern(
315                    check_type,
316                    extends_type,
317                    &mut bindings,
318                    &mut visited,
319                    &mut checker,
320                ) {
321                    let substituted_true = self.substitute_infer(cond.true_type, &bindings);
322                    return self.evaluate(substituted_true);
323                }
324
325                // Check if the result branch is directly a conditional for tail-recursion
326                // IMPORTANT: Check BEFORE calling evaluate to avoid incrementing depth
327                if tail_recursion_count < Self::MAX_TAIL_RECURSION_DEPTH {
328                    if let Some(TypeData::Conditional(next_cond_id)) =
329                        self.interner().lookup(cond.false_type)
330                    {
331                        let next_cond = self.interner().conditional_type(next_cond_id);
332                        current_cond = (*next_cond).clone();
333                        tail_recursion_count += 1;
334                        continue;
335                    }
336                    // Also detect Application that expands to Conditional (common pattern):
337                    // type TrimLeft<T> = T extends ` ${infer R}` ? TrimLeft<R> : T;
338                    // The true branch `TrimLeft<R>` is an Application, not a Conditional.
339                    if let Some(TypeData::Application(_)) = self.interner().lookup(cond.false_type)
340                    {
341                        let expanded = self.evaluate(cond.false_type);
342                        if let Some(TypeData::Conditional(next_cond_id)) =
343                            self.interner().lookup(expanded)
344                        {
345                            let next_cond = self.interner().conditional_type(next_cond_id);
346                            current_cond = (*next_cond).clone();
347                            tail_recursion_count += 1;
348                            continue;
349                        }
350                        return expanded;
351                    }
352                }
353
354                // Not a tail-recursive case - evaluate normally
355                return self.evaluate(cond.false_type);
356            }
357
358            // Subtype check path — use strict checking (no bivariant rest)
359            // to match tsc's `isTypeAssignableTo` which respects strictFunctionTypes.
360            let mut strict_checker =
361                SubtypeChecker::with_resolver(self.interner(), self.resolver());
362            let is_sub = strict_checker.is_subtype_of(check_type, extends_type);
363            trace!(
364                check = check_type.0,
365                extends = extends_type.0,
366                is_subtype = is_sub,
367                "conditional subtype check result"
368            );
369            let result_branch = if is_sub {
370                // T <: U -> true branch
371                cond.true_type
372            } else {
373                // Check if types are definitely disjoint
374                // For now, we use a simple heuristic: if not subtype, assume disjoint
375                // More sophisticated: check if intersection is never
376                cond.false_type
377            };
378
379            // Check if the result branch is directly a conditional for tail-recursion
380            // IMPORTANT: Check BEFORE calling evaluate to avoid incrementing depth
381            if tail_recursion_count < Self::MAX_TAIL_RECURSION_DEPTH {
382                if let Some(TypeData::Conditional(next_cond_id)) =
383                    self.interner().lookup(result_branch)
384                {
385                    let next_cond = self.interner().conditional_type(next_cond_id);
386                    current_cond = (*next_cond).clone();
387                    tail_recursion_count += 1;
388                    continue;
389                }
390                // Also detect Application that expands to Conditional (tail-call through
391                // type alias like `TrimLeft<R>` which is Application, not Conditional)
392                if let Some(TypeData::Application(_)) = self.interner().lookup(result_branch) {
393                    let expanded = self.evaluate(result_branch);
394                    if let Some(TypeData::Conditional(next_cond_id)) =
395                        self.interner().lookup(expanded)
396                    {
397                        let next_cond = self.interner().conditional_type(next_cond_id);
398                        current_cond = (*next_cond).clone();
399                        tail_recursion_count += 1;
400                        continue;
401                    }
402                    return expanded;
403                }
404            }
405
406            // Not a tail-recursive case - evaluate normally
407            return self.evaluate(result_branch);
408        }
409    }
410
411    /// Distribute a conditional type over a union.
412    /// (A | B) extends U ? X : Y -> (A extends U ? X : Y) | (B extends U ? X : Y)
413    pub(crate) fn distribute_conditional(
414        &mut self,
415        members: &[TypeId],
416        original_check_type: TypeId,
417        extends_type: TypeId,
418        true_type: TypeId,
419        false_type: TypeId,
420    ) -> TypeId {
421        // Limit distribution to prevent OOM with large unions
422        const MAX_DISTRIBUTION_SIZE: usize = 100;
423        if members.len() > MAX_DISTRIBUTION_SIZE {
424            self.mark_depth_exceeded();
425            return TypeId::ERROR;
426        }
427
428        let mut results: Vec<TypeId> = Vec::with_capacity(members.len());
429
430        for &member in members {
431            // Check if depth was exceeded during previous iterations
432            if self.is_depth_exceeded() {
433                return TypeId::ERROR;
434            }
435
436            // Substitute the specific member if true_type or false_type references the original check_type
437            // This handles cases like: NonNullable<T> = T extends null ? never : T
438            // When T = A | B, we need (A extends null ? never : A) | (B extends null ? never : B)
439            let substituted_true_type = if true_type == original_check_type {
440                member
441            } else {
442                true_type
443            };
444            let substituted_false_type = if false_type == original_check_type {
445                member
446            } else {
447                false_type
448            };
449
450            // Create conditional for this union member
451            let member_cond = ConditionalType {
452                check_type: member,
453                extends_type,
454                true_type: substituted_true_type,
455                false_type: substituted_false_type,
456                is_distributive: false,
457            };
458
459            // Recursively evaluate via evaluate() to respect depth limits
460            let cond_type = self.interner().conditional(member_cond);
461            let result = self.evaluate(cond_type);
462            // Check if evaluation hit depth limit
463            if result == TypeId::ERROR && self.is_depth_exceeded() {
464                return TypeId::ERROR;
465            }
466            results.push(result);
467        }
468
469        // Combine results into a union
470        self.interner().union(results)
471    }
472
473    /// Handle array extends pattern: T extends (infer U)[] ? ...
474    fn eval_conditional_array_infer(
475        &mut self,
476        cond: &ConditionalType,
477        check_unwrapped: TypeId,
478        info: TypeParamInfo,
479    ) -> TypeId {
480        if matches!(
481            self.interner().lookup(check_unwrapped),
482            Some(TypeData::TypeParameter(_) | TypeData::Infer(_))
483        ) {
484            return self.interner().conditional(cond.clone());
485        }
486
487        let inferred = match self.interner().lookup(check_unwrapped) {
488            Some(TypeData::Array(elem)) => Some(elem),
489            Some(TypeData::Tuple(elements)) => {
490                let elements = self.interner().tuple_list(elements);
491                let mut parts = Vec::new();
492                for element in elements.iter() {
493                    if element.rest {
494                        let rest_type = self.rest_element_type(element.type_id);
495                        parts.push(rest_type);
496                    } else {
497                        let elem_type = if element.optional {
498                            self.interner().union2(element.type_id, TypeId::UNDEFINED)
499                        } else {
500                            element.type_id
501                        };
502                        parts.push(elem_type);
503                    }
504                }
505                if parts.is_empty() {
506                    None
507                } else {
508                    Some(self.interner().union(parts))
509                }
510            }
511            Some(TypeData::Union(members)) => {
512                let members = self.interner().type_list(members);
513                let mut parts = Vec::new();
514                for &member in members.iter() {
515                    match self.interner().lookup(member) {
516                        Some(TypeData::Array(elem)) => parts.push(elem),
517                        Some(TypeData::ReadonlyType(inner)) => {
518                            let Some(TypeData::Array(elem)) = self.interner().lookup(inner) else {
519                                return self.evaluate(cond.false_type);
520                            };
521                            parts.push(elem);
522                        }
523                        _ => return self.evaluate(cond.false_type),
524                    }
525                }
526                if parts.is_empty() {
527                    None
528                } else if parts.len() == 1 {
529                    Some(parts[0])
530                } else {
531                    Some(self.interner().union(parts))
532                }
533            }
534            _ => None,
535        };
536
537        let Some(mut inferred) = inferred else {
538            return self.evaluate(cond.false_type);
539        };
540
541        let mut subst = TypeSubstitution::new();
542        subst.insert(info.name, inferred);
543
544        if let Some(constraint) = info.constraint {
545            let mut checker = SubtypeChecker::with_resolver(self.interner(), self.resolver());
546            checker.allow_bivariant_rest = true;
547            let is_union = matches!(self.interner().lookup(inferred), Some(TypeData::Union(_)));
548            if is_union && !cond.is_distributive {
549                // For unions in non-distributive conditionals, use filter that adds undefined
550                inferred = self.filter_inferred_by_constraint_or_undefined(
551                    inferred,
552                    constraint,
553                    &mut checker,
554                );
555            } else {
556                // For single values or distributive conditionals, fail if constraint doesn't match
557                if !checker.is_subtype_of(inferred, constraint) {
558                    return self.evaluate(cond.false_type);
559                }
560            }
561            subst.insert(info.name, inferred);
562        }
563
564        let true_inst = instantiate_type_with_infer(self.interner(), cond.true_type, &subst);
565        self.evaluate(true_inst)
566    }
567
568    /// Handle tuple extends pattern: T extends [infer U] ? ...
569    fn eval_conditional_tuple_infer(
570        &mut self,
571        cond: &ConditionalType,
572        check_unwrapped: TypeId,
573        extends_elem: &TupleElement,
574        info: TypeParamInfo,
575    ) -> TypeId {
576        if matches!(
577            self.interner().lookup(check_unwrapped),
578            Some(TypeData::TypeParameter(_) | TypeData::Infer(_))
579        ) {
580            return self.interner().conditional(cond.clone());
581        }
582
583        let inferred = match self.interner().lookup(check_unwrapped) {
584            Some(TypeData::Tuple(check_elements)) => {
585                let check_elements = self.interner().tuple_list(check_elements);
586                if check_elements.is_empty() {
587                    extends_elem.optional.then_some(TypeId::UNDEFINED)
588                } else if check_elements.len() == 1 && !check_elements[0].rest {
589                    let elem = &check_elements[0];
590                    Some(if elem.optional {
591                        self.interner().union2(elem.type_id, TypeId::UNDEFINED)
592                    } else {
593                        elem.type_id
594                    })
595                } else {
596                    None
597                }
598            }
599            Some(TypeData::Union(members)) => {
600                let members = self.interner().type_list(members);
601                let mut inferred_members = Vec::new();
602                for &member in members.iter() {
603                    let member_type = match self.interner().lookup(member) {
604                        Some(TypeData::ReadonlyType(inner)) => inner,
605                        _ => member,
606                    };
607                    match self.interner().lookup(member_type) {
608                        Some(TypeData::Tuple(check_elements)) => {
609                            let check_elements = self.interner().tuple_list(check_elements);
610                            if check_elements.is_empty() {
611                                if extends_elem.optional {
612                                    inferred_members.push(TypeId::UNDEFINED);
613                                    continue;
614                                }
615                                return self.evaluate(cond.false_type);
616                            }
617                            if check_elements.len() == 1 && !check_elements[0].rest {
618                                let elem = &check_elements[0];
619                                let elem_type = if elem.optional {
620                                    self.interner().union2(elem.type_id, TypeId::UNDEFINED)
621                                } else {
622                                    elem.type_id
623                                };
624                                inferred_members.push(elem_type);
625                            } else {
626                                return self.evaluate(cond.false_type);
627                            }
628                        }
629                        _ => return self.evaluate(cond.false_type),
630                    }
631                }
632                if inferred_members.is_empty() {
633                    None
634                } else if inferred_members.len() == 1 {
635                    Some(inferred_members[0])
636                } else {
637                    Some(self.interner().union(inferred_members))
638                }
639            }
640            _ => None,
641        };
642
643        let Some(mut inferred) = inferred else {
644            return self.evaluate(cond.false_type);
645        };
646
647        let mut subst = TypeSubstitution::new();
648        subst.insert(info.name, inferred);
649
650        if let Some(constraint) = info.constraint {
651            let mut checker = SubtypeChecker::with_resolver(self.interner(), self.resolver());
652            checker.allow_bivariant_rest = true;
653            let Some(filtered) =
654                self.filter_inferred_by_constraint(inferred, constraint, &mut checker)
655            else {
656                let false_inst =
657                    instantiate_type_with_infer(self.interner(), cond.false_type, &subst);
658                return self.evaluate(false_inst);
659            };
660            inferred = filtered;
661            subst.insert(info.name, inferred);
662        }
663
664        let true_inst = instantiate_type_with_infer(self.interner(), cond.true_type, &subst);
665        self.evaluate(true_inst)
666    }
667
668    /// Handle object extends pattern: T extends { prop: infer U } ? ...
669    fn eval_conditional_object_infer(
670        &mut self,
671        cond: &ConditionalType,
672        check_unwrapped: TypeId,
673        extends_shape_id: ObjectShapeId,
674    ) -> Option<TypeId> {
675        let extends_shape = self.interner().object_shape(extends_shape_id);
676        let mut infer_prop = None;
677        let mut infer_nested = None;
678
679        for prop in &extends_shape.properties {
680            if let Some(TypeData::Infer(info)) = self.interner().lookup(prop.type_id) {
681                if infer_prop.is_some() || infer_nested.is_some() {
682                    return None;
683                }
684                infer_prop = Some((prop.name, info, prop.optional));
685                continue;
686            }
687
688            let nested_type = match self.interner().lookup(prop.type_id) {
689                Some(TypeData::ReadonlyType(inner)) => inner,
690                _ => prop.type_id,
691            };
692            if let Some(nested_shape_id) = match self.interner().lookup(nested_type) {
693                Some(TypeData::Object(shape_id) | TypeData::ObjectWithIndex(shape_id)) => {
694                    Some(shape_id)
695                }
696                _ => None,
697            } {
698                let nested_shape = self.interner().object_shape(nested_shape_id);
699                let mut nested_infer = None;
700                for nested_prop in &nested_shape.properties {
701                    if let Some(TypeData::Infer(info)) = self.interner().lookup(nested_prop.type_id)
702                    {
703                        if nested_infer.is_some() {
704                            nested_infer = None;
705                            break;
706                        }
707                        nested_infer = Some((nested_prop.name, info));
708                    }
709                }
710                if let Some((nested_name, info)) = nested_infer {
711                    if infer_prop.is_some() || infer_nested.is_some() {
712                        return None;
713                    }
714                    infer_nested = Some((prop.name, nested_name, info));
715                }
716            }
717        }
718
719        if let Some((prop_name, info, prop_optional)) = infer_prop {
720            return Some(self.eval_conditional_object_prop_infer(
721                cond,
722                check_unwrapped,
723                prop_name,
724                info,
725                prop_optional,
726            ));
727        }
728
729        if let Some((outer_name, inner_name, info)) = infer_nested {
730            return Some(self.eval_conditional_object_nested_infer(
731                cond,
732                check_unwrapped,
733                outer_name,
734                inner_name,
735                info,
736            ));
737        }
738
739        None
740    }
741
742    /// Handle object property infer pattern
743    fn eval_conditional_object_prop_infer(
744        &mut self,
745        cond: &ConditionalType,
746        check_unwrapped: TypeId,
747        prop_name: tsz_common::interner::Atom,
748        info: TypeParamInfo,
749        prop_optional: bool,
750    ) -> TypeId {
751        if matches!(
752            self.interner().lookup(check_unwrapped),
753            Some(TypeData::TypeParameter(_) | TypeData::Infer(_))
754        ) {
755            return self.interner().conditional(cond.clone());
756        }
757
758        let inferred = match self.interner().lookup(check_unwrapped) {
759            Some(TypeData::Object(shape_id) | TypeData::ObjectWithIndex(shape_id)) => {
760                let shape = self.interner().object_shape(shape_id);
761                shape
762                    .properties
763                    .iter()
764                    .find(|prop| prop.name == prop_name)
765                    .map(|prop| {
766                        if prop_optional {
767                            self.optional_property_type(prop)
768                        } else {
769                            prop.type_id
770                        }
771                    })
772                    .or_else(|| prop_optional.then_some(TypeId::UNDEFINED))
773            }
774            Some(TypeData::Union(members)) => {
775                let members = self.interner().type_list(members);
776                let mut inferred_members = Vec::new();
777                for &member in members.iter() {
778                    let member_unwrapped = match self.interner().lookup(member) {
779                        Some(TypeData::ReadonlyType(inner)) => inner,
780                        _ => member,
781                    };
782                    match self.interner().lookup(member_unwrapped) {
783                        Some(TypeData::Object(shape_id) | TypeData::ObjectWithIndex(shape_id)) => {
784                            let shape = self.interner().object_shape(shape_id);
785                            if let Some(prop) =
786                                PropertyInfo::find_in_slice(&shape.properties, prop_name)
787                            {
788                                inferred_members.push(if prop_optional {
789                                    self.optional_property_type(prop)
790                                } else {
791                                    prop.type_id
792                                });
793                            } else if prop_optional {
794                                inferred_members.push(TypeId::UNDEFINED);
795                            } else {
796                                return self.evaluate(cond.false_type);
797                            }
798                        }
799                        _ => return self.evaluate(cond.false_type),
800                    }
801                }
802                if inferred_members.is_empty() {
803                    None
804                } else if inferred_members.len() == 1 {
805                    Some(inferred_members[0])
806                } else {
807                    Some(self.interner().union(inferred_members))
808                }
809            }
810            _ => None,
811        };
812
813        let Some(mut inferred) = inferred else {
814            return self.evaluate(cond.false_type);
815        };
816
817        let mut subst = TypeSubstitution::new();
818        subst.insert(info.name, inferred);
819
820        if let Some(constraint) = info.constraint {
821            let mut checker = SubtypeChecker::with_resolver(self.interner(), self.resolver());
822            checker.allow_bivariant_rest = true;
823            let is_union = matches!(self.interner().lookup(inferred), Some(TypeData::Union(_)));
824            if prop_optional {
825                let Some(filtered) =
826                    self.filter_inferred_by_constraint(inferred, constraint, &mut checker)
827                else {
828                    let false_inst =
829                        instantiate_type_with_infer(self.interner(), cond.false_type, &subst);
830                    return self.evaluate(false_inst);
831                };
832                inferred = filtered;
833            } else if is_union || cond.is_distributive {
834                // For unions or distributive conditionals, use filter that adds undefined
835                inferred = self.filter_inferred_by_constraint_or_undefined(
836                    inferred,
837                    constraint,
838                    &mut checker,
839                );
840            } else {
841                // For non-distributive single values, fail if constraint doesn't match
842                if !checker.is_subtype_of(inferred, constraint) {
843                    return self.evaluate(cond.false_type);
844                }
845            }
846            subst.insert(info.name, inferred);
847        }
848
849        let true_inst = instantiate_type_with_infer(self.interner(), cond.true_type, &subst);
850        self.evaluate(true_inst)
851    }
852
853    /// Handle nested object infer pattern
854    fn eval_conditional_object_nested_infer(
855        &mut self,
856        cond: &ConditionalType,
857        check_unwrapped: TypeId,
858        outer_name: tsz_common::interner::Atom,
859        inner_name: tsz_common::interner::Atom,
860        info: TypeParamInfo,
861    ) -> TypeId {
862        if matches!(
863            self.interner().lookup(check_unwrapped),
864            Some(TypeData::TypeParameter(_) | TypeData::Infer(_))
865        ) {
866            return self.interner().conditional(cond.clone());
867        }
868
869        let inferred = match self.interner().lookup(check_unwrapped) {
870            Some(TypeData::Object(shape_id) | TypeData::ObjectWithIndex(shape_id)) => {
871                let shape = self.interner().object_shape(shape_id);
872                shape
873                    .properties
874                    .iter()
875                    .find(|prop| prop.name == outer_name)
876                    .and_then(|prop| {
877                        let inner_type = match self.interner().lookup(prop.type_id) {
878                            Some(TypeData::ReadonlyType(inner)) => inner,
879                            _ => prop.type_id,
880                        };
881                        match self.interner().lookup(inner_type) {
882                            Some(
883                                TypeData::Object(inner_shape_id)
884                                | TypeData::ObjectWithIndex(inner_shape_id),
885                            ) => {
886                                let inner_shape = self.interner().object_shape(inner_shape_id);
887                                inner_shape
888                                    .properties
889                                    .iter()
890                                    .find(|prop| prop.name == inner_name)
891                                    .map(|prop| prop.type_id)
892                            }
893                            _ => None,
894                        }
895                    })
896            }
897            Some(TypeData::Union(members)) => {
898                let members = self.interner().type_list(members);
899                let mut inferred_members = Vec::new();
900                for &member in members.iter() {
901                    let member_unwrapped = match self.interner().lookup(member) {
902                        Some(TypeData::ReadonlyType(inner)) => inner,
903                        _ => member,
904                    };
905                    let Some(TypeData::Object(shape_id) | TypeData::ObjectWithIndex(shape_id)) =
906                        self.interner().lookup(member_unwrapped)
907                    else {
908                        return self.evaluate(cond.false_type);
909                    };
910                    let shape = self.interner().object_shape(shape_id);
911                    let Some(prop) = PropertyInfo::find_in_slice(&shape.properties, outer_name)
912                    else {
913                        return self.evaluate(cond.false_type);
914                    };
915                    let inner_type = match self.interner().lookup(prop.type_id) {
916                        Some(TypeData::ReadonlyType(inner)) => inner,
917                        _ => prop.type_id,
918                    };
919                    let Some(
920                        TypeData::Object(inner_shape_id)
921                        | TypeData::ObjectWithIndex(inner_shape_id),
922                    ) = self.interner().lookup(inner_type)
923                    else {
924                        return self.evaluate(cond.false_type);
925                    };
926                    let inner_shape = self.interner().object_shape(inner_shape_id);
927                    let Some(inner_prop) = inner_shape
928                        .properties
929                        .iter()
930                        .find(|prop| prop.name == inner_name)
931                    else {
932                        return self.evaluate(cond.false_type);
933                    };
934                    inferred_members.push(inner_prop.type_id);
935                }
936                if inferred_members.is_empty() {
937                    None
938                } else if inferred_members.len() == 1 {
939                    Some(inferred_members[0])
940                } else {
941                    Some(self.interner().union(inferred_members))
942                }
943            }
944            _ => None,
945        };
946
947        let Some(mut inferred) = inferred else {
948            return self.evaluate(cond.false_type);
949        };
950
951        let mut subst = TypeSubstitution::new();
952        subst.insert(info.name, inferred);
953
954        if let Some(constraint) = info.constraint {
955            let mut checker = SubtypeChecker::with_resolver(self.interner(), self.resolver());
956            checker.allow_bivariant_rest = true;
957            let is_union = matches!(self.interner().lookup(inferred), Some(TypeData::Union(_)));
958            if is_union || cond.is_distributive {
959                // For unions or distributive conditionals, use filter that adds undefined
960                inferred = self.filter_inferred_by_constraint_or_undefined(
961                    inferred,
962                    constraint,
963                    &mut checker,
964                );
965            } else {
966                // For non-distributive single values, fail if constraint doesn't match
967                if !checker.is_subtype_of(inferred, constraint) {
968                    return self.evaluate(cond.false_type);
969                }
970            }
971            subst.insert(info.name, inferred);
972        }
973
974        let true_inst = instantiate_type_with_infer(self.interner(), cond.true_type, &subst);
975        self.evaluate(true_inst)
976    }
977
978    /// Try to match conditional types at the Application level before structural expansion.
979    ///
980    /// When both `check_type` and `extends_type` are Applications with the same base type
981    /// (e.g., `Promise<string>` vs `Promise<infer U>`), we can match type arguments
982    /// directly without expanding the interface structure. This is critical for complex
983    /// generic interfaces like Promise, Map, Set where structural expansion makes the
984    /// infer pattern matching fail.
985    fn try_application_infer_match(&mut self, cond: &ConditionalType) -> Option<TypeId> {
986        // Only proceed if extends_type is an Application containing infer.
987        // Keep extends_type as-is (unevaluated) so match_infer_pattern can handle
988        // it at the Application level. This is critical for complex generic interfaces
989        // like Promise, Map, Set where structural expansion loses the ability to
990        // match type arguments directly.
991        let Some(TypeData::Application(_)) = self.interner().lookup(cond.extends_type) else {
992            return None;
993        };
994
995        if !self.type_contains_infer(cond.extends_type) {
996            return None;
997        }
998
999        // Use the raw (unevaluated) check_type — it may still be an Application
1000        // which enables Application-vs-Application matching in match_infer_pattern.
1001        let check_type = cond.check_type;
1002
1003        // Skip for special types
1004        if check_type == TypeId::ANY || check_type == TypeId::NEVER {
1005            return None;
1006        }
1007        if matches!(
1008            self.interner().lookup(check_type),
1009            Some(TypeData::TypeParameter(_))
1010        ) {
1011            return None;
1012        }
1013
1014        // Try infer pattern matching with unevaluated types.
1015        // match_infer_pattern handles Application vs Application matching
1016        // by comparing base types and recursing on type arguments.
1017        let mut checker = SubtypeChecker::with_resolver(self.interner(), self.resolver());
1018        checker.allow_bivariant_rest = true;
1019        let mut bindings = FxHashMap::default();
1020        let mut visited = FxHashSet::default();
1021        if self.match_infer_pattern(
1022            check_type,
1023            cond.extends_type,
1024            &mut bindings,
1025            &mut visited,
1026            &mut checker,
1027        ) && !bindings.is_empty()
1028        {
1029            let substituted_true = self.substitute_infer(cond.true_type, &bindings);
1030            return Some(self.evaluate(substituted_true));
1031        }
1032
1033        None
1034    }
1035}