Skip to main content

tsz_checker/checkers/
iterable_checker.rs

1//! Iterable/iterator protocol checking and for-of element type computation.
2
3use crate::diagnostics::{diagnostic_codes, diagnostic_messages, format_message};
4use crate::query_boundaries::iterable_checker::{
5    AsyncIterableTypeKind, ForOfElementKind, FullIterableTypeKind, call_signatures_for_type,
6    classify_async_iterable_type, classify_for_of_element_type, classify_full_iterable_type,
7    function_shape_for_type, is_array_type, is_string_literal_type, is_string_type, is_tuple_type,
8    union_members_for_type,
9};
10use crate::state::CheckerState;
11use tsz_parser::parser::NodeIndex;
12use tsz_solver::TypeId;
13
14// =============================================================================
15// Iterable Type Checking Methods
16// =============================================================================
17
18impl<'a> CheckerState<'a> {
19    // =========================================================================
20    // Iterable Protocol Checking
21    // =========================================================================
22
23    /// Check if a type is iterable (has Symbol.iterator protocol).
24    ///
25    /// A type is iterable if it is:
26    /// - String type
27    /// - Array type
28    /// - Tuple type
29    /// - Has a [Symbol.iterator] method
30    /// - A union where all members are iterable
31    /// - An intersection where at least one member is iterable
32    pub fn is_iterable_type(&mut self, type_id: TypeId) -> bool {
33        // Intrinsic types that are always iterable or not iterable
34        if type_id == TypeId::ANY || type_id == TypeId::UNKNOWN || type_id == TypeId::ERROR {
35            return true; // Don't report errors on any/unknown/error
36        }
37        if type_id == TypeId::STRING {
38            return true;
39        }
40        if type_id == TypeId::NUMBER
41            || type_id == TypeId::BOOLEAN
42            || type_id == TypeId::VOID
43            || type_id == TypeId::NULL
44            || type_id == TypeId::UNDEFINED
45            || type_id == TypeId::NEVER
46            || type_id == TypeId::SYMBOL
47            || type_id == TypeId::BIGINT
48        {
49            return false;
50        }
51
52        self.is_iterable_type_classified(type_id)
53    }
54
55    /// Internal helper that uses the solver's classification enum to determine iterability.
56    fn is_iterable_type_classified(&mut self, type_id: TypeId) -> bool {
57        let kind = classify_full_iterable_type(self.ctx.types, type_id);
58        match kind {
59            FullIterableTypeKind::Array(_)
60            | FullIterableTypeKind::Tuple(_)
61            | FullIterableTypeKind::StringLiteral(_) => true,
62            FullIterableTypeKind::Union(members) => {
63                members.iter().all(|&m| self.is_iterable_type(m))
64            }
65            FullIterableTypeKind::Intersection(members) => {
66                // Intersection is iterable if at least one member is iterable
67                members.iter().any(|&m| self.is_iterable_type(m))
68            }
69            FullIterableTypeKind::Object(shape_id) => {
70                // Check if object has a [Symbol.iterator] method
71                // Fall back to property access resolution for computed properties
72                // (e.g., `[Symbol.iterator]: any` may not be stored as a method in the shape)
73                self.object_has_iterator_method(shape_id)
74                    || self.type_has_symbol_iterator_via_property_access(type_id)
75            }
76            FullIterableTypeKind::Application { .. } => {
77                // Application types (Set<T>, Map<K,V>, Iterable<T>, etc.) may have
78                // Lazy(DefId) bases that can't be resolved through the type classification.
79                // Use the full property access resolution which handles all the complex
80                // resolution paths including Application types with Lazy bases from lib files.
81                self.type_has_symbol_iterator_via_property_access(type_id)
82            }
83            FullIterableTypeKind::TypeParameter { constraint } => {
84                if let Some(c) = constraint {
85                    self.is_iterable_type(c)
86                } else {
87                    // Unconstrained type parameters (extends unknown/any) should not error
88                    // TypeScript does NOT emit TS2488 for unconstrained type parameters
89                    false
90                }
91            }
92            FullIterableTypeKind::Readonly(inner) => {
93                // Unwrap readonly wrapper and check inner type
94                self.is_iterable_type(inner)
95            }
96            // Index access, Conditional, Mapped - not directly iterable
97            FullIterableTypeKind::ComplexType => false,
98            // Functions, classes without Symbol.iterator are not iterable
99            FullIterableTypeKind::FunctionOrCallable => {
100                // Callable types can have properties (including [Symbol.iterator])
101                self.type_has_symbol_iterator_via_property_access(type_id)
102            }
103            // Lazy(DefId) from lib files - use property access to resolve
104            FullIterableTypeKind::NotIterable => {
105                self.type_has_symbol_iterator_via_property_access(type_id)
106            }
107        }
108    }
109
110    /// Check if an object shape has a Symbol.iterator method.
111    ///
112    /// An object is iterable if it has a [Symbol.iterator]() method that returns an iterator.
113    /// An iterator (with just a `next()` method) is NOT automatically iterable.
114    fn object_has_iterator_method(&self, shape_id: tsz_solver::ObjectShapeId) -> bool {
115        let shape = self.ctx.types.object_shape(shape_id);
116
117        // Check for [Symbol.iterator] method (iterable protocol)
118        for prop in &shape.properties {
119            let prop_name = self.ctx.types.resolve_atom_ref(prop.name);
120            if prop_name.as_ref() == "[Symbol.iterator]" {
121                if prop.is_method {
122                    return true;
123                }
124                // Non-method properties typed as `any` are callable, so treat them as valid.
125                // e.g., `class Foo { [Symbol.iterator]: any; }`
126                if prop.type_id == TypeId::ANY {
127                    return true;
128                }
129            }
130        }
131
132        false
133    }
134
135    /// Check if a type has [Symbol.iterator] using the full property access resolution.
136    /// This handles Application types (Set<T>, Map<K,V>) with Lazy(DefId) bases from lib
137    /// files, Callable types with iterator properties, and other complex cases where simple
138    /// shape inspection fails but the full checker resolution machinery can find the property.
139    fn type_has_symbol_iterator_via_property_access(&mut self, type_id: TypeId) -> bool {
140        use tsz_solver::operations::property::PropertyAccessResult;
141        let result = self.resolve_property_access_with_env(type_id, "[Symbol.iterator]");
142        matches!(result, PropertyAccessResult::Success { .. })
143    }
144
145    /// Check if a type has a numeric index signature, making it "array-like".
146    /// TypeScript allows array destructuring of array-like types without [Symbol.iterator]().
147    pub(crate) fn has_numeric_index_signature(&mut self, type_id: TypeId) -> bool {
148        // Resolve lazy types first
149        let type_id = self.resolve_lazy_type(type_id);
150        match classify_full_iterable_type(self.ctx.types, type_id) {
151            FullIterableTypeKind::Object(shape_id) => {
152                let shape = self.ctx.types.object_shape(shape_id);
153                shape.number_index.is_some()
154            }
155            FullIterableTypeKind::Application { base } => self.has_numeric_index_signature(base),
156            FullIterableTypeKind::Readonly(inner) => self.has_numeric_index_signature(inner),
157            FullIterableTypeKind::Union(members) => members
158                .iter()
159                .all(|&m| self.is_iterable_type(m) || self.has_numeric_index_signature(m)),
160            _ => false,
161        }
162    }
163
164    /// Check if a type is async iterable (has Symbol.asyncIterator protocol).
165    pub fn is_async_iterable_type(&mut self, type_id: TypeId) -> bool {
166        // Intrinsic types that are always iterable or not iterable
167        if type_id == TypeId::ANY || type_id == TypeId::UNKNOWN || type_id == TypeId::ERROR {
168            return true; // Don't report errors on any/unknown/error
169        }
170
171        // Resolve lazy types before checking
172        let type_id = self.resolve_lazy_type(type_id);
173
174        self.is_async_iterable_type_classified(type_id)
175    }
176
177    /// Internal helper that uses the solver's classification enum to determine async iterability.
178    fn is_async_iterable_type_classified(&mut self, type_id: TypeId) -> bool {
179        match classify_async_iterable_type(self.ctx.types, type_id) {
180            AsyncIterableTypeKind::Union(members) => {
181                members.iter().all(|&m| self.is_async_iterable_type(m))
182            }
183            AsyncIterableTypeKind::Object(shape_id) => {
184                // Check if object has a [Symbol.asyncIterator] method
185                let shape = self.ctx.types.object_shape(shape_id);
186                for prop in &shape.properties {
187                    let prop_name = self.ctx.types.resolve_atom_ref(prop.name);
188                    if prop_name.as_ref() == "[Symbol.asyncIterator]"
189                        && prop.is_method
190                        && self.is_callable_with_no_required_args(prop.type_id)
191                    {
192                        return true;
193                    }
194                }
195                false
196            }
197            AsyncIterableTypeKind::Readonly(inner) => {
198                // Unwrap readonly wrapper and check inner type
199                self.is_async_iterable_type(inner)
200            }
201            AsyncIterableTypeKind::NotAsyncIterable => {
202                // Use property access to check for [Symbol.asyncIterator] on types
203                // that couldn't be classified (e.g., Application types with Lazy bases).
204                use tsz_solver::operations::property::PropertyAccessResult;
205                let result =
206                    self.resolve_property_access_with_env(type_id, "[Symbol.asyncIterator]");
207                match result {
208                    PropertyAccessResult::Success { type_id, .. } => {
209                        self.is_callable_with_no_required_args(type_id)
210                    }
211                    _ => false,
212                }
213            }
214        }
215    }
216
217    /// Returns true when a callable type can be invoked with zero arguments.
218    ///
219    /// The async iterable protocol requires `[Symbol.asyncIterator]()` to be callable
220    /// without arguments. A required parameter (e.g. `(x: number) => ...`) is invalid.
221    fn is_callable_with_no_required_args(&self, callable_type: TypeId) -> bool {
222        if callable_type == TypeId::ANY
223            || callable_type == TypeId::UNKNOWN
224            || callable_type == TypeId::ERROR
225        {
226            return true;
227        }
228
229        if let Some(sig) = function_shape_for_type(self.ctx.types, callable_type) {
230            return sig.params.iter().all(|p| p.optional || p.rest);
231        }
232
233        if let Some(call_signatures) = call_signatures_for_type(self.ctx.types, callable_type) {
234            return call_signatures
235                .iter()
236                .any(|sig| sig.params.iter().all(|p| p.optional || p.rest));
237        }
238
239        false
240    }
241
242    // =========================================================================
243    // For-Of Element Type Computation
244    // =========================================================================
245
246    /// Compute the element type produced by a `for (... of expr)` loop.
247    ///
248    /// Handles arrays, tuples, unions, strings, and custom iterators via
249    /// the `[Symbol.iterator]().next().value` protocol.
250    pub fn for_of_element_type(&mut self, iterable_type: TypeId) -> TypeId {
251        if iterable_type == TypeId::ANY
252            || iterable_type == TypeId::UNKNOWN
253            || iterable_type == TypeId::ERROR
254        {
255            return iterable_type;
256        }
257
258        // String iteration yields string
259        if iterable_type == TypeId::STRING {
260            return TypeId::STRING;
261        }
262
263        // Resolve lazy types (type aliases) before computing element type
264        let iterable_type = self.resolve_lazy_type(iterable_type);
265
266        self.for_of_element_type_classified(iterable_type, 0)
267    }
268
269    /// Internal helper that uses the solver's classification enum to compute element type.
270    /// The depth parameter prevents infinite loops from circular readonly types.
271    fn for_of_element_type_classified(&mut self, type_id: TypeId, depth: usize) -> TypeId {
272        let factory = self.ctx.types.factory();
273        if depth > 100 {
274            return TypeId::ANY;
275        }
276
277        // Handle string types (including string literals)
278        if type_id == TypeId::STRING {
279            return TypeId::STRING;
280        }
281
282        match classify_for_of_element_type(self.ctx.types, type_id) {
283            ForOfElementKind::Array(elem) => elem,
284            ForOfElementKind::Tuple(elements) => {
285                let member_types: Vec<TypeId> = elements.iter().map(|e| e.type_id).collect();
286                tsz_solver::utils::union_or_single(self.ctx.types, member_types)
287            }
288            ForOfElementKind::Union(members) => {
289                let mut element_types = Vec::with_capacity(members.len());
290                for member in members {
291                    element_types.push(self.for_of_element_type_classified(member, depth + 1));
292                }
293                factory.union(element_types)
294            }
295            ForOfElementKind::Readonly(inner) => {
296                // Unwrap readonly wrapper and compute element type for inner
297                self.for_of_element_type_classified(inner, depth + 1)
298            }
299            ForOfElementKind::String => TypeId::STRING,
300            ForOfElementKind::Other => {
301                // For custom iterators, Application types (Map, Set), etc.,
302                // try to resolve the element type via the iterator protocol:
303                // type_id[Symbol.iterator]().next().value
304                self.resolve_iterator_element_type(type_id)
305            }
306        }
307    }
308
309    /// Resolve the element type of an iterable via the iterator protocol.
310    ///
311    /// Follows the chain: type[Symbol.iterator] → call result → .`next()` → .value
312    /// Returns ANY as fallback if the protocol cannot be resolved.
313    fn resolve_iterator_element_type(&mut self, type_id: TypeId) -> TypeId {
314        use tsz_solver::operations::property::PropertyAccessResult;
315
316        // Step 1: Get [Symbol.iterator] property
317        let iterator_fn = self.resolve_property_access_with_env(type_id, "[Symbol.iterator]");
318        let iterator_fn_type = match &iterator_fn {
319            PropertyAccessResult::Success { type_id, .. } => *type_id,
320            _ => return TypeId::ANY,
321        };
322
323        // Step 2: Get the return type of the iterator function (call it)
324        let iterator_type = self.get_call_return_type(iterator_fn_type);
325
326        // If the iterator function returns `any` (e.g., `[Symbol.iterator]() { return this; }`
327        // where `this` type inference fails), fall back to using the original object type.
328        // This is the common pattern where the object IS the iterator.
329        let iterator_type = if iterator_type == TypeId::ANY {
330            type_id
331        } else {
332            iterator_type
333        };
334
335        // Step 3: Get .next() on the iterator
336        let next_result = self.resolve_property_access_with_env(iterator_type, "next");
337        let next_fn_type = match &next_result {
338            PropertyAccessResult::Success { type_id, .. } => *type_id,
339            _ => return TypeId::ANY,
340        };
341
342        // Step 4: Get the return type of next()
343        let next_return = self.get_call_return_type(next_fn_type);
344
345        // Step 5: Get .value from the IteratorResult
346        let value_result = self.resolve_property_access_with_env(next_return, "value");
347        match &value_result {
348            PropertyAccessResult::Success { type_id, .. } => *type_id,
349            _ => TypeId::ANY,
350        }
351    }
352
353    /// Get the return type of calling a function type.
354    /// Returns ANY if the type is not callable.
355    fn get_call_return_type(&self, fn_type: TypeId) -> TypeId {
356        if fn_type == TypeId::ANY {
357            return TypeId::ANY;
358        }
359        if let Some(sig) = function_shape_for_type(self.ctx.types, fn_type) {
360            return sig.return_type;
361        }
362        if let Some(call_signatures) = call_signatures_for_type(self.ctx.types, fn_type) {
363            return call_signatures
364                .first()
365                .map_or(TypeId::ANY, |sig| sig.return_type);
366        }
367        TypeId::ANY
368    }
369
370    // =========================================================================
371    // For-Of Iterability Checking with Error Reporting
372    // =========================================================================
373
374    /// Check iterability of a for-of expression and emit TS2488/TS2495/TS2504 if not iterable.
375    ///
376    /// Returns `true` if the type is iterable (or async iterable for for-await-of).
377    pub fn check_for_of_iterability(
378        &mut self,
379        expr_type: TypeId,
380        expr_idx: NodeIndex,
381        is_async: bool,
382    ) -> bool {
383        // Skip error/any/unknown types to prevent false positives
384        if expr_type == TypeId::ANY || expr_type == TypeId::UNKNOWN || expr_type == TypeId::ERROR {
385            return true;
386        }
387
388        // Resolve lazy types (type aliases) before checking iterability
389        let expr_type = self.resolve_lazy_type(expr_type);
390
391        // Check if the expression is nullish (undefined/null)
392        // Emit TS18050 "The value 'undefined'/'null' cannot be used here"
393        // when trying to iterate over undefined/null
394        if expr_type == TypeId::NULL || expr_type == TypeId::UNDEFINED {
395            self.report_nullish_object(expr_idx, expr_type, true);
396            return false;
397        }
398
399        // For async for-of, first check async iterable, then fall back to sync iterable
400        if is_async {
401            if self.is_async_iterable_type(expr_type) || self.is_iterable_type(expr_type) {
402                return true;
403            }
404            // Not async iterable - emit TS2504
405            if let Some((start, end)) = self.get_node_span(expr_idx) {
406                let type_str = self.format_type(expr_type);
407                let message = format_message(
408                    diagnostic_messages::TYPE_MUST_HAVE_A_SYMBOL_ASYNCITERATOR_METHOD_THAT_RETURNS_AN_ASYNC_ITERATOR,
409                    &[&type_str],
410                );
411                self.error(
412                    start,
413                    end.saturating_sub(start),
414                    message,
415                    diagnostic_codes::TYPE_MUST_HAVE_A_SYMBOL_ASYNCITERATOR_METHOD_THAT_RETURNS_AN_ASYNC_ITERATOR,
416                );
417            }
418            return false;
419        }
420
421        // In ES5 mode (without downlevelIteration), for-of only works with arrays and strings.
422        // - Emit TS2802 if the type has Symbol.iterator (iterable but requires ES2015/downlevelIteration).
423        // - Emit TS2461 if the type contains a string constituent but the remaining non-string
424        //   type is not array-like (TSC strips strings from union before checking array-likeness).
425        // - Emit TS2495 if the type is neither an array nor a string (not iterable at all).
426        if self.ctx.compiler_options.target.is_es5() {
427            if self.is_array_or_tuple_or_string(expr_type) {
428                return true;
429            }
430            // Mirror TSC's logic: strip string-like members from union types.
431            // If there were string members, the "remaining" non-string type still needs to be
432            // array-like, and the error message changes from TS2495 → TS2461 (no "or string type"
433            // suffix because the string part is already accounted for).
434            let has_string_constituent = self.has_string_constituent(expr_type);
435            let allows_strings = !has_string_constituent;
436            if let Some((start, end)) = self.get_node_span(expr_idx) {
437                let type_str = self.format_type(expr_type);
438                // Check if the type has Symbol.iterator (iterable but not usable in ES5 for-of
439                // without downlevelIteration). These emit TS2802 instead of TS2495/TS2461.
440                if self.is_iterable_type(expr_type) {
441                    let message = format_message(
442                        diagnostic_messages::TYPE_CAN_ONLY_BE_ITERATED_THROUGH_WHEN_USING_THE_DOWNLEVELITERATION_FLAG_OR_WITH,
443                        &[&type_str],
444                    );
445                    self.error(
446                        start,
447                        end.saturating_sub(start),
448                        message,
449                        diagnostic_codes::TYPE_CAN_ONLY_BE_ITERATED_THROUGH_WHEN_USING_THE_DOWNLEVELITERATION_FLAG_OR_WITH,
450                    );
451                } else if allows_strings {
452                    // No string in union: "Type is not an array type or a string type" (TS2495)
453                    let message = format_message(
454                        diagnostic_messages::TYPE_IS_NOT_AN_ARRAY_TYPE_OR_A_STRING_TYPE,
455                        &[&type_str],
456                    );
457                    self.error(
458                        start,
459                        end.saturating_sub(start),
460                        message,
461                        diagnostic_codes::TYPE_IS_NOT_AN_ARRAY_TYPE_OR_A_STRING_TYPE,
462                    );
463                } else {
464                    // Has string constituent but non-string part is not array-like: TS2461
465                    let message = format_message(
466                        diagnostic_messages::TYPE_IS_NOT_AN_ARRAY_TYPE,
467                        &[&type_str],
468                    );
469                    self.error(
470                        start,
471                        end.saturating_sub(start),
472                        message,
473                        diagnostic_codes::TYPE_IS_NOT_AN_ARRAY_TYPE,
474                    );
475                }
476            }
477            return false;
478        }
479
480        // Regular for-of (ES2015+) - check sync iterability
481        if self.is_iterable_type(expr_type) {
482            return true;
483        }
484
485        // Not iterable - emit TS2488
486
487        if let Some((start, end)) = self.get_node_span(expr_idx) {
488            let type_str = self.format_type(expr_type);
489            let message = format_message(
490                diagnostic_messages::TYPE_MUST_HAVE_A_SYMBOL_ITERATOR_METHOD_THAT_RETURNS_AN_ITERATOR,
491                &[&type_str],
492            );
493            self.error(
494                start,
495                end.saturating_sub(start),
496                message,
497                diagnostic_codes::TYPE_MUST_HAVE_A_SYMBOL_ITERATOR_METHOD_THAT_RETURNS_AN_ITERATOR,
498            );
499        }
500        false
501    }
502
503    /// Check iterability of a spread argument and emit TS2488 if not iterable.
504    ///
505    /// Used for spread in array literals and function call arguments.
506    /// Returns `true` if the type is iterable.
507    pub fn check_spread_iterability(&mut self, spread_type: TypeId, expr_idx: NodeIndex) -> bool {
508        // In ES5 without downlevel iteration, spread requires an array/tuple source.
509        // Match tsc by emitting TS2461 for non-array spread arguments.
510        if self.ctx.compiler_options.target.is_es5() {
511            if spread_type == TypeId::ANY || spread_type == TypeId::UNKNOWN {
512                return true;
513            }
514
515            let resolved = self.resolve_lazy_type(spread_type);
516            if self.is_array_or_tuple_type(resolved) || self.has_numeric_index_signature(resolved) {
517                return true;
518            }
519
520            if let Some((start, end)) = self.get_node_span(expr_idx) {
521                let type_str = self.format_type(resolved);
522                if self.is_iterable_type(resolved) {
523                    let message = format_message(
524                        diagnostic_messages::TYPE_CAN_ONLY_BE_ITERATED_THROUGH_WHEN_USING_THE_DOWNLEVELITERATION_FLAG_OR_WITH,
525                        &[&type_str],
526                    );
527                    self.error(
528                        start,
529                        end.saturating_sub(start),
530                        message,
531                        diagnostic_codes::TYPE_CAN_ONLY_BE_ITERATED_THROUGH_WHEN_USING_THE_DOWNLEVELITERATION_FLAG_OR_WITH,
532                    );
533                } else {
534                    let message = format_message(
535                        diagnostic_messages::TYPE_IS_NOT_AN_ARRAY_TYPE,
536                        &[&type_str],
537                    );
538                    self.error(
539                        start,
540                        end.saturating_sub(start),
541                        message,
542                        diagnostic_codes::TYPE_IS_NOT_AN_ARRAY_TYPE,
543                    );
544                }
545            }
546            return false;
547        }
548
549        // Skip error types and any/unknown
550        if spread_type == TypeId::ANY
551            || spread_type == TypeId::UNKNOWN
552            || spread_type == TypeId::ERROR
553        {
554            return true;
555        }
556
557        // Resolve lazy types (type aliases) before checking iterability
558        let spread_type = self.resolve_lazy_type(spread_type);
559
560        if self.is_iterable_type(spread_type) {
561            return true;
562        }
563
564        // Not iterable - emit TS2488
565
566        if let Some((start, end)) = self.get_node_span(expr_idx) {
567            let type_str = self.format_type(spread_type);
568            let message = format_message(
569                diagnostic_messages::TYPE_MUST_HAVE_A_SYMBOL_ITERATOR_METHOD_THAT_RETURNS_AN_ITERATOR,
570                &[&type_str],
571            );
572            self.error(
573                start,
574                end.saturating_sub(start),
575                message,
576                diagnostic_codes::TYPE_MUST_HAVE_A_SYMBOL_ITERATOR_METHOD_THAT_RETURNS_AN_ITERATOR,
577            );
578        }
579        false
580    }
581
582    /// Check iterability for array destructuring patterns and emit TS2488 if not iterable.
583    ///
584    /// This function is called before assigning types to binding elements in array
585    /// destructuring to ensure that the source type is iterable.
586    ///
587    /// ## Parameters:
588    /// - `pattern_idx`: The array binding pattern node index
589    /// - `pattern_type`: The type being destructured
590    /// - `init_expr`: The initializer expression (used for error location)
591    ///
592    /// ## Validation:
593    /// - Checks if `pattern_type` is iterable
594    /// - Emits TS2488 if the type is not iterable
595    /// - Skips check for ANY, UNKNOWN, ERROR types (defer to other checks)
596    pub fn check_destructuring_iterability(
597        &mut self,
598        pattern_idx: NodeIndex,
599        pattern_type: TypeId,
600        init_expr: NodeIndex,
601    ) -> bool {
602        // Skip check for types that defer to other validation
603        if pattern_type == TypeId::ANY
604            || pattern_type == TypeId::UNKNOWN
605            || pattern_type == TypeId::ERROR
606        {
607            return true;
608        }
609
610        // TypeScript allows empty array destructuring patterns on any type (including null/undefined)
611        // Example: let [] = null; // No error
612        // Skip iterability check if the pattern is empty.
613        //
614        // Track whether this is an assignment target (`[a] = value`) vs a binding pattern
615        // (`let [a] = value`) so ES5-specific TS2461 can stay scoped to declarations.
616        let mut is_assignment_array_target = false;
617        if let Some(pattern_node) = self.ctx.arena.get(pattern_idx) {
618            is_assignment_array_target =
619                pattern_node.kind == tsz_parser::parser::syntax_kind_ext::ARRAY_LITERAL_EXPRESSION;
620            if let Some(binding_pattern) = self.ctx.arena.get_binding_pattern(pattern_node)
621                && binding_pattern.elements.nodes.is_empty()
622            {
623                return true;
624            }
625        }
626
627        // Resolve lazy types (type aliases) before checking iterability
628        let resolved_type = self.resolve_lazy_type(pattern_type);
629
630        // In array destructuring, TypeScript still reports TS2488 for `never`.
631        if resolved_type == TypeId::NEVER {
632            let error_idx = pattern_idx;
633            if let Some((start, end)) = self.get_node_span(error_idx) {
634                let type_str = self.format_type(pattern_type);
635                let message = format_message(
636                    diagnostic_messages::TYPE_MUST_HAVE_A_SYMBOL_ITERATOR_METHOD_THAT_RETURNS_AN_ITERATOR,
637                    &[&type_str],
638                );
639                self.error(
640                    start,
641                    end.saturating_sub(start),
642                    message,
643                    diagnostic_codes::TYPE_MUST_HAVE_A_SYMBOL_ITERATOR_METHOD_THAT_RETURNS_AN_ITERATOR,
644                );
645            }
646            return false;
647        }
648
649        // In ES5 mode (without downlevelIteration), array destructuring requires actual arrays.
650        // - Emit TS2802 if the type has Symbol.iterator (iterable but requires ES2015/downlevelIteration).
651        // - Emit TS2461 if the type is not an array type.
652        if self.ctx.compiler_options.target.is_es5() && !is_assignment_array_target {
653            // Nested binding patterns can be fed an over-widened union from positional
654            // destructuring inference (e.g. `[a, [b]] = [1, ["x"]]`). tsc does not report
655            // TS2461 for these cases.
656            if init_expr.is_none()
657                && tsz_solver::type_queries::get_union_members(self.ctx.types, resolved_type)
658                    .is_some_and(|members| {
659                        members
660                            .iter()
661                            .any(|&member| self.is_array_or_tuple_type(member))
662                    })
663            {
664                return true;
665            }
666            if self.is_array_or_tuple_type(resolved_type) {
667                return true;
668            }
669            // For destructuring diagnostics, anchor to the binding pattern.
670            let error_idx = pattern_idx;
671            if let Some((start, end)) = self.get_node_span(error_idx) {
672                let type_str = self.format_type(pattern_type);
673                // Check if the type has Symbol.iterator (iterable but not usable in ES5
674                // without downlevelIteration). These emit TS2802 instead of TS2461.
675                if self.is_iterable_type(resolved_type) {
676                    let message = format_message(
677                        diagnostic_messages::TYPE_CAN_ONLY_BE_ITERATED_THROUGH_WHEN_USING_THE_DOWNLEVELITERATION_FLAG_OR_WITH,
678                        &[&type_str],
679                    );
680                    self.error(
681                        start,
682                        end.saturating_sub(start),
683                        message,
684                        diagnostic_codes::TYPE_CAN_ONLY_BE_ITERATED_THROUGH_WHEN_USING_THE_DOWNLEVELITERATION_FLAG_OR_WITH,
685                    );
686                } else {
687                    let message = format_message(
688                        diagnostic_messages::TYPE_IS_NOT_AN_ARRAY_TYPE,
689                        &[&type_str],
690                    );
691                    self.error(
692                        start,
693                        end.saturating_sub(start),
694                        message,
695                        diagnostic_codes::TYPE_IS_NOT_AN_ARRAY_TYPE,
696                    );
697                }
698            }
699            return false;
700        }
701
702        // Check if the type is iterable (ES2015+)
703        if self.is_iterable_type(resolved_type) {
704            return true;
705        }
706
707        // TypeScript also allows array destructuring for "array-like" types
708        // (types with numeric index signatures) even without [Symbol.iterator]()
709        if self.has_numeric_index_signature(resolved_type) {
710            return true;
711        }
712
713        // Not iterable - emit TS2488
714
715        // For destructuring diagnostics, anchor to the binding pattern.
716        let error_idx = pattern_idx;
717
718        if let Some((start, end)) = self.get_node_span(error_idx) {
719            let type_str = self.format_type(pattern_type);
720            let message = format_message(
721                diagnostic_messages::TYPE_MUST_HAVE_A_SYMBOL_ITERATOR_METHOD_THAT_RETURNS_AN_ITERATOR,
722                &[&type_str],
723            );
724            self.error(
725                start,
726                end.saturating_sub(start),
727                message,
728                diagnostic_codes::TYPE_MUST_HAVE_A_SYMBOL_ITERATOR_METHOD_THAT_RETURNS_AN_ITERATOR,
729            );
730        }
731        false
732    }
733
734    // =========================================================================
735    // ES5 Type Classification Helpers
736    // =========================================================================
737
738    /// Check if a type is an array or tuple type (for ES5 destructuring).
739    fn is_array_or_tuple_type(&self, type_id: TypeId) -> bool {
740        if is_array_type(self.ctx.types, type_id) || is_tuple_type(self.ctx.types, type_id) {
741            return true;
742        }
743        // Check unions: all members must be array/tuple
744        if let Some(members) = union_members_for_type(self.ctx.types, type_id) {
745            return members
746                .iter()
747                .all(|&member| self.is_array_or_tuple_type(member));
748        }
749        false
750    }
751
752    /// Check if a type contains a string-like constituent (for ES5 for-of error discrimination).
753    ///
754    /// This mirrors TSC's `hasStringConstituent` check: when a union type contains a string
755    /// member alongside non-array types, the error changes from TS2495 to TS2461.
756    fn has_string_constituent(&self, type_id: TypeId) -> bool {
757        if type_id == TypeId::STRING || is_string_type(self.ctx.types, type_id) {
758            return true;
759        }
760        if is_string_literal_type(self.ctx.types, type_id) {
761            return true;
762        }
763        if let Some(members) = union_members_for_type(self.ctx.types, type_id) {
764            return members.iter().any(|&m| self.has_string_constituent(m));
765        }
766        false
767    }
768
769    /// Check if a type is an array, tuple, or string type (for ES5 for-of).
770    fn is_array_or_tuple_or_string(&self, type_id: TypeId) -> bool {
771        if type_id == TypeId::STRING || is_string_type(self.ctx.types, type_id) {
772            return true;
773        }
774        if is_array_type(self.ctx.types, type_id) || is_tuple_type(self.ctx.types, type_id) {
775            return true;
776        }
777        // String literals count as string types
778        if is_string_literal_type(self.ctx.types, type_id) {
779            return true;
780        }
781        // Check unions: all members must be array/tuple/string
782        if let Some(members) = union_members_for_type(self.ctx.types, type_id) {
783            return members
784                .iter()
785                .all(|&member| self.is_array_or_tuple_or_string(member));
786        }
787        false
788    }
789}