Normalize implicitly-unwrapped optional types before splicing into expressions#23
Merged
Conversation
…pressions
The `@ExpoModule`/`@JS` and `@Record` macros splice a member's type
(`type.trimmedDescription`) into expression position to reach the dynamic-type
converter, e.g. `\(type).getDynamicType().cast(...) as! \(type)`. For an
implicitly-unwrapped optional (`var x: SomeType!`, `func f() -> SomeType!`, a
`@Record` property of type `T!`) the spliced string is `SomeType!`, which is
invalid Swift in expression position ("using '!' is not allowed here") and
produces code that doesn't compile. Plain optionals (`T?`) and `Optional<T>`
were unaffected; only the IUO spelling broke.
Add `expressionType(_:)`, which rewrites a trailing `!` to `?` (`T!` and `T?`
are both `Optional<T>`, treated identically by the dynamic-type / cast layer),
and apply it at every site that splices a type into expression position: the
function argument decode and result encode, the property get/set accessors, and
the `@Record` `from(object:)` / `from(dictionary:)` / `toObject()` conversions.
Type-annotation positions (where `T!` is valid) keep the original spelling.
The existing snapshot tests didn't catch this because none exercised an IUO
type through a splice, and `assertMacroExpansion` checks the generated string,
not that it compiles. Add IUO coverage for a module property, a function return
type, and a `@Record` property, asserting the cast/convert expressions use `T?`
while annotations stay `T!`.
Kudo
approved these changes
Jun 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The
@ExpoModule/@JSand@Recordmacros splice a member's type (type.trimmedDescription) into expression position to reach the dynamic-type converter, e.g.:For an implicitly-unwrapped optional, the spliced string is
SomeType!, which is invalid Swift in expression position:So a
@JS func/varor@Recordproperty declared with an IUO type (var x: SomeType!,func f() -> SomeType!) generated code that doesn't compile. Plain optionals (T?) andOptional<T>were unaffected; only theT!spelling broke. The existing snapshot tests didn't catch it because none exercised an IUO type through a splice, andassertMacroExpansionchecks the generated string, not that it compiles.How
Add
expressionType(_:), which rewrites a trailing!to?(T!andT?are bothOptional<T>, treated identically by the dynamic-type / cast layer, and?is valid in expression position). Apply it at every site that splices a type into expression position:@Recordfrom(object:)/from(dictionary:)/toObject()conversions.Type-annotation positions (where
T!is valid, e.g. the emittedvar x: T!andinit(x: T! = nil)) keep the original spelling.Test Plan
swift testinapple/(109 tests). Added IUO coverage for a module property, a function return type, and a@Recordproperty, each asserting the cast/convert expressions useT?while annotations stayT!.