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}