|
| 1 | +# Learnings: Migration Intersection Types Refactoring |
| 2 | + |
| 3 | +## Key Challenge: Literal Type Widening in Scala 3 Macros |
| 4 | + |
| 5 | +When using `ConstantType(StringConstant(fieldName)).asType` in Scala 3 macros, the resulting type variable gets widened to `String` instead of preserving the literal type (e.g., `"age"`). |
| 6 | + |
| 7 | +### What Didn't Work |
| 8 | + |
| 9 | +1. **Direct intersection with literal type**: `TP & fn` where `fn` is captured from `asType` - the literal type gets widened to `String`. |
| 10 | + |
| 11 | +2. **Constructing AndType directly**: `AndType(TypeRepr.of[TP], fieldNameType).asType` - same widening issue. |
| 12 | + |
| 13 | +3. **Type alias with intersection**: `type AddField[T, F] = T & F` - the literal type still gets widened when passed through the type system. |
| 14 | + |
| 15 | +### What Worked: Wrapper Type Pattern |
| 16 | + |
| 17 | +The solution is to wrap the literal type in a phantom type that preserves it: |
| 18 | + |
| 19 | +```scala |
| 20 | +sealed trait FieldName[N <: String & Singleton] |
| 21 | +type AddField[T, F <: String & Singleton] = T & FieldName[F] |
| 22 | +``` |
| 23 | + |
| 24 | +In the macros, construct the type as: |
| 25 | +```scala |
| 26 | +val fieldNameType = ConstantType(StringConstant(fieldName)) |
| 27 | +val fieldNameWrapped = TypeRepr.of[FieldName].appliedTo(fieldNameType) |
| 28 | +val newTPType = AndType(TypeRepr.of[TP], fieldNameWrapped) |
| 29 | +``` |
| 30 | + |
| 31 | +The `FieldName[N]` wrapper preserves the literal type because: |
| 32 | +1. The literal type is stored as a type parameter to `FieldName` |
| 33 | +2. When `asType` is called on the `AppliedType`, the type parameter is preserved |
| 34 | +3. The validation macro can extract the literal type by pattern matching on `AppliedType(tycon, List(field))` where `tycon.typeSymbol.name == "FieldName"` |
| 35 | + |
| 36 | +### Why Tuples Worked Originally |
| 37 | + |
| 38 | +The original tuple-based approach (`Tuple.Concat[TP, fn *: EmptyTuple]`) worked because: |
| 39 | +1. `Tuple.Concat` is a type-level computation that the compiler evaluates lazily |
| 40 | +2. The literal type `fn` is used as a type argument in a type expression |
| 41 | +3. The compiler preserves literal types in type-level computations |
| 42 | + |
| 43 | +### Extraction Pattern |
| 44 | + |
| 45 | +To extract field names from the intersection type: |
| 46 | +```scala |
| 47 | +def extract(tpe: TypeRepr): Set[String] = tpe.dealias match { |
| 48 | + case AndType(left, right) => extract(left) ++ extract(right) |
| 49 | + case ConstantType(StringConstant(s)) => Set(s) |
| 50 | + case t if t =:= TypeRepr.of[Any] => Set.empty |
| 51 | + case AppliedType(tycon, List(field)) if tycon.typeSymbol.name == "FieldName" => |
| 52 | + extract(field) |
| 53 | + case _ => Set.empty |
| 54 | +} |
| 55 | +``` |
0 commit comments