@@ -354,7 +354,7 @@ export class ObjectType extends Type {
354354 }
355355
356356 // Resolve aliases and type member access
357- if ( target instanceof TypeAlias || target instanceof TypeMemberAccess || target instanceof KeyofType ) {
357+ if ( target instanceof TypeAlias || target instanceof TypeMemberAccess || target instanceof KeyofType || target instanceof MappedType || target instanceof TypeIndexAccess ) {
358358 const resolved = aliases . resolve ( target ) ;
359359 // Avoid infinite recursion if alias can't be resolved
360360 if ( resolved === target ) return false ;
@@ -1023,6 +1023,85 @@ export class TypeMemberAccess extends Type {
10231023 }
10241024}
10251025
1026+ /**
1027+ * Mapped type: { [K in source]: valueType }
1028+ * Represents a mapped object type that iterates over keys of a source type.
1029+ * When resolved, expands to an ObjectType with one property per key.
1030+ */
1031+ export class MappedType extends Type {
1032+ /**
1033+ * @param {string } keyParam - The name of the key parameter (e.g. 'K')
1034+ * @param {Type } sourceType - The source type whose keys to iterate (e.g. KeyofType)
1035+ * @param {Type } valueType - The value type expression (may reference keyParam)
1036+ * @param {boolean } optional - Whether properties should be optional
1037+ * @param {boolean } readonly - Whether properties should be readonly
1038+ */
1039+ constructor ( keyParam , sourceType , valueType , optional = false , readonly = false ) {
1040+ super ( ) ;
1041+ this . kind = 'mapped' ;
1042+ this . keyParam = keyParam ;
1043+ this . sourceType = sourceType ;
1044+ this . valueType = valueType ;
1045+ this . optional = optional ;
1046+ this . readonly = readonly ;
1047+ }
1048+
1049+ toString ( ) {
1050+ const ro = this . readonly ? 'readonly ' : '' ;
1051+ const opt = this . optional ? '?' : '' ;
1052+ return `{ ${ ro } [${ this . keyParam } in ${ this . sourceType } ]${ opt } : ${ this . valueType } }` ;
1053+ }
1054+
1055+ equals ( other ) {
1056+ return other instanceof MappedType
1057+ && this . keyParam === other . keyParam
1058+ && this . sourceType . equals ( other . sourceType )
1059+ && this . valueType . equals ( other . valueType )
1060+ && this . optional === other . optional ;
1061+ }
1062+
1063+ isCompatibleWith ( target , aliases ) {
1064+ const resolved = aliases . resolve ( this ) ;
1065+ if ( resolved !== this ) return resolved . isCompatibleWith ( target , aliases ) ;
1066+ if ( isAnyType ( target ) ) return true ;
1067+ return this . equals ( target ) ;
1068+ }
1069+ }
1070+
1071+ /**
1072+ * Indexed type access: T[K] where K is a type variable or literal.
1073+ * Used in mapped type value expressions like T[K].
1074+ */
1075+ export class TypeIndexAccess extends Type {
1076+ /**
1077+ * @param {Type } baseType - The type being indexed (e.g. TypeAlias('T'))
1078+ * @param {Type } keyType - The key type (e.g. TypeAlias('K') or LiteralType)
1079+ */
1080+ constructor ( baseType , keyType ) {
1081+ super ( ) ;
1082+ this . kind = 'index_access' ;
1083+ this . baseType = baseType ;
1084+ this . keyType = keyType ;
1085+ }
1086+
1087+ toString ( ) {
1088+ return `${ this . baseType } [${ this . keyType } ]` ;
1089+ }
1090+
1091+ equals ( other ) {
1092+ return other instanceof TypeIndexAccess
1093+ && this . baseType . equals ( other . baseType )
1094+ && this . keyType . equals ( other . keyType ) ;
1095+ }
1096+
1097+ isCompatibleWith ( target , aliases ) {
1098+ const resolved = aliases . resolve ( this ) ;
1099+ if ( resolved !== this ) return resolved . isCompatibleWith ( target , aliases ) ;
1100+ if ( isAnyType ( target ) ) return true ;
1101+ return this . equals ( target ) ;
1102+ }
1103+ }
1104+
10261105/**
10271106 * Type alias reference (not resolved yet)
10281107 */
@@ -1157,6 +1236,59 @@ export class TypeAliasMap {
11571236 return StringType ;
11581237 }
11591238
1239+ /**
1240+ * Resolve a TypeIndexAccess T[K] to the concrete property type.
1241+ * If K is a string literal and T is an ObjectType, looks up the property.
1242+ * @param {TypeIndexAccess } type
1243+ * @returns {Type }
1244+ */
1245+ resolveIndexAccess ( type ) {
1246+ const resolvedBase = this . resolve ( type . baseType ) ;
1247+ const resolvedKey = this . resolve ( type . keyType ) ;
1248+ if ( resolvedBase instanceof ObjectType && resolvedKey instanceof LiteralType ) {
1249+ const prop = resolvedBase . properties . get ( resolvedKey . value ) ;
1250+ if ( prop ) return prop . type ;
1251+ }
1252+ if ( resolvedBase instanceof PrimitiveType && resolvedBase . name === 'any' ) return AnyType ;
1253+ return AnyType ;
1254+ }
1255+
1256+ /**
1257+ * Resolve a MappedType by expanding its key set and building an ObjectType.
1258+ * @param {MappedType } type
1259+ * @returns {Type }
1260+ */
1261+ resolveMappedType ( type ) {
1262+ const resolvedSource = this . resolve ( type . sourceType ) ;
1263+
1264+ // Collect the concrete key literals
1265+ let keyLiterals ;
1266+ if ( resolvedSource instanceof UnionType ) {
1267+ keyLiterals = resolvedSource . types . filter ( t => t instanceof LiteralType && t . baseType === StringType ) ;
1268+ } else if ( resolvedSource instanceof LiteralType && resolvedSource . baseType === StringType ) {
1269+ keyLiterals = [ resolvedSource ] ;
1270+ } else if ( resolvedSource instanceof PrimitiveType && resolvedSource . name === 'string' ) {
1271+ // Open key set — produce a RecordType
1272+ const subs = new Map ( [ [ type . keyParam , StringType ] ] ) ;
1273+ const resolvedValue = this . resolve ( substituteTypeParams ( type . valueType , subs ) ) ;
1274+ return new RecordType ( StringType , resolvedValue ) ;
1275+ } else {
1276+ // Cannot determine keys — fallback
1277+ return AnyType ;
1278+ }
1279+
1280+ if ( keyLiterals . length === 0 ) return new ObjectType ( new Map ( ) ) ;
1281+
1282+ const newProps = new Map ( ) ;
1283+ for ( const keyLiteral of keyLiterals ) {
1284+ const subs = new Map ( [ [ type . keyParam , keyLiteral ] ] ) ;
1285+ const substituted = substituteTypeParams ( type . valueType , subs ) ;
1286+ const resolvedValue = this . resolve ( substituted ) ;
1287+ newProps . set ( String ( keyLiteral . value ) , { type : resolvedValue , optional : type . optional , readonly : type . readonly } ) ;
1288+ }
1289+ return new ObjectType ( newProps ) ;
1290+ }
1291+
11601292 /**
11611293 * Resolve a type alias (recursive with cycle detection)
11621294 * @param {Type } type
@@ -1169,6 +1301,12 @@ export class TypeAliasMap {
11691301 if ( type instanceof KeyofType ) {
11701302 return this . resolveKeyof ( type ) ;
11711303 }
1304+ if ( type instanceof TypeIndexAccess ) {
1305+ return this . resolveIndexAccess ( type ) ;
1306+ }
1307+ if ( type instanceof MappedType ) {
1308+ return this . resolveMappedType ( type ) ;
1309+ }
11721310 if ( ! ( type instanceof TypeAlias ) ) {
11731311 return type ;
11741312 }
@@ -1307,6 +1445,26 @@ export function substituteTypeParams(type, substitutions) {
13071445 return new KeyofType ( substituteTypeParams ( type . subjectType , substitutions ) ) ;
13081446 }
13091447
1448+ if ( type instanceof MappedType ) {
1449+ // Substitute in sourceType and valueType, but NOT in keyParam (it's a bound variable)
1450+ const filteredSubs = new Map ( substitutions ) ;
1451+ filteredSubs . delete ( type . keyParam ) ; // keyParam is locally bound — don't substitute it
1452+ return new MappedType (
1453+ type . keyParam ,
1454+ substituteTypeParams ( type . sourceType , substitutions ) ,
1455+ substituteTypeParams ( type . valueType , filteredSubs ) ,
1456+ type . optional ,
1457+ type . readonly
1458+ ) ;
1459+ }
1460+
1461+ if ( type instanceof TypeIndexAccess ) {
1462+ return new TypeIndexAccess (
1463+ substituteTypeParams ( type . baseType , substitutions ) ,
1464+ substituteTypeParams ( type . keyType , substitutions )
1465+ ) ;
1466+ }
1467+
13101468 // Primitives and literals don't need substitution
13111469 return type ;
13121470}
0 commit comments