Skip to main content

tsz_solver/evaluation/evaluate_rules/
mapped.rs

1//! Mapped type evaluation.
2//!
3//! Handles TypeScript's mapped types: `{ [K in keyof T]: T[K] }`
4//! Including homomorphic mapped types that preserve modifiers.
5
6use crate::instantiation::instantiate::{TypeSubstitution, instantiate_type};
7use crate::objects::{PropertyCollectionResult, collect_properties};
8use crate::relations::subtype::TypeResolver;
9use crate::types::Visibility;
10use crate::types::{
11    IndexSignature, IntrinsicKind, LiteralValue, MappedModifier, MappedType, ObjectFlags,
12    ObjectShape, PropertyInfo, TupleListId, TypeData, TypeId,
13};
14use rustc_hash::FxHashMap;
15use tsz_common::interner::Atom;
16
17use super::super::evaluate::TypeEvaluator;
18
19pub(crate) struct MappedKeys {
20    pub string_literals: Vec<Atom>,
21    pub has_string: bool,
22    pub has_number: bool,
23}
24
25impl<'a, R: TypeResolver> TypeEvaluator<'a, R> {
26    /// Helper for key remapping in mapped types.
27    /// Returns Ok(Some(remapped)) if remapping succeeded,
28    /// Ok(None) if the key should be filtered (remapped to never),
29    /// Err(()) if we can't process and should return the original mapped type.
30    #[tracing::instrument(level = "trace", skip(self), fields(
31        param_name = ?mapped.type_param.name,
32        key_type = key_type.0,
33        has_name_type = mapped.name_type.is_some(),
34    ))]
35    fn remap_key_type_for_mapped(
36        &mut self,
37        mapped: &MappedType,
38        key_type: TypeId,
39    ) -> Result<Option<TypeId>, ()> {
40        let Some(name_type) = mapped.name_type else {
41            return Ok(Some(key_type));
42        };
43
44        tracing::trace!(
45            key_type_lookup = ?self.interner().lookup(key_type),
46            name_type_lookup = ?self.interner().lookup(name_type),
47            "remap_key_type_for_mapped: before substitution"
48        );
49
50        let mut subst = TypeSubstitution::new();
51        subst.insert(mapped.type_param.name, key_type);
52        let remapped = instantiate_type(self.interner(), name_type, &subst);
53
54        tracing::trace!(
55            remapped_before_eval = remapped.0,
56            remapped_lookup = ?self.interner().lookup(remapped),
57            "remap_key_type_for_mapped: after substitution"
58        );
59
60        let remapped = self.evaluate(remapped);
61
62        tracing::trace!(
63            remapped_after_eval = remapped.0,
64            remapped_eval_lookup = ?self.interner().lookup(remapped),
65            is_never = remapped == TypeId::NEVER,
66            "remap_key_type_for_mapped: after evaluation"
67        );
68
69        if remapped == TypeId::NEVER {
70            return Ok(None);
71        }
72        Ok(Some(remapped))
73    }
74
75    /// Helper to compute modifiers for a mapped type property.
76    fn get_mapped_modifiers(
77        &mut self,
78        mapped: &MappedType,
79        is_homomorphic: bool,
80        source_object: Option<TypeId>,
81        key_name: Atom,
82    ) -> (bool, bool) {
83        // NOTE: This helper is now only used for index signatures.
84        // Direct property modifiers are handled via the memoized map in evaluate_mapped.
85        let source_mods = if let Some(source_obj) = source_object {
86            match collect_properties(source_obj, self.interner(), self.resolver()) {
87                PropertyCollectionResult::Properties { properties, .. } => properties
88                    .iter()
89                    .find(|p| p.name == key_name)
90                    .map_or((false, false), |p| (p.optional, p.readonly)),
91                _ => (false, false),
92            }
93        } else {
94            (false, false)
95        };
96
97        let optional = match mapped.optional_modifier {
98            Some(MappedModifier::Add) => true,
99            Some(MappedModifier::Remove) => false,
100            None => {
101                // For homomorphic types with no explicit modifier, preserve original
102                if is_homomorphic { source_mods.0 } else { false }
103            }
104        };
105
106        let readonly = match mapped.readonly_modifier {
107            Some(MappedModifier::Add) => true,
108            Some(MappedModifier::Remove) => false,
109            None => {
110                // For homomorphic types with no explicit modifier, preserve original
111                if is_homomorphic { source_mods.1 } else { false }
112            }
113        };
114
115        (optional, readonly)
116    }
117
118    /// Evaluate a mapped type: { [K in Keys]: Template }
119    ///
120    /// Algorithm:
121    /// 1. Extract the constraint (Keys) - this defines what keys to iterate over
122    /// 2. For each key K in the constraint:
123    ///    - Substitute K into the template type
124    ///    - Apply readonly/optional modifiers
125    /// 3. Construct a new object type with the resulting properties
126    pub fn evaluate_mapped(&mut self, mapped: &MappedType) -> TypeId {
127        // TODO: Array/Tuple Preservation for Homomorphic Mapped Types
128        // If source_object is an Array or Tuple, we should construct a Mapped Array/Tuple
129        // instead of degrading to a plain Object. This is required to preserve
130        // Array.prototype methods (push, pop, map) and tuple-specific behavior.
131        // Example: type Boxed<T> = { [K in keyof T]: Box<T[K]> }
132        //   Boxed<[number, string]> should be [Box<number>, Box<string>] (Tuple)
133        //   Boxed<number[]> should be Box<number>[] (Array)
134        // Current implementation degrades both to plain Objects.
135
136        // Check if depth was already exceeded
137        if self.is_depth_exceeded() {
138            return TypeId::ERROR;
139        }
140
141        // Get the constraint - this tells us what keys to iterate over
142        let constraint = mapped.constraint;
143
144        // SPECIAL CASE: Don't expand mapped types over type parameters.
145        // When the constraint is `keyof T` where T is a type parameter, we should
146        // keep the mapped type deferred. Even though we might be able to evaluate
147        // `keyof T` to concrete keys (via T's constraint), the template instantiation
148        // would fail because T[key] can't be resolved for a type parameter.
149        //
150        // This is critical for patterns like:
151        //   function f<T extends any[]>(a: Boxified<T>) { a.pop(); }
152        // where Boxified<T> = { [P in keyof T]: Box<T[P]> }
153        //
154        // If we expand this, T["pop"] becomes ERROR. We need to keep it deferred
155        // and handle property access on the deferred mapped type specially.
156        if self.is_mapped_type_over_type_parameter(mapped) {
157            tracing::trace!(
158                constraint = ?self.interner().lookup(constraint),
159                "evaluate_mapped: DEFERRED - mapped type over type parameter"
160            );
161            return self.interner().mapped(mapped.clone());
162        }
163
164        // Evaluate the constraint to get concrete keys
165        let keys = self.evaluate_keyof_or_constraint(constraint);
166
167        // If we can't determine concrete keys, keep it as a mapped type (deferred)
168        let key_set = match self.extract_mapped_keys(keys) {
169            Some(keys) => keys,
170            None => {
171                tracing::trace!(
172                    keys_lookup = ?self.interner().lookup(keys),
173                    "evaluate_mapped: DEFERRED - could not extract concrete keys"
174                );
175                return self.interner().mapped(mapped.clone());
176            }
177        };
178
179        // Limit number of keys to prevent OOM with large mapped types.
180        // WASM environments have limited memory, but 100 is too restrictive for
181        // real-world code (large SDKs, generated API types often have 150-250 keys).
182        // 250 covers ~99% of real-world use cases while remaining safe for WASM.
183        #[cfg(target_arch = "wasm32")]
184        const MAX_MAPPED_KEYS: usize = 250;
185        #[cfg(not(target_arch = "wasm32"))]
186        const MAX_MAPPED_KEYS: usize = 500;
187        if key_set.string_literals.len() > MAX_MAPPED_KEYS {
188            self.mark_depth_exceeded();
189            return TypeId::ERROR;
190        }
191
192        // Check if this is a homomorphic mapped type (template is T[K] indexed access)
193        // In this case, we should preserve the original property modifiers
194        let is_homomorphic = self.is_homomorphic_mapped_type(mapped);
195
196        // Extract source object type from the constraint if it is `keyof T`
197        // This is needed for homomorphic mapped types and for preserving Array/Tuple
198        // structure in mapped types over arrays/tuples (even non-homomorphic ones).
199        let source_object = self.extract_source_from_keyof(mapped.constraint);
200
201        // PERF: Memoize source properties into a hash map for O(1) lookup during the key loop.
202        // This avoids repeated O(N) collect_properties calls inside the loop.
203        let mut source_prop_map = FxHashMap::default();
204        if let Some(source) = source_object {
205            match collect_properties(source, self.interner(), self.resolver()) {
206                PropertyCollectionResult::Properties { properties, .. } => {
207                    for prop in properties {
208                        source_prop_map
209                            .insert(prop.name, (prop.optional, prop.readonly, prop.type_id));
210                    }
211                }
212                PropertyCollectionResult::Any | PropertyCollectionResult::NonObject => {
213                    // Any type properties are treated as (false, false, ANY)
214                }
215            }
216        }
217
218        // HOMOMORPHIC ARRAY/TUPLE PRESERVATION
219        // If source_object is an Array or Tuple, preserve the structure instead of
220        // degrading to a plain Object. This preserves Array methods (push, pop, map)
221        // and tuple-specific behavior.
222        //
223        // Example: type Partial<T> = { [P in keyof T]?: T[P] }
224        //   Partial<[number, string]> should be [number?, string?] (Tuple)
225        //   Partial<number[]> should be (number | undefined)[] (Array)
226        //
227        // CRITICAL: Only preserve if there's NO name remapping (as clause).
228        // Name remapping breaks homomorphism and degrades to plain object.
229        if let Some(source) = source_object {
230            // Name remapping breaks homomorphism - don't preserve structure
231            if mapped.name_type.is_none() {
232                // Resolve the source to check if it's an Array or Tuple
233                // Use evaluate() to resolve Lazy types (interfaces/classes)
234                let resolved = self.evaluate(source);
235
236                match self.interner().lookup(resolved) {
237                    // Array type: map the element type
238                    Some(TypeData::Array(element_type)) => {
239                        return self.evaluate_mapped_array(mapped, element_type);
240                    }
241
242                    // Tuple type: map each element
243                    Some(TypeData::Tuple(tuple_id)) => {
244                        return self.evaluate_mapped_tuple(mapped, tuple_id);
245                    }
246
247                    // ReadonlyArray: map the element type and preserve readonly
248                    Some(TypeData::ObjectWithIndex(shape_id)) => {
249                        // Check if this is a ReadonlyArray (has readonly numeric index)
250                        // Note: We DON'T check properties.is_empty() because ReadonlyArray<T>
251                        // has methods like length, map, filter, etc. We only care about the index signature.
252                        let shape = self.interner().object_shape(shape_id);
253                        let has_readonly_index = shape
254                            .number_index
255                            .as_ref()
256                            .is_some_and(|idx| idx.readonly && idx.key_type == TypeId::NUMBER);
257
258                        if has_readonly_index {
259                            // This is ReadonlyArray<T> - map element type
260                            // Extract the element type from the number index signature
261                            if let Some(index) = &shape.number_index {
262                                return self.evaluate_mapped_array_with_readonly(
263                                    mapped,
264                                    index.value_type,
265                                    true,
266                                );
267                            }
268                        }
269                    }
270
271                    _ => {}
272                }
273            }
274        }
275
276        // Build the resulting object properties
277        let mut properties = Vec::new();
278
279        for key_name in key_set.string_literals {
280            // Check if depth was exceeded during previous iterations
281            if self.is_depth_exceeded() {
282                return TypeId::ERROR;
283            }
284
285            // Create substitution: type_param.name -> literal key type
286            // Use canonical constructor for O(1) equality
287            let key_literal = self.interner().literal_string_atom(key_name);
288            let remapped = match self.remap_key_type_for_mapped(mapped, key_literal) {
289                Ok(Some(remapped)) => remapped,
290                Ok(None) => continue,
291                Err(()) => return self.interner().mapped(mapped.clone()),
292            };
293            // Extract property name(s) from remapped key.
294            // Handle unions: `as \`${K}1\` | \`${K}2\`` produces multiple properties per key.
295            let remapped_names: Vec<Atom> =
296                if let Some(name) = crate::visitor::literal_string(self.interner(), remapped) {
297                    vec![name]
298                } else if let Some(TypeData::Union(list_id)) = self.interner().lookup(remapped) {
299                    let members = self.interner().type_list(list_id);
300                    let names: Vec<Atom> = members
301                        .iter()
302                        .filter_map(|&m| crate::visitor::literal_string(self.interner(), m))
303                        .collect();
304                    if names.is_empty() {
305                        return self.interner().mapped(mapped.clone());
306                    }
307                    names
308                } else {
309                    return self.interner().mapped(mapped.clone());
310                };
311
312            let mut subst = TypeSubstitution::new();
313            subst.insert(mapped.type_param.name, key_literal);
314
315            // Substitute into the template
316            let mut property_type =
317                self.evaluate(instantiate_type(self.interner(), mapped.template, &subst));
318
319            // Check if evaluation hit depth limit
320            if property_type == TypeId::ERROR && self.is_depth_exceeded() {
321                return TypeId::ERROR;
322            }
323
324            // Get modifiers for this specific key (preserves homomorphic behavior)
325            // Use memoized source property info for O(1) lookup.
326            let source_info = source_prop_map.get(&key_name);
327            let (source_optional, source_readonly) =
328                source_info.map_or((false, false), |(opt, ro, _)| (*opt, *ro));
329
330            let optional = match mapped.optional_modifier {
331                Some(MappedModifier::Add) => true,
332                Some(MappedModifier::Remove) => false,
333                None => {
334                    if is_homomorphic {
335                        source_optional
336                    } else {
337                        false
338                    }
339                }
340            };
341
342            let readonly = match mapped.readonly_modifier {
343                Some(MappedModifier::Add) => true,
344                Some(MappedModifier::Remove) => false,
345                None => {
346                    if is_homomorphic {
347                        source_readonly
348                    } else {
349                        false
350                    }
351                }
352            };
353
354            // TypeScript homomorphic mapped type behavior: when `-?` removes optionality
355            // from an optional source property, the property type should be the DECLARED
356            // type (without the `| undefined` that IndexedAccess adds for optional properties).
357            if !optional
358                && matches!(mapped.optional_modifier, Some(MappedModifier::Remove))
359                && is_homomorphic
360                && source_optional
361            {
362                // Use the memoized declared type directly
363                if let Some((_, _, declared_type)) = source_info {
364                    property_type = *declared_type;
365                }
366            }
367
368            for remapped_name in remapped_names {
369                properties.push(PropertyInfo {
370                    name: remapped_name,
371                    type_id: property_type,
372                    write_type: property_type,
373                    optional,
374                    readonly,
375                    is_method: false,
376                    visibility: Visibility::Public,
377                    parent_id: None,
378                });
379            }
380        }
381
382        let string_index = if key_set.has_string {
383            match self.remap_key_type_for_mapped(mapped, TypeId::STRING) {
384                Ok(Some(remapped)) => {
385                    if remapped != TypeId::STRING {
386                        return self.interner().mapped(mapped.clone());
387                    }
388                    let key_type = TypeId::STRING;
389                    let mut subst = TypeSubstitution::new();
390                    subst.insert(mapped.type_param.name, key_type);
391                    let mut value_type =
392                        self.evaluate(instantiate_type(self.interner(), mapped.template, &subst));
393
394                    // Get modifiers for string index
395                    let empty_atom = self.interner().intern_string("");
396                    let (idx_optional, idx_readonly) = self.get_mapped_modifiers(
397                        mapped,
398                        is_homomorphic,
399                        source_object,
400                        empty_atom,
401                    );
402                    if idx_optional {
403                        value_type = self.interner().union2(value_type, TypeId::UNDEFINED);
404                    }
405                    Some(IndexSignature {
406                        key_type,
407                        value_type,
408                        readonly: idx_readonly,
409                    })
410                }
411                Ok(None) => None,
412                Err(()) => return self.interner().mapped(mapped.clone()),
413            }
414        } else {
415            None
416        };
417
418        let number_index = if key_set.has_number {
419            match self.remap_key_type_for_mapped(mapped, TypeId::NUMBER) {
420                Ok(Some(remapped)) => {
421                    if remapped != TypeId::NUMBER {
422                        return self.interner().mapped(mapped.clone());
423                    }
424                    let key_type = TypeId::NUMBER;
425                    let mut subst = TypeSubstitution::new();
426                    subst.insert(mapped.type_param.name, key_type);
427                    let mut value_type =
428                        self.evaluate(instantiate_type(self.interner(), mapped.template, &subst));
429
430                    // Get modifiers for number index
431                    let empty_atom = self.interner().intern_string("");
432                    let (idx_optional, idx_readonly) = self.get_mapped_modifiers(
433                        mapped,
434                        is_homomorphic,
435                        source_object,
436                        empty_atom,
437                    );
438                    if idx_optional {
439                        value_type = self.interner().union2(value_type, TypeId::UNDEFINED);
440                    }
441                    Some(IndexSignature {
442                        key_type,
443                        value_type,
444                        readonly: idx_readonly,
445                    })
446                }
447                Ok(None) => None,
448                Err(()) => return self.interner().mapped(mapped.clone()),
449            }
450        } else {
451            None
452        };
453
454        if string_index.is_some() || number_index.is_some() {
455            self.interner().object_with_index(ObjectShape {
456                flags: ObjectFlags::empty(),
457                properties,
458                string_index,
459                number_index,
460                symbol: None,
461            })
462        } else {
463            self.interner().object(properties)
464        }
465    }
466
467    /// Check if a mapped type's constraint is `keyof T` where T is a type parameter.
468    ///
469    /// When this is true, we should not expand the mapped type because the template
470    /// instantiation would fail (T[key] can't be resolved for a type parameter).
471    fn is_mapped_type_over_type_parameter(&self, mapped: &MappedType) -> bool {
472        // Check if the constraint is `keyof T`
473        let Some(TypeData::KeyOf(source)) = self.interner().lookup(mapped.constraint) else {
474            return false;
475        };
476
477        // Check if the source is a type parameter
478        matches!(
479            self.interner().lookup(source),
480            Some(TypeData::TypeParameter(_) | TypeData::Infer(_))
481        )
482    }
483
484    /// Evaluate a keyof or constraint type for mapped type iteration.
485    fn evaluate_keyof_or_constraint(&mut self, constraint: TypeId) -> TypeId {
486        if let Some(TypeData::Conditional(cond_id)) = self.interner().lookup(constraint) {
487            let cond = self.interner().conditional_type(cond_id);
488            return self.evaluate_conditional(cond.as_ref());
489        }
490
491        // If constraint is already a union of literals, return it
492        if let Some(TypeData::Union(_)) = self.interner().lookup(constraint) {
493            return constraint;
494        }
495
496        // If constraint is a literal, return it
497        if let Some(TypeData::Literal(LiteralValue::String(_))) = self.interner().lookup(constraint)
498        {
499            return constraint;
500        }
501
502        // If constraint is KeyOf, evaluate it
503        if let Some(TypeData::KeyOf(operand)) = self.interner().lookup(constraint) {
504            return self.evaluate_keyof(operand);
505        }
506
507        // Evaluate the constraint to resolve type aliases (Lazy), Applications, etc.
508        // For example, `type Keys = "a" | "b"; { [P in Keys]: T }` has a Lazy(DefId)
509        // constraint that must be evaluated to get the concrete union `"a" | "b"`.
510        let evaluated = self.evaluate(constraint);
511        if evaluated != constraint {
512            return self.evaluate_keyof_or_constraint(evaluated);
513        }
514
515        // Otherwise return as-is
516        constraint
517    }
518
519    /// Extract mapped keys from a type (for mapped type iteration).
520    fn extract_mapped_keys(&self, type_id: TypeId) -> Option<MappedKeys> {
521        let key = self.interner().lookup(type_id)?;
522
523        let mut keys = MappedKeys {
524            string_literals: Vec::new(),
525            has_string: false,
526            has_number: false,
527        };
528
529        match key {
530            // NEW: Handle KeyOf types directly if evaluate_keyof deferred
531            // This fixes Bug #1: Key Remapping with conditionals
532            TypeData::KeyOf(operand) => {
533                tracing::trace!(
534                    operand = operand.0,
535                    operand_lookup = ?self.interner().lookup(operand),
536                    "extract_mapped_keys: handling KeyOf type"
537                );
538                // NORTH STAR: Use collect_properties to extract keys from KeyOf operand.
539                // This handles interfaces, classes, intersections, and type parameters.
540                let prop_result = collect_properties(operand, self.interner(), self.resolver());
541                tracing::trace!(
542                    operand = operand.0,
543                    prop_result = ?std::mem::discriminant(&prop_result),
544                    "extract_mapped_keys: collect_properties result"
545                );
546                match prop_result {
547                    PropertyCollectionResult::Properties {
548                        properties,
549                        string_index,
550                        number_index,
551                    } => {
552                        for prop in properties {
553                            keys.string_literals.push(prop.name);
554                        }
555                        keys.has_string = string_index.is_some();
556                        keys.has_number = number_index.is_some();
557                        tracing::trace!(
558                            string_literals = ?keys.string_literals,
559                            has_string = keys.has_string,
560                            has_number = keys.has_number,
561                            "extract_mapped_keys: extracted keys from KeyOf"
562                        );
563                        Some(keys)
564                    }
565                    PropertyCollectionResult::Any => {
566                        keys.has_string = true;
567                        keys.has_number = true;
568                        tracing::trace!("extract_mapped_keys: KeyOf is Any type");
569                        Some(keys)
570                    }
571                    PropertyCollectionResult::NonObject => {
572                        tracing::trace!("extract_mapped_keys: KeyOf operand is not an object");
573                        None
574                    }
575                }
576            }
577            TypeData::Literal(LiteralValue::String(s)) => {
578                keys.string_literals.push(s);
579                Some(keys)
580            }
581            TypeData::Union(members) => {
582                let members = self.interner().type_list(members);
583                for &member in members.iter() {
584                    if member == TypeId::STRING {
585                        keys.has_string = true;
586                        continue;
587                    }
588                    if member == TypeId::NUMBER {
589                        keys.has_number = true;
590                        continue;
591                    }
592                    if member == TypeId::SYMBOL {
593                        // We don't model symbol index signatures yet; ignore symbol keys.
594                        continue;
595                    }
596                    // Use visitor helper for data extraction (North Star Rule 3)
597                    if let Some(s) = crate::visitor::literal_string(self.interner(), member) {
598                        keys.string_literals.push(s);
599                    } else if let Some(n) = crate::visitor::literal_number(self.interner(), member)
600                    {
601                        // Numeric literals become string property names (e.g., 0 → "0").
602                        // This handles enum member values like `enum E { A = 0 }`.
603                        let s = self.interner().intern_string(
604                            &crate::relations::subtype_rules::literals::format_number_for_template(
605                                n.0,
606                            ),
607                        );
608                        keys.string_literals.push(s);
609                    } else {
610                        // Non-literal in union - can't fully evaluate
611                        return None;
612                    }
613                }
614                if !keys.has_string && !keys.has_number && keys.string_literals.is_empty() {
615                    // Only symbol keys (or nothing) - defer until we support symbol indices.
616                    return None;
617                }
618                Some(keys)
619            }
620            TypeData::Intrinsic(IntrinsicKind::String) => {
621                keys.has_string = true;
622                Some(keys)
623            }
624            TypeData::Intrinsic(IntrinsicKind::Number) => {
625                keys.has_number = true;
626                Some(keys)
627            }
628            TypeData::Intrinsic(IntrinsicKind::Never) => {
629                // Mapped over `never` yields an empty object.
630                Some(keys)
631            }
632            TypeData::Enum(_def_id, members) => {
633                // Enum used as mapped type constraint: extract keys from member union.
634                // For `enum E { A, B }`, members is the union `0 | 1`, and the keys
635                // are the enum values. Recursively extract from the members type.
636                self.extract_mapped_keys(members)
637            }
638            // Can't extract literals from other types
639            _ => None,
640        }
641    }
642
643    /// Check if a mapped type is homomorphic (template is T[K] indexed access).
644    /// Homomorphic mapped types preserve modifiers from the source type.
645    ///
646    /// A mapped type is homomorphic if:
647    /// 1. The constraint is `keyof T` for some type T
648    /// 2. The template is `T[K]` where T is the same type and K is the iteration parameter
649    ///
650    /// Also handles the post-instantiation case where the `keyof T` constraint was
651    /// eagerly evaluated to a union of string literals during `instantiate_type`.
652    /// In that case, we verify that `template = obj[P]` and `keyof obj == constraint`.
653    fn is_homomorphic_mapped_type(&mut self, mapped: &MappedType) -> bool {
654        // Method 1: Constraint is explicitly `keyof T` (pre-evaluation form)
655        if let Some(source_from_constraint) = self.extract_source_from_keyof(mapped.constraint) {
656            // Check if template is an IndexAccess type T[K]
657            return match self.interner().lookup(mapped.template) {
658                Some(TypeData::IndexAccess(obj, idx)) => {
659                    if obj != source_from_constraint {
660                        return false;
661                    }
662                    match self.interner().lookup(idx) {
663                        Some(TypeData::TypeParameter(param)) => {
664                            param.name == mapped.type_param.name
665                        }
666                        _ => false,
667                    }
668                }
669                _ => false,
670            };
671        }
672
673        // Method 2: Post-instantiation form where `keyof T` was eagerly evaluated
674        // to a union of string literals. The template still has the original structure
675        // `T[P]` with the concrete object. Verify by computing `keyof obj` and
676        // comparing with the constraint.
677        // Key remapping (`as` clause / name_type) breaks homomorphism.
678        if mapped.name_type.is_none()
679            && let Some(TypeData::IndexAccess(obj, idx)) = self.interner().lookup(mapped.template)
680            && let Some(TypeData::TypeParameter(param)) = self.interner().lookup(idx)
681            && param.name == mapped.type_param.name
682        {
683            // Don't match if obj is still a type parameter (not yet instantiated)
684            if matches!(
685                self.interner().lookup(obj),
686                Some(TypeData::TypeParameter(_))
687            ) {
688                return false;
689            }
690            // Verify: the constraint is exactly the keys of obj
691            let expected_keys = self.evaluate_keyof(obj);
692            return expected_keys == mapped.constraint;
693        }
694
695        false
696    }
697
698    /// Extract the source type T from a `keyof T` constraint.
699    /// Handles aliased constraints like `type Keys<T> = keyof T`.
700    fn extract_source_from_keyof(&mut self, constraint: TypeId) -> Option<TypeId> {
701        match self.interner().lookup(constraint) {
702            Some(TypeData::KeyOf(source)) => Some(source),
703            // Handle aliased constraints (Application)
704            Some(TypeData::Application(_)) => {
705                // Evaluate to resolve the alias
706                let evaluated = self.evaluate(constraint);
707                // Recursively check the evaluated type
708                if evaluated != constraint {
709                    self.extract_source_from_keyof(evaluated)
710                } else {
711                    None
712                }
713            }
714            _ => None,
715        }
716    }
717
718    /// Evaluate a homomorphic mapped type over an Array type.
719    ///
720    /// For example: `type Partial<T> = { [P in keyof T]?: T[P] }`
721    ///   `Partial<number[]>` should produce `(number | undefined)[]`
722    ///
723    /// We instantiate the template with `K = number` to get the mapped element type.
724    fn evaluate_mapped_array(&mut self, mapped: &MappedType, _element_type: TypeId) -> TypeId {
725        // Create substitution: type_param.name -> number
726        let mut subst = TypeSubstitution::new();
727        subst.insert(mapped.type_param.name, TypeId::NUMBER);
728
729        // Substitute into the template to get the mapped element type
730        let mut mapped_element =
731            self.evaluate(instantiate_type(self.interner(), mapped.template, &subst));
732
733        // CRITICAL: Handle optional modifier (Partial<T[]> case)
734        // TypeScript adds undefined to the element type when ? modifier is present
735        if matches!(mapped.optional_modifier, Some(MappedModifier::Add)) {
736            mapped_element = self.interner().union2(mapped_element, TypeId::UNDEFINED);
737        }
738
739        // Check if readonly modifier should be applied
740        let is_readonly = matches!(mapped.readonly_modifier, Some(MappedModifier::Add));
741
742        // Create the new array type
743        if is_readonly {
744            // Wrap the array type in ReadonlyType to get readonly semantics
745            let array_type = self.interner().array(mapped_element);
746            self.interner().readonly_type(array_type)
747        } else {
748            self.interner().array(mapped_element)
749        }
750    }
751
752    /// Evaluate a homomorphic mapped type over an Array type with explicit readonly flag.
753    ///
754    /// Used for `ReadonlyArray`<T> to preserve readonly semantics.
755    fn evaluate_mapped_array_with_readonly(
756        &mut self,
757        mapped: &MappedType,
758        _element_type: TypeId,
759        is_readonly: bool,
760    ) -> TypeId {
761        // Create substitution: type_param.name -> number
762        let mut subst = TypeSubstitution::new();
763        subst.insert(mapped.type_param.name, TypeId::NUMBER);
764
765        // Substitute into the template to get the mapped element type
766        let mut mapped_element =
767            self.evaluate(instantiate_type(self.interner(), mapped.template, &subst));
768
769        // CRITICAL: Handle optional modifier (Partial<T[]> case)
770        if matches!(mapped.optional_modifier, Some(MappedModifier::Add)) {
771            mapped_element = self.interner().union2(mapped_element, TypeId::UNDEFINED);
772        }
773
774        // Apply readonly modifier if present
775        let final_readonly = match mapped.readonly_modifier {
776            Some(MappedModifier::Add) => true,
777            Some(MappedModifier::Remove) => false,
778            None => is_readonly, // Preserve original readonly status
779        };
780
781        if final_readonly {
782            // Wrap the array type in ReadonlyType to get readonly semantics
783            let array_type = self.interner().array(mapped_element);
784            self.interner().readonly_type(array_type)
785        } else {
786            self.interner().array(mapped_element)
787        }
788    }
789
790    /// Evaluate a homomorphic mapped type over a Tuple type.
791    ///
792    /// For example: `type Partial<T> = { [P in keyof T]?: T[P] }`
793    ///   `Partial<[number, string]>` should produce `[number?, string?]`
794    ///
795    /// We instantiate the template with `K = 0, 1, 2...` for each tuple element.
796    /// This preserves tuple structure including optional and rest elements.
797    fn evaluate_mapped_tuple(&mut self, mapped: &MappedType, tuple_id: TupleListId) -> TypeId {
798        use crate::types::TupleElement;
799
800        let tuple_elements = self.interner().tuple_list(tuple_id);
801        let mut mapped_elements = Vec::new();
802
803        for (i, elem) in tuple_elements.iter().enumerate() {
804            // CRITICAL: Handle rest elements specially
805            // For rest elements (...T[]), we cannot use index substitution.
806            // We must map the array type itself.
807            if elem.rest {
808                // Rest elements like ...number[] need to be mapped as arrays
809                // Check if the rest type is an Array
810                let rest_type = elem.type_id;
811                let mapped_rest_type = match self.interner().lookup(rest_type) {
812                    Some(TypeData::Array(inner_elem)) => {
813                        // Map the inner array element
814                        // Reuse the array mapping logic
815                        self.evaluate_mapped_array(mapped, inner_elem)
816                    }
817                    Some(TypeData::Tuple(inner_tuple_id)) => {
818                        // Nested tuple in rest - recurse
819                        self.evaluate_mapped_tuple(mapped, inner_tuple_id)
820                    }
821                    _ => {
822                        // Fallback: try index substitution (may not work correctly)
823                        let index_type = self.interner().literal_number(i as f64);
824                        let mut subst = TypeSubstitution::new();
825                        subst.insert(mapped.type_param.name, index_type);
826                        self.evaluate(instantiate_type(self.interner(), mapped.template, &subst))
827                    }
828                };
829
830                // Handle optional modifier for rest elements
831                let final_rest_type =
832                    if matches!(mapped.optional_modifier, Some(MappedModifier::Add)) {
833                        self.interner().union2(mapped_rest_type, TypeId::UNDEFINED)
834                    } else {
835                        mapped_rest_type
836                    };
837
838                mapped_elements.push(TupleElement {
839                    type_id: final_rest_type,
840                    name: elem.name,
841                    optional: elem.optional,
842                    rest: true,
843                });
844                continue;
845            }
846
847            // Non-rest elements: use index substitution
848            // Create a literal number type for this tuple position
849            let index_type = self.interner().literal_number(i as f64);
850
851            // Create substitution: type_param.name -> literal number
852            let mut subst = TypeSubstitution::new();
853            subst.insert(mapped.type_param.name, index_type);
854
855            // Substitute into the template to get the mapped element type
856            let mapped_type =
857                self.evaluate(instantiate_type(self.interner(), mapped.template, &subst));
858
859            // Get the modifiers for this element
860            // Note: readonly is currently unused for tuple elements, but we preserve the logic
861            // in case TypeScript adds readonly tuple element support in the future
862            // CRITICAL: Handle optional and readonly modifiers independently
863            let optional = match mapped.optional_modifier {
864                Some(MappedModifier::Add) => true,
865                Some(MappedModifier::Remove) => false,
866                None => elem.optional, // Preserve original optional
867            };
868            // Note: readonly modifier is intentionally ignored for tuple elements,
869            // as TypeScript doesn't support readonly on individual tuple elements.
870
871            mapped_elements.push(TupleElement {
872                type_id: mapped_type,
873                name: elem.name,
874                optional,
875                rest: elem.rest,
876            });
877        }
878
879        self.interner().tuple(mapped_elements)
880    }
881}