Skip to content

Commit 565ca05

Browse files
Documentation: Add JavaScript Interop Cheat Sheet
1 parent 5f536c8 commit 565ca05

File tree

2 files changed

+377
-0
lines changed

2 files changed

+377
-0
lines changed
Lines changed: 376 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,376 @@
1+
# JavaScript Interop Cheat Sheet
2+
3+
Practical recipes for manipulating JavaScript values from Swift with JavaScriptKit. Each section shows the shortest path to access, call, or convert the APIs you interact with the most.
4+
5+
## Access JavaScript Values
6+
7+
### Global entry points
8+
9+
```swift
10+
let global: JSObject = JSObject.global
11+
let document: JSObject = global.document.object!
12+
let math: JSObject = global.Math.object!
13+
```
14+
15+
- Use ``JSObject/global`` for `globalThis` and drill into properties.
16+
- Accessing through [dynamic member lookup](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0195-dynamic-member-lookup.md) returns ``JSValue``; call `.object`, `.number`, `.string`, etc. to unwrap a concrete type (callable values are represented as ``JSObject`` as well).
17+
- Prefer storing ``JSObject`` references (`document` above) when you call multiple members to avoid repeated conversions (for performance).
18+
19+
### Properties, subscripts, and symbols
20+
21+
```swift
22+
extension JSObject {
23+
public subscript(_ name: String) -> JSValue { get set }
24+
public subscript(_ index: Int) -> JSValue { get set }
25+
public subscript(_ name: JSSymbol) -> JSValue { get set }
26+
/// Use this API when you want to avoid repeated String serialization overhead
27+
public subscript(_ name: JSString) -> JSValue { get set }
28+
/// A convenience method of `subscript(_ name: String) -> JSValue`
29+
/// to access the member through Dynamic Member Lookup.
30+
/// ```swift
31+
/// let document: JSObject = JSObject.global.document.object!
32+
/// ```
33+
public subscript(dynamicMember name: String) -> JSValue { get set }
34+
}
35+
extension JSValue {
36+
/// An unsafe convenience method of `JSObject.subscript(_ index: Int) -> JSValue`
37+
/// - Precondition: `self` must be a JavaScript Object.
38+
public subscript(dynamicMember name: String) -> JSValue
39+
public subscript(_ index: Int) -> JSValue
40+
}
41+
```
42+
43+
**Example**
44+
45+
```swift
46+
document.title = .string("Swift <3 Web")
47+
let obj = JSObject.global.myObject.object!
48+
let value = obj["key"].string // Access object property with String
49+
let propName = JSString("key")
50+
let value2 = obj[propName].string // Access object property with JSString
51+
52+
let array = JSObject.global.Array.object!.new(1, 2, 3)
53+
array[0] = .number(10) // Assign to array index
54+
55+
let symbol = JSSymbol("secret")
56+
let data = obj[symbol].object
57+
```
58+
59+
## Call Functions and Methods
60+
61+
```swift
62+
extension JSObject {
63+
/// Call this function with given `arguments` using [Callable values of user-defined nominal types](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0253-callable.md)
64+
/// ```swift
65+
/// let alert = JSObject.global.alert.object!
66+
/// alert("Hello from Swift")
67+
/// ```
68+
public func callAsFunction(_ arguments: ConvertibleToJSValue...) -> JSValue
69+
public func callAsFunction(this: JSObject, _ arguments: ConvertibleToJSValue...) -> JSValue
70+
71+
/// Returns the `name` member method binding this object as `this` context.
72+
public subscript(_ name: String) -> ((ConvertibleToJSValue...) -> JSValue)? { get }
73+
public subscript(_ name: JSString) -> ((ConvertibleToJSValue...) -> JSValue)? { get }
74+
/// A convenience method of `subscript(_ name: String) -> ((ConvertibleToJSValue...) -> JSValue)?` to access the member through Dynamic Member Lookup.
75+
/// ```swift
76+
/// let document = JSObject.global.document.object!
77+
/// let divElement = document.createElement!("div")
78+
/// ```
79+
public subscript(dynamicMember name: String) -> ((ConvertibleToJSValue...) -> JSValue)? { get }
80+
}
81+
extension JSValue {
82+
/// An unsafe convenience method of `JSObject.subscript(_ name: String) -> ((ConvertibleToJSValue...) -> JSValue)?`
83+
/// - Precondition: `self` must be a JavaScript Object and specified member should be a callable object.
84+
public subscript(dynamicMember name: String) -> ((ConvertibleToJSValue...) -> JSValue)
85+
}
86+
```
87+
88+
**Example**
89+
90+
```swift
91+
let alert = JSObject.global.alert.object!
92+
alert("Hello from Swift")
93+
94+
let console = JSObject.global.console.object!
95+
_ = console.log!("Cheat sheet ready", 1, true)
96+
97+
let document = JSObject.global.document.object!
98+
let button = document.createElement!("button").object!
99+
_ = button.classList.add("primary")
100+
```
101+
102+
- **Dynamic Member Lookup and `!`**: When calling a method on ``JSObject`` (like `createElement!`), it returns an optional closure, so `!` is used to unwrap and call it. In contrast, calling on ``JSValue`` (like `button.classList.add`) returns a non-optional closure that traps on failure for convenience.
103+
104+
Need to bind manually? Grab the function object and supply `this`:
105+
106+
```swift
107+
let appendChild = document.body.appendChild.object!
108+
appendChild(this: document.body.object!, document.createElement!("div"))
109+
```
110+
111+
### Passing options objects
112+
113+
When JavaScript APIs require an options object, create one using ``JSObject``:
114+
115+
```swift
116+
public class JSObject: ExpressibleByDictionaryLiteral {
117+
/// Creates an empty JavaScript object (equivalent to {} or new Object())
118+
public init()
119+
120+
/// Creates a new object with the key-value pairs in the dictionary literal
121+
public init(dictionaryLiteral elements: (String, JSValue)...)
122+
}
123+
```
124+
125+
**Example**
126+
127+
```swift
128+
// Create options object with dictionary literal
129+
let listeningOptions: JSObject = ["once": .boolean(true), "passive": .boolean(true)]
130+
button.addEventListener!("click", handler, listeningOptions)
131+
132+
// Create empty object and add properties
133+
let fetchOptions = JSObject()
134+
fetchOptions["method"] = .string("POST")
135+
let headers: JSObject = ["Content-Type": .string("application/json")]
136+
fetchOptions["headers"] = headers.jsValue
137+
fetchOptions["body"] = "{}".jsValue
138+
139+
let fetch = JSObject.global.fetch.object!
140+
let response = fetch("https://api.example.com", fetchOptions)
141+
```
142+
143+
### Throwing JavaScript
144+
145+
JavaScript exceptions surface as ``JSException``. Wrap the function (or object) in a throwing helper.
146+
147+
```swift
148+
// Method
149+
let JSON = JSObject.global.JSON.object!
150+
do {
151+
let value = try JSON.throwing.parse!("{\"flag\":true}")
152+
} catch let error as JSException {
153+
print("Invalid JSON", error)
154+
}
155+
156+
// Function
157+
let validateAge: JSObject = JSObject.global.validateAge.object!
158+
do {
159+
try validateAge.throws(-3)
160+
} catch let error as JSException {
161+
print("Validation failed:", error)
162+
}
163+
```
164+
165+
- Use ``JSObject/throwing`` to access object methods that may throw JavaScript exceptions.
166+
- Use ``JSObject/throws`` to call the callable object itself that may throw JavaScript exceptions.
167+
168+
### Constructors and `new`
169+
170+
```swift
171+
let url = JSObject.global.URL.object!.new("https://example.com", "https://example.com")
172+
let searchParams = url.searchParams.object!
173+
```
174+
175+
Use ``JSThrowingFunction/new(_:)`` (via `throws.new`) when the constructor can throw.
176+
177+
## Convert Between Swift and JavaScript
178+
179+
### Swift -> JavaScript
180+
181+
Types conforming to ``ConvertibleToJSValue`` can be converted via the `.jsValue` property. Conversion behavior depends on the context:
182+
183+
| Swift type | JavaScript result | Notes |
184+
|------------|------------------|-------|
185+
| `Bool` | `JSValue.boolean(Bool)` | |
186+
| `String` | `JSValue.string(JSString)` | Wrapped in ``JSString`` to avoid extra copies |
187+
| `Int`, `UInt`, `Int8-32`, `UInt8-32`, `Float`, `Double` | `JSValue.number(Double)` | All numeric types convert to `Double` |
188+
| `Int64`, `UInt64` | `JSValue.bigInt(JSBigInt)` | Converted to `BigInt` (requires `import JavaScriptBigIntSupport`) |
189+
| `Data` | `Uint8Array` | Converted to `Uint8Array` (requires `import JavaScriptFoundationCompat`) |
190+
| `Array<Element>` where `Element: ConvertibleToJSValue` | JavaScript Array | Each element converted via `.jsValue` |
191+
| `Dictionary<String, Value>` where `Value: ConvertibleToJSValue` | Plain JavaScript object | Keys must be `String` |
192+
| `Optional.none` | `JSValue.null` | Use ``JSValue/undefined`` when you specifically need `undefined` |
193+
| `Optional.some(wrapped)` | `wrapped.jsValue` | |
194+
| ``JSValue``, ``JSObject``, ``JSString`` | Passed through | No conversion needed |
195+
196+
**Function arguments**: Automatic conversion when passing to JavaScript functions:
197+
198+
```swift
199+
let alert = JSObject.global.alert.object!
200+
alert("Hello") // String automatically converts via .jsValue
201+
202+
let console = JSObject.global.console.object!
203+
console.log!("Count", 42, true) // All arguments auto-convert
204+
```
205+
206+
**Property assignment**: Explicit conversion required:
207+
208+
```swift
209+
let obj = JSObject.global.myObject.object!
210+
let count: Int = 42
211+
let message = "Hello"
212+
213+
obj["count"] = count.jsValue
214+
obj["message"] = message.jsValue
215+
216+
obj.count = count.jsValue
217+
obj.message = message.jsValue
218+
219+
// Alternative: use JSValue static methods
220+
obj["count"] = .number(Double(count))
221+
obj.message = .string(message)
222+
divElement.innerText = .string("Count \(count)")
223+
canvasElement.width = .number(Double(size))
224+
```
225+
226+
### JavaScript -> Swift
227+
228+
Access JavaScript values through ``JSValue`` accessors:
229+
230+
```swift
231+
let jsValue: JSValue = // ... some JavaScript value
232+
233+
// Primitive types via direct accessors (most common pattern)
234+
let message: String? = jsValue.string
235+
let n: Double? = jsValue.number
236+
let flag: Bool? = jsValue.boolean
237+
let obj: JSObject? = jsValue.object
238+
239+
// Access nested properties through JSObject subscripts
240+
if let obj = jsValue.object {
241+
let nested = obj.key.string
242+
let arrayItem = obj.items[0].string
243+
let count = obj.count.number
244+
}
245+
246+
// Arrays (if elements conform to ConstructibleFromJSValue)
247+
if let items = [String].construct(from: jsValue) {
248+
// Use items
249+
}
250+
251+
// Dictionaries (if values conform to ConstructibleFromJSValue)
252+
if let data = [String: Int].construct(from: jsValue) {
253+
// Use data
254+
}
255+
256+
// For complex Decodable types, use JSValueDecoder
257+
struct User: Decodable {
258+
let name: String
259+
let age: Int
260+
}
261+
let user = try JSValueDecoder().decode(User.self, from: jsValue)
262+
```
263+
264+
## Pass Swift Closures back to JavaScript
265+
266+
```swift
267+
public class JSClosure: JSObject, JSClosureProtocol {
268+
public init(_ body: @escaping (sending [JSValue]) -> JSValue)
269+
public static func async(
270+
priority: TaskPriority? = nil,
271+
_ body: @escaping (sending [JSValue]) async throws(JSException) -> JSValue
272+
) -> JSClosure
273+
public func release()
274+
}
275+
276+
public class JSOneshotClosure: JSObject, JSClosureProtocol {
277+
public init(_ body: @escaping (sending [JSValue]) -> JSValue)
278+
public static func async(
279+
priority: TaskPriority? = nil,
280+
_ body: @escaping (sending [JSValue]) async throws(JSException) -> JSValue
281+
) -> JSOneshotClosure
282+
public func release()
283+
}
284+
```
285+
286+
**Example**
287+
288+
```swift
289+
let document = JSObject.global.document.object!
290+
let console = JSObject.global.console.object!
291+
292+
// Persistent closure - keep reference while JavaScript can call it
293+
let button = document.createElement!("button").object!
294+
let handler = JSClosure { args in
295+
console.log!("Clicked", args[0])
296+
return .undefined
297+
}
298+
button.addEventListener!("click", handler)
299+
300+
// One-shot closure - automatically released after first call
301+
button.addEventListener!(
302+
"click",
303+
JSOneshotClosure { _ in
304+
console.log!("One-off click")
305+
return .undefined
306+
},
307+
["once": true]
308+
)
309+
310+
// Async closure - bridges Swift async to JavaScript Promise
311+
let asyncHandler = JSClosure.async { _ async throws(JSException) -> JSValue in
312+
try! await Task.sleep(nanoseconds: 1_000_000)
313+
console.log!("Async closure finished")
314+
return .undefined
315+
}
316+
button.addEventListener!("async", asyncHandler)
317+
```
318+
319+
## Promises and `async/await`
320+
321+
```swift
322+
public final class JSPromise: JSBridgedClass {
323+
public init(unsafelyWrapping object: JSObject)
324+
public init(resolver: @escaping (@escaping (Result) -> Void) -> Void)
325+
public static func async(
326+
body: @escaping () async throws(JSException) -> Void
327+
) -> JSPromise
328+
public static func async(
329+
body: @escaping () async throws(JSException) -> JSValue
330+
) -> JSPromise
331+
332+
public enum Result {
333+
case success(JSValue)
334+
case failure(JSValue)
335+
}
336+
337+
// Available when JavaScriptEventLoop is linked
338+
public var value: JSValue { get async throws(JSException) }
339+
public var result: Result { get async }
340+
}
341+
```
342+
343+
**Example**
344+
345+
```swift
346+
import JavaScriptEventLoop
347+
348+
JavaScriptEventLoop.installGlobalExecutor()
349+
350+
let console = JSObject.global.console.object!
351+
let fetch = JSObject.global.fetch.object!
352+
353+
// Wrap existing JavaScript Promise and await from Swift
354+
Task {
355+
do {
356+
let response = try await JSPromise(
357+
unsafelyWrapping: fetch("https://example.com").object!
358+
).value
359+
console.log!("Fetched data", response)
360+
} catch let error as JSException {
361+
console.error!("Fetch failed", error.thrownValue)
362+
}
363+
}
364+
365+
// Expose Swift async work to JavaScript
366+
let swiftPromise = JSPromise.async {
367+
try await Task.sleep(nanoseconds: 1_000_000_000)
368+
return .string("Swift async complete")
369+
}
370+
```
371+
372+
- Wrap existing promise-returning APIs with ``JSPromise/init(unsafelyWrapping:)``.
373+
- Use `JSPromise.async(body:)` (with `Void` or `JSValue` return type) to expose Swift `async/await` work to JavaScript callers.
374+
- To await JavaScript `Promise` from Swift, import `JavaScriptEventLoop`, call `JavaScriptEventLoop.installGlobalExecutor()` early, and use the `value` property.
375+
- The `value` property suspends until the promise resolves or rejects, rethrowing rejections as ``JSException``.
376+

Sources/JavaScriptKit/Documentation.docc/Documentation.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ Check out the [examples](https://github.com/swiftwasm/JavaScriptKit/tree/main/Ex
5151

5252
### Articles
5353

54+
- <doc:JavaScript-Interop-Cheat-Sheet>
5455
- <doc:Package-Output-Structure>
5556
- <doc:Deploying-Pages>
5657
- <doc:JavaScript-Environment-Requirements>

0 commit comments

Comments
 (0)