tsz_solver/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::instantiate::{TypeSubstitution, instantiate_type};
7use crate::objects::{PropertyCollectionResult, collect_properties};
8use crate::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::subtype_rules::literals::format_number_for_template(n.0),
605 );
606 keys.string_literals.push(s);
607 } else {
608 // Non-literal in union - can't fully evaluate
609 return None;
610 }
611 }
612 if !keys.has_string && !keys.has_number && keys.string_literals.is_empty() {
613 // Only symbol keys (or nothing) - defer until we support symbol indices.
614 return None;
615 }
616 Some(keys)
617 }
618 TypeData::Intrinsic(IntrinsicKind::String) => {
619 keys.has_string = true;
620 Some(keys)
621 }
622 TypeData::Intrinsic(IntrinsicKind::Number) => {
623 keys.has_number = true;
624 Some(keys)
625 }
626 TypeData::Intrinsic(IntrinsicKind::Never) => {
627 // Mapped over `never` yields an empty object.
628 Some(keys)
629 }
630 TypeData::Enum(_def_id, members) => {
631 // Enum used as mapped type constraint: extract keys from member union.
632 // For `enum E { A, B }`, members is the union `0 | 1`, and the keys
633 // are the enum values. Recursively extract from the members type.
634 self.extract_mapped_keys(members)
635 }
636 // Can't extract literals from other types
637 _ => None,
638 }
639 }
640
641 /// Check if a mapped type is homomorphic (template is T[K] indexed access).
642 /// Homomorphic mapped types preserve modifiers from the source type.
643 ///
644 /// A mapped type is homomorphic if:
645 /// 1. The constraint is `keyof T` for some type T
646 /// 2. The template is `T[K]` where T is the same type and K is the iteration parameter
647 ///
648 /// Also handles the post-instantiation case where the `keyof T` constraint was
649 /// eagerly evaluated to a union of string literals during `instantiate_type`.
650 /// In that case, we verify that `template = obj[P]` and `keyof obj == constraint`.
651 fn is_homomorphic_mapped_type(&mut self, mapped: &MappedType) -> bool {
652 // Method 1: Constraint is explicitly `keyof T` (pre-evaluation form)
653 if let Some(source_from_constraint) = self.extract_source_from_keyof(mapped.constraint) {
654 // Check if template is an IndexAccess type T[K]
655 return match self.interner().lookup(mapped.template) {
656 Some(TypeData::IndexAccess(obj, idx)) => {
657 if obj != source_from_constraint {
658 return false;
659 }
660 match self.interner().lookup(idx) {
661 Some(TypeData::TypeParameter(param)) => {
662 param.name == mapped.type_param.name
663 }
664 _ => false,
665 }
666 }
667 _ => false,
668 };
669 }
670
671 // Method 2: Post-instantiation form where `keyof T` was eagerly evaluated
672 // to a union of string literals. The template still has the original structure
673 // `T[P]` with the concrete object. Verify by computing `keyof obj` and
674 // comparing with the constraint.
675 // Key remapping (`as` clause / name_type) breaks homomorphism.
676 if mapped.name_type.is_none()
677 && let Some(TypeData::IndexAccess(obj, idx)) = self.interner().lookup(mapped.template)
678 && let Some(TypeData::TypeParameter(param)) = self.interner().lookup(idx)
679 && param.name == mapped.type_param.name
680 {
681 // Don't match if obj is still a type parameter (not yet instantiated)
682 if matches!(
683 self.interner().lookup(obj),
684 Some(TypeData::TypeParameter(_))
685 ) {
686 return false;
687 }
688 // Verify: the constraint is exactly the keys of obj
689 let expected_keys = self.evaluate_keyof(obj);
690 return expected_keys == mapped.constraint;
691 }
692
693 false
694 }
695
696 /// Extract the source type T from a `keyof T` constraint.
697 /// Handles aliased constraints like `type Keys<T> = keyof T`.
698 fn extract_source_from_keyof(&mut self, constraint: TypeId) -> Option<TypeId> {
699 match self.interner().lookup(constraint) {
700 Some(TypeData::KeyOf(source)) => Some(source),
701 // Handle aliased constraints (Application)
702 Some(TypeData::Application(_)) => {
703 // Evaluate to resolve the alias
704 let evaluated = self.evaluate(constraint);
705 // Recursively check the evaluated type
706 if evaluated != constraint {
707 self.extract_source_from_keyof(evaluated)
708 } else {
709 None
710 }
711 }
712 _ => None,
713 }
714 }
715
716 /// Evaluate a homomorphic mapped type over an Array type.
717 ///
718 /// For example: `type Partial<T> = { [P in keyof T]?: T[P] }`
719 /// `Partial<number[]>` should produce `(number | undefined)[]`
720 ///
721 /// We instantiate the template with `K = number` to get the mapped element type.
722 fn evaluate_mapped_array(&mut self, mapped: &MappedType, _element_type: TypeId) -> TypeId {
723 // Create substitution: type_param.name -> number
724 let mut subst = TypeSubstitution::new();
725 subst.insert(mapped.type_param.name, TypeId::NUMBER);
726
727 // Substitute into the template to get the mapped element type
728 let mut mapped_element =
729 self.evaluate(instantiate_type(self.interner(), mapped.template, &subst));
730
731 // CRITICAL: Handle optional modifier (Partial<T[]> case)
732 // TypeScript adds undefined to the element type when ? modifier is present
733 if matches!(mapped.optional_modifier, Some(MappedModifier::Add)) {
734 mapped_element = self.interner().union2(mapped_element, TypeId::UNDEFINED);
735 }
736
737 // Check if readonly modifier should be applied
738 let is_readonly = matches!(mapped.readonly_modifier, Some(MappedModifier::Add));
739
740 // Create the new array type
741 if is_readonly {
742 // Wrap the array type in ReadonlyType to get readonly semantics
743 let array_type = self.interner().array(mapped_element);
744 self.interner().readonly_type(array_type)
745 } else {
746 self.interner().array(mapped_element)
747 }
748 }
749
750 /// Evaluate a homomorphic mapped type over an Array type with explicit readonly flag.
751 ///
752 /// Used for `ReadonlyArray`<T> to preserve readonly semantics.
753 fn evaluate_mapped_array_with_readonly(
754 &mut self,
755 mapped: &MappedType,
756 _element_type: TypeId,
757 is_readonly: bool,
758 ) -> TypeId {
759 // Create substitution: type_param.name -> number
760 let mut subst = TypeSubstitution::new();
761 subst.insert(mapped.type_param.name, TypeId::NUMBER);
762
763 // Substitute into the template to get the mapped element type
764 let mut mapped_element =
765 self.evaluate(instantiate_type(self.interner(), mapped.template, &subst));
766
767 // CRITICAL: Handle optional modifier (Partial<T[]> case)
768 if matches!(mapped.optional_modifier, Some(MappedModifier::Add)) {
769 mapped_element = self.interner().union2(mapped_element, TypeId::UNDEFINED);
770 }
771
772 // Apply readonly modifier if present
773 let final_readonly = match mapped.readonly_modifier {
774 Some(MappedModifier::Add) => true,
775 Some(MappedModifier::Remove) => false,
776 None => is_readonly, // Preserve original readonly status
777 };
778
779 if final_readonly {
780 // Wrap the array type in ReadonlyType to get readonly semantics
781 let array_type = self.interner().array(mapped_element);
782 self.interner().readonly_type(array_type)
783 } else {
784 self.interner().array(mapped_element)
785 }
786 }
787
788 /// Evaluate a homomorphic mapped type over a Tuple type.
789 ///
790 /// For example: `type Partial<T> = { [P in keyof T]?: T[P] }`
791 /// `Partial<[number, string]>` should produce `[number?, string?]`
792 ///
793 /// We instantiate the template with `K = 0, 1, 2...` for each tuple element.
794 /// This preserves tuple structure including optional and rest elements.
795 fn evaluate_mapped_tuple(&mut self, mapped: &MappedType, tuple_id: TupleListId) -> TypeId {
796 use crate::types::TupleElement;
797
798 let tuple_elements = self.interner().tuple_list(tuple_id);
799 let mut mapped_elements = Vec::new();
800
801 for (i, elem) in tuple_elements.iter().enumerate() {
802 // CRITICAL: Handle rest elements specially
803 // For rest elements (...T[]), we cannot use index substitution.
804 // We must map the array type itself.
805 if elem.rest {
806 // Rest elements like ...number[] need to be mapped as arrays
807 // Check if the rest type is an Array
808 let rest_type = elem.type_id;
809 let mapped_rest_type = match self.interner().lookup(rest_type) {
810 Some(TypeData::Array(inner_elem)) => {
811 // Map the inner array element
812 // Reuse the array mapping logic
813 self.evaluate_mapped_array(mapped, inner_elem)
814 }
815 Some(TypeData::Tuple(inner_tuple_id)) => {
816 // Nested tuple in rest - recurse
817 self.evaluate_mapped_tuple(mapped, inner_tuple_id)
818 }
819 _ => {
820 // Fallback: try index substitution (may not work correctly)
821 let index_type = self.interner().literal_number(i as f64);
822 let mut subst = TypeSubstitution::new();
823 subst.insert(mapped.type_param.name, index_type);
824 self.evaluate(instantiate_type(self.interner(), mapped.template, &subst))
825 }
826 };
827
828 // Handle optional modifier for rest elements
829 let final_rest_type =
830 if matches!(mapped.optional_modifier, Some(MappedModifier::Add)) {
831 self.interner().union2(mapped_rest_type, TypeId::UNDEFINED)
832 } else {
833 mapped_rest_type
834 };
835
836 mapped_elements.push(TupleElement {
837 type_id: final_rest_type,
838 name: elem.name,
839 optional: elem.optional,
840 rest: true,
841 });
842 continue;
843 }
844
845 // Non-rest elements: use index substitution
846 // Create a literal number type for this tuple position
847 let index_type = self.interner().literal_number(i as f64);
848
849 // Create substitution: type_param.name -> literal number
850 let mut subst = TypeSubstitution::new();
851 subst.insert(mapped.type_param.name, index_type);
852
853 // Substitute into the template to get the mapped element type
854 let mapped_type =
855 self.evaluate(instantiate_type(self.interner(), mapped.template, &subst));
856
857 // Get the modifiers for this element
858 // Note: readonly is currently unused for tuple elements, but we preserve the logic
859 // in case TypeScript adds readonly tuple element support in the future
860 // CRITICAL: Handle optional and readonly modifiers independently
861 let optional = match mapped.optional_modifier {
862 Some(MappedModifier::Add) => true,
863 Some(MappedModifier::Remove) => false,
864 None => elem.optional, // Preserve original optional
865 };
866 let _readonly = match mapped.readonly_modifier {
867 Some(MappedModifier::Add) => true,
868 Some(MappedModifier::Remove) | None => false,
869 // Tuple elements don't have readonly in current TypeScript
870 };
871
872 mapped_elements.push(TupleElement {
873 type_id: mapped_type,
874 name: elem.name,
875 optional,
876 rest: elem.rest,
877 });
878 }
879
880 self.interner().tuple(mapped_elements)
881 }
882}