Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/expo-modules-core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

### 💡 Others

- [iOS] Improved conversions of returned arrays and dictionaries with mixed element types. ([#42641](https://github.com/expo/expo/pull/42641) by [@barthap](https://github.com/barthap))

## 55.0.12 — 2026-02-25

_This version does not introduce any user-facing changes._
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,7 @@ internal struct DynamicArrayType: AnyDynamicType {

func convertResult<ResultType>(_ result: ResultType, appContext: AppContext) throws -> Any {
if let result = result as? [Any] {
return try result.map({ element in
return try elementType.convertResult(element, appContext: appContext)
})
return result.map { Conversions.convertFunctionResult($0, appContext: appContext) }
}
return result
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ internal struct DynamicDictionaryType: AnyDynamicType {
throw Conversions.CastingException<[AnyHashable: Any]>(value)
}

func convertResult<ResultType>(_ result: ResultType, appContext: AppContext) throws -> Any {
if let result = result as? [AnyHashable: Any] {
return result.mapValues { Conversions.convertFunctionResult($0, appContext: appContext) }
}
return result
}

var description: String {
"[Hashable: \(valueType.description)]"
}
Expand Down
104 changes: 104 additions & 0 deletions packages/expo-modules-core/ios/Tests/DynamicTypeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,19 @@ struct DynamicTypeTests {
try (~[String].self).cast(84, appContext: appContext)
}
}

@Test
func `returns mixed elements to JS`() throws {
let mixedArray: [Any] = [1, ArrayBuffer.allocate(size: 3)]

let converted = try (~[Any].self).convertResult(mixedArray, appContext: appContext)
let jsValue = try (~[Any].self).castToJS(converted, appContext: appContext)

#expect(jsValue.kind == .object)
let jsArray = try jsValue.asArray()
#expect(try jsArray.first??.getInt() == 1)
#expect(try jsArray.last??.isArrayBuffer() == true)
}

@Test
func `wraps is true`() {
Expand Down Expand Up @@ -280,6 +293,97 @@ struct DynamicTypeTests {
}
}

// MARK: - DynamicDictionaryType

@Suite("DynamicDictionaryType")
struct DynamicDictionaryTypeTests {
let appContext: AppContext

init() {
appContext = AppContext.create()
}

@Test
func `is created`() {
#expect(~[String: Double].self is DynamicDictionaryType)
#expect(~[String: String?].self is DynamicDictionaryType)
#expect(~[String: [Int]].self is DynamicDictionaryType)
#expect(~[String: Any].self is DynamicDictionaryType)
}

@Test
func `casts succeeds`() throws {
#expect(try (~[String: Double].self).cast(["a": 1.2, "b": 3.4], appContext: appContext) as? [String: Double] == ["a": 1.2, "b": 3.4])
#expect(try (~[String: [String]].self).cast(["key": ["hello", "expo"]], appContext: appContext) as? [String: [String]] == ["key": ["hello", "expo"]])
}

@Test
func `casts from JS value`() throws {
let appContext = AppContext.create()
let jsValue = try appContext.runtime.eval("({a: 1.2, b: 3.4})")
#expect(try (~[String: Double].self).cast(jsValue: jsValue, appContext: appContext) as? [String: Double] == ["a": 1.2, "b": 3.4])
}

@Test
func `casts dictionary values`() throws {
let value = 9.9
let anyValue: [String: Any] = ["key": value]
let result = try (~[String: Double].self).cast(anyValue, appContext: appContext) as! [AnyHashable: Any]

#expect(result is [String: Double])
#expect(result as? [String: Double] == ["key": value])
}

@Test
func `returns mixed elements to JS`() throws {
let mixedDict: [String: Any] = ["num": 1, "buf": ArrayBuffer.allocate(size: 3)]

let converted = try (~[String: Any].self).convertResult(mixedDict, appContext: appContext)
let jsValue = try (~[String: Any].self).castToJS(converted, appContext: appContext)

#expect(jsValue.kind == .object)
let jsObject = try jsValue.asObject()
#expect(try jsObject.getProperty("num").getInt() == 1)
#expect(jsObject.getProperty("buf").isArrayBuffer() == true)
}

@Test
func `throws CastingException`() {
#expect(throws: Conversions.CastingException<[AnyHashable: Any]>.self) {
try (~[String: String].self).cast(84, appContext: appContext)
}
}

@Test
func `wraps is true`() {
#expect((~[String: Double].self ~> [String: Double].self) == true)
#expect((~[String: [String]].self ~> [String: [String]].self) == true)
#expect((~[String: CGPoint].self ~> [String: CGPoint].self) == true)
#expect((~[String: Any].self ~> [String: Any].self) == true)
}

@Test
func `wraps is false`() {
#expect((~[String: String].self !~> [String: Int].self) == true)
#expect((~[String: Double].self !~> Double.self) == true)
}

@Test
func `equals is true`() {
#expect((~[String: String].self == ~[String: String].self) == true)
#expect((~[String: CGSize].self == ~[String: CGSize].self) == true)
#expect((~[String: [[Double]]].self == ~[String: [[Double]]].self) == true)
#expect((~[String: Any].self == ~[String: Any].self) == true)
}

@Test
func `equals is false`() {
#expect((~[String: Int].self != ~[String: Double].self) == true)
#expect((~[String: [String]].self != ~[String: String].self) == true)
#expect((~[String: URL].self != ~[String: String].self) == true)
}
}

// MARK: - DynamicConvertibleType

@Suite("DynamicConvertibleType")
Expand Down
Loading