diff --git a/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/src/processor.js b/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/src/processor.js index ef446af0..04578e9e 100644 --- a/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/src/processor.js +++ b/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/src/processor.js @@ -663,6 +663,7 @@ export class TypeProcessor { * @private */ visitType(type, node) { + const typeArguments = this.getTypeArguments(type); if (this.checker.isArrayType(type)) { const typeArgs = this.checker.getTypeArguments(type); if (typeArgs && typeArgs.length > 0) { @@ -672,6 +673,11 @@ export class TypeProcessor { return "[JSObject]"; } + const recordType = this.convertRecordType(type, typeArguments, node); + if (recordType) { + return recordType; + } + // Treat A and A as the same type if (isTypeReference(type)) { type = type.target; @@ -760,6 +766,54 @@ export class TypeProcessor { return swiftType; } + /** + * Convert a `Record` TypeScript type into a Swift dictionary type. + * Falls back to `JSObject` when keys are not string-compatible or type arguments are missing. + * @param {ts.Type} type + * @param {ts.Type[]} typeArguments + * @param {ts.Node} node + * @returns {string | null} + * @private + */ + convertRecordType(type, typeArguments, node) { + const symbol = type.aliasSymbol ?? type.getSymbol(); + if (!symbol || symbol.name !== "Record") { + return null; + } + if (typeArguments.length !== 2) { + this.diagnosticEngine.print("warning", "Record expects two type arguments", node); + return "JSObject"; + } + const [keyType, valueType] = typeArguments; + const stringType = this.checker.getStringType(); + if (!this.checker.isTypeAssignableTo(keyType, stringType)) { + this.diagnosticEngine.print( + "warning", + `Record key type must be assignable to string: ${this.checker.typeToString(keyType)}`, + node + ); + return "JSObject"; + } + + const valueSwiftType = this.visitType(valueType, node); + return `[String: ${valueSwiftType}]`; + } + + /** + * Retrieve type arguments for a given type, including type alias instantiations. + * @param {ts.Type} type + * @returns {ts.Type[]} + * @private + */ + getTypeArguments(type) { + if (isTypeReference(type)) { + return this.checker.getTypeArguments(type); + } + // Non-TypeReference alias instantiations store type arguments separately. + // @ts-ignore: `aliasTypeArguments` is intentionally accessed for alias instantiations. + return type.aliasTypeArguments ?? []; + } + /** * Derive the type name from a type * @param {ts.Type} type - TypeScript type diff --git a/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/__snapshots__/ts2swift.test.js.snap b/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/__snapshots__/ts2swift.test.js.snap index 6fdb79f7..e6fb234c 100644 --- a/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/__snapshots__/ts2swift.test.js.snap +++ b/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/__snapshots__/ts2swift.test.js.snap @@ -251,6 +251,36 @@ exports[`ts2swift > snapshots Swift output for ReExportFrom.d.ts > ReExportFrom " `; +exports[`ts2swift > snapshots Swift output for RecordDictionary.d.ts > RecordDictionary 1`] = ` +"// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, +// DO NOT EDIT. +// +// To update this file, just rebuild your project or run +// \`swift package bridge-js\`. + +@_spi(Experimental) @_spi(BridgeJS) import JavaScriptKit + +@JSFunction func takeRecord(_ value: [String: Double]) throws(JSException) -> Void + +@JSFunction func returnRecord() throws(JSException) -> [String: String] + +@JSFunction func optionalRecord(_ value: Optional<[String: Bool]>) throws(JSException) -> Optional<[String: Bool]> + +@JSFunction func nestedRecord(_ value: [String: [String: Double]]) throws(JSException) -> [String: [String: Double]] + +@JSFunction func recordWithArrayValues(_ values: [String: [Double]]) throws(JSException) -> [String: [Double]] + +@JSFunction func recordWithObjects(_ values: [String: Optional]) throws(JSException) -> [String: Optional] + +@JSClass struct Box { + @JSGetter var value: Double + @JSSetter func setValue(_ value: Double) throws(JSException) +} + +@JSFunction func unsupportedKeyRecord(_ values: JSObject) throws(JSException) -> Void +" +`; + exports[`ts2swift > snapshots Swift output for StringEnum.d.ts > StringEnum 1`] = ` "// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, // DO NOT EDIT. diff --git a/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/fixtures/RecordDictionary.d.ts b/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/fixtures/RecordDictionary.d.ts new file mode 100644 index 00000000..cfaf5f30 --- /dev/null +++ b/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/fixtures/RecordDictionary.d.ts @@ -0,0 +1,17 @@ +export interface Box { + value: number; +} + +export function takeRecord(value: Record): void; + +export function returnRecord(): Record; + +export function optionalRecord(value: Record | null): Record | null; + +export function nestedRecord(value: Record>): Record>; + +export function recordWithArrayValues(values: Record): Record; + +export function recordWithObjects(values: Record): Record; + +export function unsupportedKeyRecord(values: Record): void;