PONY λ M2 Modula-2

JavaScript.CodeCompared.To/Swift

An interactive executable cheatsheet comparing JavaScript and Swift

JavaScript (ES2025) Swift 6.3
Variables & Constants
let vs. var — the role reversal
let mutableCount = 0; mutableCount = 1; // let is mutable in JavaScript const maxValue = 100; // const is immutable console.log(mutableCount); console.log(maxValue);
var mutableCount = 0 mutableCount = 1 // var is mutable in Swift let maxValue = 100 // let is immutable print(mutableCount) print(maxValue)
Swift's let and var are the reverse of JavaScript's. In Swift, let declares an immutable constant (like JavaScript's const) and var declares a mutable variable (like JavaScript's let). The Swift compiler enforces immutability at compile time, so using let wherever possible is idiomatic Swift.
Type annotations
const username = "Alice"; // inferred as string at runtime const age = 30; // inferred as number const active = true; // inferred as boolean console.log(username, age, active);
let username: String = "Alice" // explicit annotation let age: Int = 30 let active: Bool = true print(username, age, active)
Swift type annotations use a colon after the variable name — the same syntax as TypeScript. Unlike JavaScript's single number type, Swift distinguishes between integer types (Int, Int32, Int64) and floating-point types (Float, Double). The annotation is optional when Swift can infer the type from the value.
Type inference
// JavaScript: types determined at runtime, can change let value = 42; value = "now a string"; // valid in JavaScript console.log(typeof value);
// Swift: types inferred at compile time, fixed forever var value = 42 // Int inferred from literal // value = "now a string" // ❌ compile error — type mismatch print(value) print(type(of: value)) // Int
Swift's type inference happens at compile time. Once Swift infers a variable's type from its initial value, that type is permanent — you cannot assign a value of a different type later. This eliminates a whole class of JavaScript bugs where a variable silently changes from a number to a string.
Tuple destructuring and swapping
const [first, second] = [10, 20]; let x = 1, y = 2; [x, y] = [y, x]; // swap without temp variable console.log(first, second, x, y);
let (first, second) = (10, 20) var x = 1 var y = 2 (x, y) = (y, x) // swap without temp variable print(first, second, x, y)
Swift uses parentheses for tuple destructuring, whereas JavaScript uses bracket destructuring for arrays. Tuple swapping works in both languages without a temporary variable. Swift tuples can also have named elements: let point = (x: 10, y: 20), accessible as point.x.
Types & Type Inference
Numeric types
const wholeNumber = 42; const decimal = 3.14; // JavaScript has one numeric type for all numbers console.log(typeof wholeNumber); // "number" console.log(typeof decimal); // "number"
let wholeNumber: Int = 42 let decimal: Double = 3.14 // Swift has separate integer and floating-point types print(type(of: wholeNumber)) // Int print(type(of: decimal)) // Double
JavaScript uses a single 64-bit float for all numbers, so integers and decimals share a type. Swift distinguishes between Int (integer, platform-native size) and Double (64-bit float). Mixing the two requires explicit conversion: Double(wholeNumber). This prevents the silent precision loss that JavaScript can introduce when integers exceed 2^53.
Explicit type conversion
const text = "42"; const number = Number(text); const back = String(number); console.log(number + 1); console.log(back + "!");
let text = "42" let number = Int(text) ?? 0 // Int("42") returns Int? — nil if invalid let back = String(number) print(number + 1) print(back + "!")
Swift never implicitly converts between types — all conversions are explicit. Converting a string to an integer returns an Optional<Int> (written Int?) because the string might not be a valid number. The ?? 0 provides a default value when conversion fails. JavaScript's Number() returns NaN for invalid strings, which silently propagates through arithmetic.
Type checking
const value = "hello"; console.log(typeof value === "string"); // true console.log(value instanceof String); // false — primitives differ from objects console.log(42 instanceof Number); // false
let value: Any = "hello" print(value is String) // true if let text = value as? String { print("It is a string: \(text)") }
Swift's is operator checks type membership and returns a Bool. The as? operator attempts a conditional cast, returning an Optional that is nil if the cast fails. This avoids JavaScript's typeof/instanceof split: in JavaScript, primitive "hello" and its wrapper new String("hello") are different types; in Swift, String is one unified type with no wrapper distinction.
Boolean logic
const isValid = true; const isEmpty = false; console.log(isValid && !isEmpty); console.log(isValid || isEmpty); console.log(1 == true); // true in JavaScript — coercion! console.log(0 == false); // true in JavaScript — coercion!
let isValid = true let isEmpty = false print(isValid && !isEmpty) print(isValid || isEmpty) // 1 == true would be a compile error — no implicit coercion print(isValid == true) // explicit comparison, always fine
Swift's strong typing means there is no implicit coercion between integers and booleans. In JavaScript, 1 == true evaluates to true due to type coercion; in Swift, comparing an Int to a Bool is a compile error. The === strict-equality operator that JavaScript developers use to avoid coercion has no equivalent in Swift — it is simply unnecessary because Swift's == always compares values of the same type.
Strings
String length and case
const greeting = "Hello, World!"; console.log(greeting.length); console.log(greeting.toUpperCase()); console.log(greeting.toLowerCase());
let greeting = "Hello, World!" print(greeting.count) print(greeting.uppercased()) print(greeting.lowercased())
Swift uses .count instead of .length. Methods like uppercased() use parentheses — they are method calls, not properties like JavaScript's toUpperCase. Swift's String counts Unicode characters correctly, so "café".count returns 4, not 5 (unlike JavaScript, which counts UTF-16 code units and returns 5 for the same string).
String interpolation
const name = "Alice"; const age = 30; const message = `Hello, ${name}! You are ${age} years old.`; console.log(message);
let name = "Alice" let age = 30 let message = "Hello, \(name)! You are \(age) years old." print(message)
Swift uses \(expression) inside regular double-quoted strings for interpolation — no backtick syntax. The expression inside \() can be any Swift expression, including function calls and arithmetic: "Result: \(10 * 5)". JavaScript uses ${expression} inside backtick template literals. Both languages evaluate the expression and convert the result to a string automatically.
Multi-line strings
const poem = `Roses are red, Violets are blue, Swift is typed, And JavaScript too.`; console.log(poem);
let poem = """ Roses are red, Violets are blue, Swift is typed, And JavaScript too. """ print(poem)
Swift's multi-line strings use triple double-quotes """. The opening """ must be followed by a newline, and the closing """ must be on its own line. Indentation of the closing delimiter sets the baseline for stripping leading whitespace from each line — useful for aligning multi-line strings in indented code. JavaScript uses backtick template literals for multi-line strings.
String methods
const text = " hello, world "; console.log(text.trim()); console.log("hello".includes("ell")); console.log(String("ha").repeat(3));
import Foundation // required for trimmingCharacters let text = " hello, world " print(text.trimmingCharacters(in: .whitespaces)) print("hello".contains("ell")) print(String(repeating: "ha", count: 3))
Swift's string methods have more descriptive names than JavaScript's. trimmingCharacters(in: .whitespaces) replaces trim() — it requires import Foundation because it is a Foundation extension on String. contains(_:) and String(repeating:count:) are part of the Swift standard library and need no import.
String concatenation
const first = "Hello"; const second = " World"; const combined = first + second; console.log(combined); let growing = "Start"; growing += " middle"; growing += " end"; console.log(growing);
let first = "Hello" let second = " World" let combined = first + second print(combined) var growing = "Start" growing += " middle" growing += " end" print(growing)
String concatenation with + and += works the same in both languages. In Swift, only var strings can use += — using += on a let constant is a compile error. The + operator creates a new string and works with both let and var.
Optionals
Optional basics
let username = null; // JavaScript uses null let nickname = undefined; // or undefined — two absence values console.log(username === null); console.log(nickname === undefined);
var username: String? = nil // Optional<String> — one absence value var nickname: String? = nil print(username == nil) print(nickname == nil) // username = 42 ❌ compile error — wrong type
Swift has a single absence value, nil, and the type system tracks whether a value can be absent. A String? (Optional String) holds either a String value or nil. A plain String can never be nil — the compiler enforces this. JavaScript's null and undefined can appear in any variable without type-system tracking.
Optional binding with if let
function greet(name) { if (name !== null && name !== undefined) { console.log("Hello, " + name); } else { console.log("Hello, stranger"); } } greet("Alice"); greet(null);
func greet(name: String?) { if let actualName = name { print("Hello, \(actualName)") } else { print("Hello, stranger") } } greet(name: "Alice") greet(name: nil)
if let unwraps an optional into a non-optional binding that is only in scope inside the if block. If the optional is nil, the else branch executes. Swift 5.7+ allows shorthand if let name when the binding name matches the variable. JavaScript null-checking is manual and not tracked by the compiler — Swift's if let makes the intent explicit and compiler-checked.
Nil coalescing (??)
const setting = null; const value = setting ?? "default"; console.log(value); const score = null; const display = "Score: " + (score ?? 0); console.log(display);
let setting: String? = nil let value = setting ?? "default" print(value) let score: Int? = nil let display = "Score: \(score ?? 0)" print(display)
Both JavaScript and Swift use the ?? nil/null coalescing operator with identical syntax and semantics. The right side is the default value used when the left side is nil/null. Swift's version is type-safe — the compiler verifies that the right-hand side matches the unwrapped type of the optional.
Optional chaining (?.)
const user = null; const city = user?.address?.city ?? "Unknown"; console.log(city); const user2 = { address: { city: "Tokyo" } }; console.log(user2?.address?.city ?? "Unknown");
struct Address { let city: String } struct User { let address: Address? } let user: User? = nil print(user?.address?.city ?? "Unknown") let user2 = User(address: Address(city: "Tokyo")) print(user2.address?.city ?? "Unknown")
Both languages use ?. for optional chaining, which short-circuits to nil/undefined if any link in the chain is absent. In Swift, chaining through any optional makes the entire result an optional. Swift's optional chaining is type-safe — the compiler knows the exact result type of every chain.
Guard let (early exit)
function processUser(user) { if (!user) return; if (!user.name) return; console.log("Processing: " + user.name); } processUser({ name: "Alice" }); processUser(null);
func processUser(user: [String: String]?) { guard let unwrapped = user else { return } guard let name = unwrapped["name"] else { return } print("Processing: \(name)") } processUser(user: ["name": "Alice"]) processUser(user: nil)
Swift's guard let is the idiomatic early-exit pattern. Unlike if let, the unwrapped binding is available in the enclosing scope after the guard statement. The else clause must exit the scope via return, throw, break, or continue. This prevents deeply nested if let chains and keeps the "happy path" unindented.
Arrays
Array creation and access
const fruits = ["apple", "banana", "cherry"]; console.log(fruits[0]); console.log(fruits.length); console.log(fruits[fruits.length - 1]);
let fruits = ["apple", "banana", "cherry"] print(fruits[0]) print(fruits.count) print(fruits.last!) // .last returns Optional — ! is safe here (non-empty array)
Swift arrays use zero-based indexing and .count instead of .length. Swift provides .first and .last properties that return optionals, which is safer than index-based access — accessing an out-of-bounds index crashes at runtime with no graceful error. When you know the array is non-empty, the ! force-unwrap is safe; prefer ?? "fallback" when in doubt.
Adding and removing elements
const items = ["a", "b", "c"]; items.push("d"); items.unshift("z"); const last = items.pop(); const first = items.shift(); console.log(items, last, first);
var items = ["a", "b", "c"] items.append("d") items.insert("z", at: 0) let last = items.removeLast() let first = items.removeFirst() print(items, last, first)
Swift's array mutation methods have descriptive argument labels: append instead of push, insert(_:at:) instead of unshift. Removing elements returns the removed value. Swift arrays require var to be mutable — a let array is fully immutable, and any mutation attempt is a compile error.
Iteration
const numbers = [1, 2, 3, 4, 5]; for (const number of numbers) { console.log(number); } numbers.forEach((number, index) => { console.log(index + ": " + number); });
let numbers = [1, 2, 3, 4, 5] for number in numbers { print(number) } for (index, number) in numbers.enumerated() { print("\(index): \(number)") }
Swift's for-in loop is the idiomatic way to iterate sequences. The enumerated() method pairs each element with its index — note that Swift's enumeration yields (index, element) in that order, which is the same as JavaScript's forEach callback order (element, index) reversed. Use .indices if you only need the indices.
map, filter, reduce
const numbers = [1, 2, 3, 4, 5]; const doubled = numbers.map(n => n * 2); const evens = numbers.filter(n => n % 2 === 0); const total = numbers.reduce((sum, n) => sum + n, 0); console.log(doubled, evens, total);
let numbers = [1, 2, 3, 4, 5] let doubled = numbers.map { number in number * 2 } let evens = numbers.filter { number in number % 2 == 0 } let total = numbers.reduce(0) { accumulator, number in accumulator + number } print(doubled, evens, total)
Swift's higher-order functions use trailing closure syntax with { }. Closure arguments are named explicitly (number in) or implicitly with $0, $1 — shorthand: numbers.map { $0 * 2 }. The concepts map directly from JavaScript's Array.prototype.map/filter/reduce. Swift's reduce(_:_:) puts the initial value as the first argument rather than the last.
Sorting
const words = ["banana", "apple", "cherry"]; const sorted = [...words].sort(); const byLength = [...words].sort((a, b) => a.length - b.length); console.log(sorted); console.log(byLength);
let words = ["banana", "apple", "cherry"] let sorted = words.sorted() let byLength = words.sorted { word1, word2 in word1.count < word2.count } print(sorted) print(byLength)
Swift's sorted() returns a new sorted array without mutating the original (use sort() on a var array to sort in place). The sort closure returns Booltrue if the first argument should come before the second. This differs from JavaScript's comparator, which returns a negative/zero/positive number. Think of it as "should first come before second?"
Dictionaries
Dictionary creation
const scores = { alice: 95, bob: 87, charlie: 92 }; scores.dave = 88; console.log(scores.alice); console.log(scores["bob"]);
var scores = ["alice": 95, "bob": 87, "charlie": 92] scores["dave"] = 88 print(scores["alice"] as Any) // Optional(95) print(scores["alice"]!) // 95 (force-unwrapped)
Swift dictionaries use [KeyType: ValueType] syntax. Unlike JavaScript object properties, Swift dictionary key access always returns an Optional — if the key does not exist, the result is nil rather than undefined. You must unwrap the optional to use the value. Setting a key with a subscript adds or updates it.
Safe key access
const scores = { alice: 95 }; const unknown = scores.bob; // undefined const safe = scores.bob ?? 0; // 0 console.log(unknown, safe);
let scores = ["alice": 95] let unknown: Int? = scores["bob"] // nil — key not found let safe = scores["bob"] ?? 0 // 0 print(unknown as Any, safe)
Dictionary subscript access in Swift always returns an optional. This forces you to handle the absent-key case explicitly. JavaScript returns undefined for missing keys, which is silently usable in arithmetic (undefined + 1 is NaN) and can propagate bugs. Swift's optionals make absence visible in the type system and force a decision at each use site.
Iterating key-value pairs
const config = { host: "localhost", port: "8080", debug: "true" }; for (const [key, value] of Object.entries(config)) { console.log(key + ": " + value); }
let config = ["host": "localhost", "port": "8080", "debug": "true"] for (key, value) in config { print("\(key): \(value)") }
Swift's for (key, value) in dictionary directly destructures key-value pairs — no equivalent of JavaScript's Object.entries() is needed. The iteration order is not guaranteed in either language (JavaScript objects maintain insertion order only from ES2015+). Swift also provides .keys and .values for iterating only one dimension.
Dictionary operations
const inventory = { apples: 5, bananas: 3 }; console.log(Object.keys(inventory)); console.log(Object.values(inventory)); console.log("apples" in inventory); delete inventory.bananas; console.log(inventory);
var inventory = ["apples": 5, "bananas": 3] print(Array(inventory.keys)) print(Array(inventory.values)) print(inventory.keys.contains("apples")) inventory.removeValue(forKey: "bananas") print(inventory)
Swift dictionaries provide .keys and .values as collections — wrap in Array(...) to get a concrete array. Checking for a key uses .keys.contains(_:) instead of JavaScript's in operator. Removing a key uses removeValue(forKey:) or setting the subscript to nil: inventory["bananas"] = nil.
Control Flow
if / else
const temperature = 22; if (temperature > 30) { console.log("Hot"); } else if (temperature > 15) { console.log("Comfortable"); } else { console.log("Cold"); }
let temperature = 22 if temperature > 30 { print("Hot") } else if temperature > 15 { print("Comfortable") } else { print("Cold") }
Swift if statements look nearly identical to JavaScript's, but parentheses around the condition are optional and conventionally omitted. The braces are required in Swift — there are no single-line braceless if statements. Swift 5.9+ added if expressions that return a value: let label = if temperature > 30 { "Hot" } else { "Cool" }.
Ternary operator
const score = 75; const grade = score >= 60 ? "Pass" : "Fail"; console.log(grade);
let score = 75 let grade = score >= 60 ? "Pass" : "Fail" print(grade)
The ternary operator condition ? trueValue : falseValue is identical in Swift and JavaScript.
For loops with ranges
for (let i = 0; i < 5; i++) { console.log(i); } for (const item of ["a", "b", "c"]) { console.log(item); }
for index in 0..<5 { print(index) } for item in ["a", "b", "c"] { print(item) }
Swift uses ranges for numeric loops: 0..<5 is a half-open range (0 through 4), and 0...5 is a closed range (0 through 5). Swift has no three-part for (init; condition; increment) loop — use ranges, while, or sequence iteration instead. The for-in loop works on any type that conforms to Sequence.
While and repeat-while
let count = 0; while (count < 3) { console.log(count); count++; } let x = 0; do { console.log("do:", x); x++; } while (x < 2);
var count = 0 while count < 3 { print(count) count += 1 // no ++ in Swift } var x = 0 repeat { print("repeat:", x) x += 1 } while x < 2
Swift uses repeat { } while where JavaScript uses do { } while. The ++ and -- operators were removed in Swift 3 to eliminate ambiguity between prefix and postfix behavior — use += 1 and -= 1 instead.
Switch — no fallthrough
const direction = "north"; switch (direction) { case "north": console.log("Going up"); break; case "south": console.log("Going down"); break; default: console.log("Going sideways"); }
let direction = "north" switch direction { case "north": print("Going up") case "south": print("Going down") default: print("Going sideways") }
Swift's switch does not fall through by default — no break needed. Each case executes its block and stops. In JavaScript, omitting break causes the next case to execute (fallthrough). Swift's switch must be exhaustive — the compiler verifies that all cases are covered by cases or a default. Add fallthrough explicitly if fallthrough behavior is intentional.
Guard for early exit
function processOrder(quantity) { if (quantity <= 0) return; if (quantity > 100) return; console.log("Processing order for " + quantity + " items"); } processOrder(50); processOrder(-1);
func processOrder(quantity: Int) { guard quantity > 0 else { return } guard quantity <= 100 else { return } print("Processing order for \(quantity) items") } processOrder(quantity: 50) processOrder(quantity: -1)
guard is Swift's idiomatic early-exit construct. It reads as "guard that this condition holds, else exit." Unlike if let, any bindings created in a guard let are available in the enclosing scope after the guard statement. Guard statements improve readability by keeping the happy path at the leftmost indent level.
Functions & Closures
Function definition
function greet(name) { return "Hello, " + name + "!"; } console.log(greet("World"));
func greet(name: String) -> String { "Hello, \(name)!" // single-expression functions can omit return } print(greet(name: "World"))
Swift functions declare parameter types and return type explicitly. Callers use argument labels by default — greet(name: "World") not greet("World"). This makes call sites self-documenting. A single-expression function body can omit return. Use an underscore as the external label to allow unlabeled arguments: func greet(_ name: String)greet("Alice").
Argument labels
function move(x, y, speed) { console.log("Moving to (" + x + ", " + y + ") at speed " + speed); } move(10, 20, 5);
func move(to x: Int, and y: Int, at speed: Int) { print("Moving to (\(x), \(y)) at speed \(speed)") } move(to: 10, and: 20, at: 5)
Swift functions can have two names per parameter: an external label (used by callers) and an internal name (used inside the function body). This creates self-documenting call sites — move(to: 10, and: 20, at: 5) reads like a sentence. JavaScript has no equivalent; argument purpose must be inferred from order or from object parameter patterns.
Default parameter values
function greet(name, greeting = "Hello") { console.log(greeting + ", " + name + "!"); } greet("Alice"); greet("Bob", "Hi");
func greet(name: String, greeting: String = "Hello") { print("\(greeting), \(name)!") } greet(name: "Alice") greet(name: "Bob", greeting: "Hi")
Swift default parameter values work the same as JavaScript's. Parameters with defaults can be omitted at the call site. Because Swift call sites use argument labels, skipping a default parameter is always unambiguous — you reference the next parameter by name rather than by position.
Multiple return values
function minMax(numbers) { return { min: Math.min(...numbers), max: Math.max(...numbers) }; } const { min, max } = minMax([3, 1, 4, 1, 5, 9]); console.log(min, max);
func minMax(numbers: [Int]) -> (minimum: Int, maximum: Int) { return (numbers.min() ?? 0, numbers.max() ?? 0) } let (minimum, maximum) = minMax(numbers: [3, 1, 4, 1, 5, 9]) print(minimum, maximum)
Swift functions return multiple values as a named tuple — no need to wrap them in an object like JavaScript. The named elements (.minimum, .maximum) serve the same role as JavaScript's destructured property names. Tuples are lightweight value types, not heap-allocated objects, so there is no performance overhead.
Closures
const double = (n) => n * 2; const numbers = [1, 2, 3]; console.log(numbers.map(double));
let double = { (number: Int) -> Int in number * 2 } let numbers = [1, 2, 3] print(numbers.map(double))
Swift closures are enclosed in { } with the signature before in. Type inference often allows a shorter form: { number in number * 2 }. Shorthand argument names $0, $1 eliminate naming altogether: numbers.map { $0 * 2 }. Swift closures capture values from the surrounding scope like JavaScript arrow functions do.
Trailing closures
const numbers = [1, 2, 3, 4, 5]; const evens = numbers.filter(n => n % 2 === 0); const result = numbers.reduce((sum, n) => sum + n, 0); console.log(evens, result);
let numbers = [1, 2, 3, 4, 5] let evens = numbers.filter { number in number % 2 == 0 } let result = numbers.reduce(0) { accumulator, number in accumulator + number } print(evens, result)
When the last argument of a function is a closure, Swift allows moving it outside the parentheses. This "trailing closure" syntax closely resembles Ruby's block syntax. If the closure is the only argument, the parentheses are omitted entirely: numbers.filter { $0 % 2 == 0 }. JavaScript arrow functions inside () serve the same syntactic role.
Structs & Classes
Struct — value type
class Point { constructor(x, y) { this.x = x; this.y = y; } } const point1 = new Point(3, 4); const point2 = point1; // reference — same object point2.x = 99; console.log(point1.x, point2.x); // both 99
struct Point { var x: Int var y: Int } var point1 = Point(x: 3, y: 4) var point2 = point1 // copy — independent value point2.x = 99 print(point1.x, point2.x) // 3 99 — separate copies
Swift structs are value types — assignment copies the entire value. Mutating one copy does not affect others. JavaScript has no value type equivalent; all objects are reference types. In Swift, structs are preferred for simple data containers because they are thread-safe by default, have no identity-related surprises, and make ownership semantics clear.
Class — reference type
class Counter { #count = 0; increment() { this.#count++; } get value() { return this.#count; } } const counter = new Counter(); counter.increment(); counter.increment(); console.log(counter.value);
class Counter { private var count = 0 func increment() { count += 1 } var value: Int { count } } let counter = Counter() counter.increment() counter.increment() print(counter.value)
Swift classes are reference types — multiple variables can hold a reference to the same instance, like JavaScript objects. Swift uses access control keywords (private, internal, public) instead of JavaScript's # prefix for private fields. Swift has no new keyword — types are instantiated by calling the type name like a function.
Automatic memberwise initializer
class Person { constructor(name, age) { this.name = name; this.age = age; } } const person = new Person("Alice", 30); console.log(person.name, person.age);
struct Person { let name: String let age: Int } let person = Person(name: "Alice", age: 30) print(person.name, person.age)
Swift structs get a free memberwise initializer automatically — no init method needed for the common case. The initializer Person(name: "Alice", age: 30) is generated by the compiler. Classes require an explicit init. This is one reason Swift favors structs: less boilerplate for data-holding types.
Computed properties
class Circle { constructor(radius) { this.radius = radius; } get area() { return Math.PI * this.radius ** 2; } get circumference() { return 2 * Math.PI * this.radius; } } const circle = new Circle(5); console.log(circle.area); console.log(circle.circumference);
struct Circle { let radius: Double var area: Double { Double.pi * radius * radius } var circumference: Double { 2 * Double.pi * radius } } let circle = Circle(radius: 5) print(circle.area) print(circle.circumference)
Swift computed properties use shorthand { expression } for read-only getters — the same as JavaScript's get accessor. They look like stored properties to callers; no parentheses are needed. Swift's Double.pi is a static constant; JavaScript uses Math.PI. String formatting (String(format: "%.2f", value)) requires import Foundation in Swift.
Protocols
Protocol definition
// JavaScript uses duck typing — no interface enforcement function makeSound(animal) { if (typeof animal.speak !== "function") { throw new Error("Not an animal"); } animal.speak(); } makeSound({ speak: () => console.log("Woof!") });
protocol Animal { var name: String { get } func speak() } struct Dog: Animal { let name: String func speak() { print("\(name) says: Woof!") } } let dog = Dog(name: "Rex") dog.speak()
Swift protocols define a contract that types must fulfill — similar to TypeScript interfaces but checked at compile time, not runtime. A type adopts a protocol by listing it after : in its declaration. Protocol requirements include property requirements (var name: String { get }) and method requirements. JavaScript relies on duck typing, where anything with a speak method qualifies — no compile-time check.
Protocol default implementations
// JavaScript mixes shared behavior via class inheritance or mixins class Animal { describe() { return "I am " + this.name; } } class Cat extends Animal { constructor() { super(); this.name = "Whiskers"; } } console.log(new Cat().describe());
protocol Describable { var name: String { get } } extension Describable { func describe() -> String { "I am \(name)" } } struct Cat: Describable { let name: String } print(Cat(name: "Whiskers").describe())
Protocol extensions provide default implementations for protocol methods. Types that adopt the protocol get the implementation for free, but can override it. Unlike class inheritance, protocol extensions work with value types (structs and enums). This is Swift's primary composition mechanism — "protocol-oriented programming" — and it avoids the fragile base class problem of deep inheritance hierarchies.
Multiple protocol conformance
// JavaScript simulates multiple interfaces with duck typing function describeVehicle(vehicle) { console.log("Wheels:", vehicle.wheels, "Engine:", vehicle.engineCC + "cc"); } describeVehicle({ wheels: 4, engineCC: 2000 });
protocol Wheeled { var wheels: Int { get } } protocol Motorized { var engineCC: Int { get } } struct Car: Wheeled, Motorized { let wheels = 4 let engineCC = 2000 } let car = Car() print("Wheels:", car.wheels, "Engine:", String(car.engineCC) + "cc")
A Swift type can conform to multiple protocols, listed with commas. This is Swift's alternative to multiple inheritance — composing behavior from several independent contracts. Protocol conformance is checked at compile time, so a type that fails to implement a required method produces a compile error. JavaScript achieves similar results through duck typing or explicit mixin patterns.
Enums
Simple enum
const Direction = Object.freeze({ North: "north", South: "south", East: "east", West: "west" }); const heading = Direction.North; console.log(heading);
enum Direction { case north, south, east, west } let heading = Direction.north print(heading)
Swift enums are first-class types, not aliases for strings or numbers. The compiler knows all cases at compile time, enabling exhaustive switch checking. Enum cases use lowercase by convention. JavaScript commonly uses frozen objects or string literals to simulate enums, with no compiler enforcement that all cases are handled.
Enum with raw values
const Status = { Active: 1, Inactive: 0, Pending: 2 }; const current = Status.Active; console.log(current === 1);
enum Status: Int { case active = 1 case inactive = 0 case pending = 2 } let current = Status.active print(current.rawValue) print(Status(rawValue: 1) == Status.active) // round-trip
Swift enums with raw values (e.g., : Int, : String) associate a backing value with each case. The .rawValue property accesses it. The failable initializer Status(rawValue: 1) converts from raw value back to an enum case, returning an Optional. String-backed enums are common for JSON serialization.
Enum with associated values
// JavaScript has no built-in associated values — uses tagged objects const result1 = { type: "success", value: 42 }; const result2 = { type: "failure", message: "Not found" }; if (result1.type === "success") { console.log("Success:", result1.value); }
enum Outcome { case success(Int) case failure(String) } let result1 = Outcome.success(42) let result2 = Outcome.failure("Not found") switch result1 { case .success(let value): print("Success:", value) case .failure(let message): print("Failure:", message) }
Swift enums can carry different data with each case — "associated values." This is one of Swift's most distinctive features, enabling algebraic data types. TypeScript's discriminated unions achieve similar patterns, but Swift's version is more concise and fully compiler-checked. There is no JavaScript equivalent — tagged object conventions are manually enforced.
Enum methods and properties
const colors = { Red: { hex: "#FF0000" }, Green: { hex: "#00FF00" }, }; function describeColor(color) { return color.hex; } console.log(describeColor(colors.Red));
enum Color: String { case red = "#FF0000" case green = "#00FF00" case blue = "#0000FF" var description: String { "\(self) (\(rawValue))" } } print(Color.red.description) print(Color.green.rawValue)
Swift enums can have computed properties and methods just like structs and classes. They can also conform to protocols, have static properties, and even have initializers. This makes Swift enums far more powerful than their counterparts in most languages — they are algebraic data types, not just named constants.
Error Handling
Throwing and catching errors
function divide(a, b) { if (b === 0) throw new Error("Division by zero"); return a / b; } try { console.log(divide(10, 2)); console.log(divide(10, 0)); } catch (error) { console.log("Error:", error.message); }
enum MathError: Error { case divisionByZero } func divide(numerator: Int, denominator: Int) throws -> Int { if denominator == 0 { throw MathError.divisionByZero } return numerator / denominator } do { print(try divide(numerator: 10, denominator: 2)) print(try divide(numerator: 10, denominator: 0)) } catch MathError.divisionByZero { print("Error: division by zero") }
Swift's error handling requires throws in the function signature and try at every call site, making error propagation visible in the code — there are no silent exceptions. Errors conform to the Error protocol. Unlike JavaScript's catch (error) which catches any thrown value, Swift's catch can pattern-match on specific error types.
try? and try! — optional error handling
function parseJSON(text) { try { return JSON.parse(text); } catch { return null; } } const good = parseJSON('{"key": 1}'); const bad = parseJSON("not json"); console.log(good !== null, bad === null);
enum ParseError: Error { case invalid } func parseNumber(_ text: String) throws -> Int { guard let n = Int(text) else { throw ParseError.invalid } return n } let good = try? parseNumber("42") // Optional(42) let bad = try? parseNumber("oops") // nil print(good as Any, bad as Any)
try? converts a throwing call's result into an Optional — nil on any error, the value otherwise. This is the Swift equivalent of wrapping a call in try/catch and returning null on failure. try! asserts that no error will be thrown and crashes at runtime if one occurs — use it only when failure is logically impossible.
Typed error cases
class NetworkError extends Error { constructor(message, code) { super(message); this.code = code; } } try { throw new NetworkError("Timeout", 408); } catch (error) { if (error instanceof NetworkError) { console.log(error.message, error.code); } }
enum NetworkError: Error { case timeout(code: Int) case notFound case serverError(message: String) } do { throw NetworkError.timeout(code: 408) } catch NetworkError.timeout(let code) { print("Timeout, code:", code) } catch { print("Other error:", error) }
Swift error enums with associated values carry structured data alongside the error case. The catch clause can pattern-match on specific cases and bind their associated values. This is more expressive than JavaScript's class-based errors, where you must inspect properties after an instanceof check. The final bare catch is a catch-all, equivalent to JavaScript's untyped catch.
Pattern Matching
Switch on tuples
const x = 0, y = 5; if (x === 0 && y === 0) { console.log("Origin"); } else if (x === 0) { console.log("On Y axis"); } else if (y === 0) { console.log("On X axis"); } else { console.log("Elsewhere"); }
let x = 0 let y = 5 switch (x, y) { case (0, 0): print("Origin") case (0, _): print("On Y axis") case (_, 0): print("On X axis") default: print("Elsewhere") }
Swift's switch can match on tuples, using _ as a wildcard for elements to ignore. This is far more expressive than JavaScript's switch, which can only match a single value. Pattern matching in Swift is exhaustive — the compiler verifies all cases are covered.
Switch with ranges
const score = 75; let grade; if (score >= 90) grade = "A"; else if (score >= 80) grade = "B"; else if (score >= 70) grade = "C"; else grade = "F"; console.log(grade);
let score = 75 let grade: String switch score { case 90...100: grade = "A" case 80..<90: grade = "B" case 70..<80: grade = "C" default: grade = "F" } print(grade)
Swift's switch accepts ranges as case patterns. ... is a closed range (inclusive upper bound), ..< is a half-open range (exclusive upper bound). This is more readable than a chain of if/else if for range checks. Swift 5.9+ allows this same pattern as a switch expression that produces a value directly.
Matching by type
function describe(value) { if (typeof value === "string") { console.log('String: "' + value + '"'); } else if (typeof value === "number") { console.log("Number:", value); } else { console.log("Other"); } } describe("hello"); describe(42);
func describe(value: Any) { switch value { case let text as String: print("String: \"\(text)\"") case let number as Int: print("Number:", number) default: print("Other") } } describe(value: "hello") describe(value: 42)
Swift's switch can perform type casting with as patterns. The let text as String pattern both checks the type and binds the value to a name. This replaces JavaScript's typeof and instanceof checks with a more concise and composable pattern.
Switch with where clause
const numbers = [1, -2, 3, -4, 5]; for (const number of numbers) { if (number > 0) { console.log("Positive:", number); } else { console.log("Non-positive:", number); } }
let numbers = [1, -2, 3, -4, 5] for number in numbers { switch number { case let n where n > 0: print("Positive:", n) default: print("Non-positive:", number) } }
A where clause adds an extra condition to a switch case. The pattern case let n where n > 0 matches any value greater than zero and binds it to n. This is useful when the match requires more than simple equality or range checking. JavaScript has no equivalent — only if/else chains.
Matching enum associated values
const messages = [ { type: "text", content: "Hello" }, { type: "image", url: "photo.jpg" }, { type: "text", content: "World" }, ]; for (const msg of messages) { if (msg.type === "text") console.log("Text:", msg.content); if (msg.type === "image") console.log("Image:", msg.url); }
enum Message { case text(String) case image(url: String) } let messages: [Message] = [.text("Hello"), .image(url: "photo.jpg"), .text("World")] for message in messages { switch message { case .text(let content): print("Text:", content) case .image(let url): print("Image:", url) } }
Switching on an enum with associated values extracts the payload cleanly. Because all enum cases are known to the compiler, the switch is exhaustive — no default case needed. Adding a new case later causes a compile error at every unhandled switch site, making refactoring safer than JavaScript's tagged-object approach.
Extensions
Extending built-in types
// JavaScript allows prototype extension (avoid in libraries) Array.prototype.second = function() { return this[1] ?? null; }; console.log([10, 20, 30].second());
extension Array { var second: Element? { count > 1 ? self[1] : nil } } let result = [10, 20, 30].second ?? 0 print(result)
Swift extensions add methods and computed properties to existing types — including built-in ones — without subclassing. Unlike JavaScript prototype mutation, Swift extensions are checked at compile time, don't affect unrelated code, and cannot remove or override existing methods. Extensions can also add protocol conformance to existing types.
Extension methods
function words(text) { return text.split(" "); } function wordCount(text) { return words(text).length; } console.log(words("Hello World Swift")); console.log(wordCount("Hello World Swift"));
extension String { func words() -> [String] { split(separator: " ").map(String.init) } var wordCount: Int { words().count } } print("Hello World Swift".words()) print("Hello World Swift".wordCount)
Swift extensions are the idiomatic way to add capabilities to existing types. This is safer than JavaScript prototype extension because extensions are scoped to the file or module, cannot remove existing methods, and are compiled into the type at compile time. The extended methods appear on the type everywhere in scope.
Adding protocol conformance via extension
class Temperature { constructor(celsius) { this.celsius = celsius; } display() { const fahrenheit = this.celsius * 9 / 5 + 32; console.log(this.celsius + "°C / " + fahrenheit + "°F"); } } new Temperature(100).display();
protocol Displayable { func display() } struct Temperature { let celsius: Double } extension Temperature: Displayable { func display() { let fahrenheit = celsius * 9 / 5 + 32 print("\(celsius)°C / \(fahrenheit)°F") } } Temperature(celsius: 100).display()
Extending a type to conform to a protocol in a separate extension keeps code organized by responsibility. The Temperature struct's core definition stays focused on its data; protocol conformances live in separate extensions. JavaScript has no equivalent — protocol conformance does not exist as a first-class concept, so behavior must be added directly to the class.
Extending Int for convenience
function isEven(n) { return n % 2 === 0; } function square(n) { return n * n; } console.log(isEven(4)); console.log(square(7)); console.log([1, 2, 3, 4, 5].filter(isEven).map(square));
extension Int { var isEven: Bool { self % 2 == 0 } var squared: Int { self * self } } print(4.isEven) print(7.squared) print([1, 2, 3, 4, 5].filter { $0.isEven }.map { $0.squared })
Swift allows extending even primitive types like Int to add domain-specific helpers. The result reads naturally at the call site — 4.isEven feels like a built-in property. This is the Swift equivalent of adding utility functions, but with the readability benefit of method call syntax without modifying any global namespace.