PONY λ M2 Modula-2

TypeScript.CodeCompared.To/Swift

An interactive executable cheatsheet comparing TypeScript and Swift

TypeScript 6.0 Swift 6.3
Hello World & Running
Hello, World
console.log("Hello, World!");
print("Hello, World!")
Swift scripts run as top-level executable statements, exactly like a TypeScript file executed directly with tsx — no class Main or entry-point boilerplate. print appends a newline automatically, just as console.log does.
String interpolation
const name = "Ada"; console.log(`Hello, ${name}!`);
let name = "Ada" print("Hello, \(name)!")
Swift interpolates inside ordinary double-quoted strings with \(expression) rather than TypeScript's backtick-delimited ${expression}. There is no separate template-literal syntax in Swift — any string literal supports interpolation.
Variables & Types
let vs. var — a false friend
let mutableCount = 0; mutableCount = 1; const fixedMaximum = 100; console.log(mutableCount, fixedMaximum);
var mutableCount = 0 mutableCount = 1 let fixedMaximum = 100 print(mutableCount, fixedMaximum)
This is the single most important trap for a TypeScript programmer: in TypeScript, let is mutable and const is fixed. In Swift, the meaning is reversed — let is the immutable, compiler-enforced binding, and var is the mutable one. Swift has no keyword that behaves like TypeScript's let.
Type annotations
const username: string = "Alice"; const age: number = 30; const active: boolean = true; console.log(username, age, active);
let username: String = "Alice" let age: Int = 30 let active: Bool = true print(username, age, active)
The colon-suffixed annotation syntax is nearly identical between the two languages. The difference is underneath: TypeScript's string/number/boolean are erased before the program runs, while Swift's String/Int/Bool remain real, checked types in the compiled binary.
Numeric types
const wholeNumber: number = 42; const fraction: number = 3.14; console.log(typeof wholeNumber, typeof fraction);
let wholeNumber: Int = 42 let fraction: Double = 3.14 print(type(of: wholeNumber), type(of: fraction))
TypeScript has one runtime numeric type, a 64-bit float, so number covers integers and decimals alike. Swift distinguishes Int from Double as genuinely different types; mixing them requires an explicit conversion such as Double(wholeNumber), and the compiler rejects silent mixing.
Tuple destructuring and swapping
const [first, second] = [10, 20]; let x = 1, y = 2; [x, y] = [y, x]; console.log(first, second, x, y);
let (first, second) = (10, 20) var x = 1 var y = 2 (x, y) = (y, x) print(first, second, x, y)
Swift destructures tuples with parentheses, where TypeScript destructures arrays with brackets. Both languages can swap two variables without a temporary. Swift tuples can also carry named elements, for example let point = (x: 10, y: 20), accessed as point.x.
Type System Philosophy
Structural vs. nominal typing
interface HasName { name: string; } function greet(entity: HasName): string { return `Hello, ${entity.name}`; } // Any object with a "name" property satisfies HasName — no declaration needed. console.log(greet({ name: "Ada", extra: true }));
protocol HasName { var name: String { get } } struct Person: HasName { let name: String } func greet(entity: HasName) -> String { "Hello, \(entity.name)" } print(greet(entity: Person(name: "Ada")))
This is the deepest philosophical difference between the two languages. TypeScript's typing is structural — any object with a matching shape satisfies an interface, with no explicit declaration required. Swift's typing is nominal — a type must explicitly state : HasName to conform to a protocol, even if its shape already matches. A Swift type with the right members but no declared conformance simply does not satisfy the protocol.
Erased vs. runtime types
interface Point { x: number; y: number; } function isPoint(value: unknown): value is Point { // No runtime trace of "Point" exists — this check is hand-written. return typeof value === "object" && value !== null && "x" in value && "y" in value; } console.log(isPoint({ x: 1, y: 2 }));
struct Point { let x: Double; let y: Double } func isPoint(_ value: Any) -> Bool { value is Point // Swift's type system genuinely exists at runtime } print(isPoint(Point(x: 1, y: 2))) print(isPoint("not a point"))
TypeScript types are completely erased when the compiler emits JavaScript — value is Point style guards are hand-written checks the developer must maintain, and nothing prevents a mismatched object from being cast with as Point. Swift's types exist in the compiled binary and are enforced by the runtime, so is performs a genuine type check rather than a convention the programmer promises to uphold.
"any" and "unknown" have no real Swift equivalent
let value: any = "hello"; value = 42; // any bypasses all checking — this reassignment compiles fine console.log(typeof value); // value.thisDoesNotExist(); // this would also compile, then throw a TypeError at // runtime — "any" defers every mistake to execution time
// Swift's closest analogue is "Any", but it still requires an explicit, // checked cast before you can call anything on the value — there is no // escape hatch that turns off the type system the way "any" does. let value: Any = "hello" if let text = value as? String { print(text.count) } // value.thisDoesNotExist() // compile error — Any has no members at all
TypeScript's any turns off type checking entirely — a value typed any can be reassigned to any type and any member access compiles, even one that does not exist, deferring the failure to runtime. unknown is safer but still lets you widen freely with a cast. Swift has no equivalent escape hatch: Any can hold any value, but the compiler still requires an as?/as! cast, checked against the value's real runtime type, before any member can be accessed.
Optionals
Optionals vs. undefined/null
let username: string | undefined = undefined; let nickname: string | null = null; console.log(username === undefined, nickname === null);
var username: String? = nil var nickname: String? = nil print(username == nil, nickname == nil)
TypeScript has two absence values, undefined and null, which must be unioned onto a type explicitly (string | undefined). Swift has exactly one, nil, and wraps it into a genuine Optional<T> type (spelled T?) rather than a union. A non-optional String can never be nil — the compiler statically rules it out, whereas TypeScript's guarantee depends on strictNullChecks being enabled and honestly followed.
Unwrapping with if let
function greet(name: string | undefined): void { if (name !== undefined) { console.log(`Hello, ${name}`); } else { console.log("Hello, stranger"); } } greet("Alice"); greet(undefined);
func greet(name: String?) { if let actualName = name { print("Hello, \(actualName)") } else { print("Hello, stranger") } } greet(name: "Alice") greet(name: nil)
TypeScript narrows string | undefined to string with a plain if comparison, and the narrowing is a compiler analysis over the union — there is no separate binding step. Swift's if let actualName = name both tests for nil and introduces a new, non-optional binding scoped to the block, making the unwrap an explicit, visible step rather than an inferred narrowing.
guard let for early exit
function processUser(user: { name?: string } | undefined): void { if (user === undefined) return; if (user.name === undefined) return; console.log(`Processing: ${user.name}`); } processUser({ name: "Alice" }); processUser(undefined);
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 unwrap: unlike if let, the unwrapped binding remains available for the rest of the enclosing scope after the guard statement, and the else branch is required to exit (return, throw, break, or continue). TypeScript's closest analogue is a plain early-return if check with no compiler-enforced exit requirement.
Nil-coalescing (??)
const setting: string | undefined = undefined; const value = setting ?? "default"; console.log(value);
let setting: String? = nil let value = setting ?? "default" print(value)
The ?? operator is spelled and behaves the same way in both languages: it evaluates to the right-hand side only when the left-hand side is nil/undefined/null. Swift's version is additionally checked so that the right-hand side must match the optional's wrapped type.
Optional chaining (?.)
interface Address { city: string; } interface User { address?: Address; } const user: User = {}; console.log(user.address?.city ?? "Unknown");
struct Address { let city: String } struct User { let address: Address? } let user = User(address: nil) print(user.address?.city ?? "Unknown")
TypeScript borrowed ?. directly from languages like Swift, and the short-circuiting behavior is identical: if any link in the chain is absent, the whole expression evaluates to undefined/nil without throwing. In Swift, chaining through any optional automatically wraps the final result in an optional, which the compiler tracks precisely.
The dangers of force-unwrap (!)
function lookup(map: Map<string, number>, key: string): number { // The non-null assertion "!" is a compile-time-only promise to the // compiler — it emits no runtime check at all in the JavaScript output. return map.get(key)!; } const scores = new Map([["ada", 95]]); console.log(lookup(scores, "missing")); // returns undefined, no crash, no warning
func lookup(_ dictionary: [String: Int], key: String) -> Int { // "!" force-unwraps at runtime and traps (crashes) if the value is nil. dictionary[key]! } let scores = ["ada": 95] print(lookup(scores, key: "ada")) // print(lookup(scores, key: "missing")) // fatal error: unexpectedly found nil — crashes immediately
TypeScript's non-null assertion ! is purely a compile-time suppression of the type checker — it is erased entirely, so an incorrect assertion just silently produces undefined at runtime with no error at all. Swift's force-unwrap ! is a genuine runtime operation: if the optional is actually nil, the program traps and crashes immediately with a clear fatal error, rather than letting a wrong value quietly propagate.
Strings
String operations
const text = "Hello, World"; console.log(text.toUpperCase()); console.log(text.includes("World")); console.log(text.length);
let text = "Hello, World" print(text.uppercased()) print(text.contains("World")) print(text.count)
Swift's string API favors descriptive method names — uppercased(), contains(_:) — and .count rather than .length. Swift's String counts extended grapheme clusters (what a human perceives as one character), which can differ from JavaScript's UTF-16 code-unit-based .length for text containing emoji or combining characters.
Multi-line strings
const poem = `Roses are red, Violets are blue, Swift is typed, And so, now, are you.`; console.log(poem);
let poem = """ Roses are red, Violets are blue, Swift is typed, And so, now, are you. """ print(poem)
Swift uses triple double-quotes """ for multi-line strings, where TypeScript uses backtick template literals for the same purpose. The closing """'s indentation establishes the baseline that Swift strips from every line, which keeps multi-line strings readable inside indented code.
Concatenation and templates
const first = "Hello"; const second = " World"; let combined = first + second; combined += "!"; console.log(combined);
let first = "Hello" let second = " World" var combined = first + second combined += "!" print(combined)
+ and += concatenate strings identically in both languages. In Swift, += requires the target to be declared with var; attempting it on a let constant is a compile error, unlike TypeScript where reassigning a let-bound string simply requires it not be const.
Arrays & Dictionaries
Array creation and access
const fruits: string[] = ["apple", "banana", "cherry"]; console.log(fruits[0]); console.log(fruits.length); console.log(fruits.at(-1));
let fruits = ["apple", "banana", "cherry"] print(fruits[0]) print(fruits.count) print(fruits.last ?? "none")
Swift arrays are homogeneous by default and use .count rather than .length. Swift provides .first and .last as optional-returning properties — a safer alternative to raw indexing, since an out-of-bounds Swift array index crashes at runtime with no graceful undefined the way a bad TypeScript index access would return.
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 mutation methods use descriptive names — append instead of push, insert(_:at:) instead of unshift — and every removal method returns the removed element directly. A Swift array declared with let is fully immutable; mutation requires var, which TypeScript arrays never enforce since const only prevents rebinding the variable, not mutating the array.
map, filter, reduce
const numbers = [1, 2, 3, 4, 5]; const doubled = numbers.map(number => number * 2); const evens = numbers.filter(number => number % 2 === 0); const total = numbers.reduce((sum, number) => sum + number, 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)
The names and semantics of map/filter/reduce carry over directly, but Swift spells closures with braces and in rather than TypeScript's arrow syntax. Swift's reduce(_:_:) takes the initial value as its first argument, where TypeScript's reduce takes it last.
Dictionaries vs. Record/Map
const scores: Record<string, number> = { alice: 95, bob: 87 }; scores.charlie = 92; console.log(scores.alice); console.log(scores.dave); // undefined — no compile-time or runtime signal
var scores = ["alice": 95, "bob": 87] scores["charlie"] = 92 print(scores["alice"] ?? -1) print(scores["dave"] ?? -1) // nil, made visible as an Optional
A TypeScript Record<string, number> is really just an object typed as a fixed-key/value map, and reading a missing key silently returns undefined even when the index signature promises a number. Swift's [String: Int] dictionary makes every lookup return Int?, so the possibility of a missing key is visible in the type itself and must be handled, typically with ?? or optional binding.
Iterating key-value pairs
const config: Record<string, string> = { host: "localhost", port: "8080" }; for (const [key, value] of Object.entries(config)) { console.log(`${key}: ${value}`); }
let config = ["host": "localhost", "port": "8080"] for (key, value) in config { print("\(key): \(value)") }
Swift's for (key, value) in dictionary destructures pairs directly, with no equivalent of Object.entries() needed. Neither language guarantees iteration order for its map type (TypeScript objects only preserve insertion order for string keys as an implementation detail codified since ES2015, not a documented contract for arbitrary key types).
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 read almost identically to TypeScript's, but the parentheses around the condition are optional in Swift and conventionally omitted, while the braces are mandatory — there is no braceless single-statement if the way TypeScript allows.
for loops and ranges
for (let index = 0; index < 5; index++) { console.log(index); } 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 has no three-part for (init; condition; increment) loop; numeric iteration uses ranges instead — 0..<5 is a half-open range excluding 5, and 0...5 is a closed range including 5. Iterating a collection directly with for item in items is the same pattern as TypeScript's for...of.
switch — no fallthrough, must be exhaustive
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 never falls through by default — no break statements are needed, and omitting one in TypeScript is a common source of bugs that Swift structurally rules out (add the explicit fallthrough keyword if you actually want that behavior). Swift additionally requires every switch to be exhaustive, verified at compile time, where TypeScript only warns about unhandled cases when strict mode and careful typing line up.
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 } var x = 0 repeat { print("repeat:", x) x += 1 } while x < 2
Swift spells the run-at-least-once loop repeat { } while where TypeScript uses do { } while. Swift also removed ++ and -- entirely, in favor of += 1 and -= 1, to eliminate ambiguity between prefix and postfix forms.
Functions & Closures
Function definitions
function greet(name: string): string { return `Hello, ${name}!`; } console.log(greet("World"));
func greet(name: String) -> String { "Hello, \(name)!" // single-expression bodies can omit "return" } print(greet(name: "World"))
Swift places the return type after -> rather than after a colon at the end of the parameter list, and every call site uses the parameter name as an argument label by default — greet(name: "World"), not greet("World"). A single-expression function body can omit return entirely.
Argument labels — no TypeScript equivalent
function move(x: number, y: number, speed: number): void { 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 declare a separate external label (used by callers) and internal name (used inside the body) for each parameter, producing call sites that read like a sentence: move(to: 10, and: 20, at: 5). TypeScript has no equivalent — a caller either relies on positional order or must adopt a conventional options-object parameter to achieve similar clarity.
Closures vs. arrow functions
const double = (value: number): number => value * 2; const numbers = [1, 2, 3]; console.log(numbers.map(double));
let double = { (value: Int) -> Int in value * 2 } let numbers = [1, 2, 3] print(numbers.map(double))
Both languages have first-class closures that capture variables from the enclosing scope by reference. Swift wraps the closure body in braces with the parameter list and in keyword replacing the arrow; type inference often shortens this to { value in value * 2 }, and shorthand argument names ($0, $1) can eliminate parameter naming altogether.
Trailing closure syntax
const numbers = [1, 2, 3, 4, 5]; const evens = numbers.filter(number => number % 2 === 0); const result = numbers.reduce((sum, number) => sum + number, 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 a closure is the last argument to a function, Swift lets it move outside the parentheses as a "trailing closure" — numbers.filter { ... } rather than numbers.filter({ ... }). TypeScript has no syntactic equivalent; an arrow function argument always stays inside the call's parentheses.
Default parameter values
function greet(name: string, greeting: string = "Hello"): void { 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")
Default parameter values work the same way in both languages. Because Swift call sites use argument labels, skipping a default parameter is always unambiguous — the following argument is identified by name rather than by position, which avoids the "gap" problem TypeScript can hit with purely positional optional parameters.
Structs, Classes & Value Semantics
TypeScript objects are always references
class Point { constructor(public x: number, public y: number) {} } const point1 = new Point(3, 4); const point2 = point1; // reference — same underlying object point2.x = 99; console.log(point1.x, point2.x); // both 99, surprising to a Swift developer
// Swift equivalent, using a class (reference type), to show the same // aliasing behavior TypeScript objects always exhibit. final class Point { var x: Int var y: Int init(x: Int, y: Int) { self.x = x; self.y = y } } let point1 = Point(x: 3, y: 4) let point2 = point1 point2.x = 99 print(point1.x, point2.x) // both 99 — Point is a class, a reference type
JavaScript and TypeScript have exactly one kind of compound value: the object, always held and assigned by reference. A TypeScript programmer has no vocabulary for "copy on assignment" because nothing in the language ever behaves that way. Swift makes this an explicit choice per type, which is the subject of the next several concepts.
Structs — value types, copied on assignment
interface Point { x: number; y: number; } const point1: Point = { x: 3, y: 4 }; const point2 = { ...point1 }; // must spread explicitly to get an independent copy point2.x = 99; console.log(point1.x, point2.x); // 3 99 — but only because of the manual spread
struct Point { var x: Int var y: Int } var point1 = Point(x: 3, y: 4) var point2 = point1 // copied automatically — no spread needed point2.x = 99 print(point1.x, point2.x) // 3 99 — independent copies by default
A Swift struct is a value type: assigning it, passing it to a function, or storing it in a collection always copies the value. TypeScript can only approximate this by remembering to spread ({ ...point1 }) at every assignment — a manual discipline, not a language guarantee — so forgetting a spread silently reintroduces aliasing. Structs are Swift's default choice for simple data, favored because independent copies eliminate a whole class of aliasing bugs.
Classes — reference types, like TypeScript objects
class Counter { #count = 0; increment(): void { this.#count++; } get value(): number { return this.#count; } } const counter = new Counter(); counter.increment(); counter.increment(); console.log(counter.value);
final 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 class instances behave exactly like TypeScript objects: assignment shares the same instance, and mutating through one reference is visible through all others. Swift uses access-control keywords (private, internal, public) instead of the # prefix TypeScript borrowed from the ECMAScript private-fields proposal, and instantiation calls the type name directly with no new keyword.
Class inheritance
class Animal { speak(): string { return "..."; } } class Dog extends Animal { override speak(): string { return "Woof"; } } console.log(new Dog().speak());
class Animal { func speak() -> String { "..." } } class Dog: Animal { override func speak() -> String { "Woof" } } print(Dog().speak())
Both languages support single inheritance for classes, with a colon in Swift (Dog: Animal) replacing TypeScript's extends. Swift requires override on every overriding method — matching TypeScript's optional override modifier — but Swift additionally requires that inherited method calls be resolvable statically unless the base method is marked to allow it, keeping the inheritance model stricter overall.
Automatic memberwise initializers
class Person { constructor(public name: string, public age: number) {} } 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)
A Swift struct gets a free memberwise initializer generated by the compiler — no constructor body needed for the common case, which is even terser than TypeScript's constructor(public name: string) parameter-property shorthand. Swift classes, by contrast, always require an explicit init, another practical reason structs are preferred for plain data.
Copy-on-write and mutation surprises
// TypeScript arrays and objects are always shared references, so mutating // through a second binding always affects the first — an assumption a // TypeScript programmer will unconsciously carry into Swift. const original = [1, 2, 3]; const alias = original; alias.push(4); console.log(original.length); // 4 — mutation was visible through the alias
// Swift arrays are value types with copy-on-write: the assignment below // looks cheap (no immediate copy) but the moment "alias" is mutated, Swift // copies the underlying storage so "original" is left untouched. var original = [1, 2, 3] var alias = original alias.append(4) print(original.count) // 3 — original is untouched, unlike the TypeScript version above
This is a common surprise going the other direction: a TypeScript programmer's instinct is that assigning a collection to a new variable creates an alias, so mutating one mutates both. Swift arrays and dictionaries are value types that use copy-on-write internally for efficiency, but semantically they always behave as independent copies — mutating alias here never touches original.
Interfaces & Protocols
interface vs. protocol
interface Speaker { speak(): string; } // Structural: any object shaped like Speaker satisfies it implicitly. function announce(speaker: Speaker): string { return speaker.speak(); } console.log(announce({ speak: () => "Woof" }));
protocol Speaker { func speak() -> String } struct Dog: Speaker { // must explicitly declare conformance func speak() -> String { "Woof" } } func announce(speaker: Speaker) -> String { speaker.speak() } print(announce(speaker: Dog()))
A TypeScript interface is satisfied structurally — any value with a matching speak(): string method qualifies, even an inline object literal, with no declaration required. A Swift protocol must be explicitly adopted with : Speaker on the conforming type's declaration; a struct with an identical speak() method but no stated conformance does not satisfy Speaker and will not compile where one is expected.
type aliases vs. protocols
type Point = { x: number; y: number }; function distance(point: Point): number { return Math.sqrt(point.x ** 2 + point.y ** 2); } console.log(distance({ x: 3, y: 4 }));
protocol Point { var x: Double { get } var y: Double { get } } struct CartesianPoint: Point { let x: Double let y: Double } func distance(point: Point) -> Double { (point.x * point.x + point.y * point.y).squareRoot() } print(distance(point: CartesianPoint(x: 3, y: 4)))
TypeScript's type alias for an object shape and its interface are close cousins, both satisfied structurally. Swift has no structural counterpart at all — a shape-only contract must be a protocol, requiring explicit, nominal conformance from every adopting type. Swift typealias exists but only renames an existing type; it cannot define a new structural shape.
Protocol extensions (default implementations)
interface Describable { name: string; } // TypeScript has no way to attach a default method body to an interface — // shared behavior needs a base class or a free function instead. function describe(item: Describable): string { return `I am ${item.name}`; } console.log(describe({ name: "Whiskers" }));
protocol Describable { var name: String { get } } extension Describable { func describe() -> String { "I am \(name)" } // shared default, free for every conformer } struct Cat: Describable { let name: String } print(Cat(name: "Whiskers").describe())
Swift protocol extensions let a protocol supply a default method implementation that every conforming type receives automatically, and may override. TypeScript interfaces are pure shape declarations with no method bodies at all — shared behavior must come from a base class (reintroducing reference-type inheritance) or a standalone helper function, since TypeScript has nothing structurally equivalent to a protocol extension.
Multiple protocol conformance
interface Wheeled { wheels: number; } interface Motorized { engineCC: number; } type Car = Wheeled & Motorized; function describeVehicle(vehicle: Car): void { 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 } func describeVehicle(vehicle: Wheeled & Motorized) { print("Wheels: \(vehicle.wheels), Engine: \(vehicle.engineCC)cc") } describeVehicle(vehicle: Car())
TypeScript composes multiple interfaces with the intersection operator & on a type alias. Swift uses the same & syntax for an anonymous protocol composition in a parameter position, and lists multiple protocols comma-separated after a type's name when declaring conformance — a close syntactic parallel once you know TypeScript's & is doing double duty across both languages.
Unions & Enums
Discriminated unions vs. enums with associated values
type Shape = | { kind: "circle"; radius: number } | { kind: "rect"; width: number; height: number }; function area(shape: Shape): number { switch (shape.kind) { case "circle": return Math.PI * shape.radius ** 2; case "rect": return shape.width * shape.height; } } console.log(area({ kind: "rect", width: 3, height: 4 }));
enum Shape { case circle(radius: Double) case rect(width: Double, height: Double) } func area(shape: Shape) -> Double { switch shape { case .circle(let radius): return Double.pi * radius * radius case .rect(let width, let height): return width * height } } print(area(shape: .rect(width: 3, height: 4)))
This pairing is the closest conceptual parallel between the two languages. A TypeScript discriminated union tags each variant object with a literal kind field the developer must define and check by hand. A Swift enum with associated values is a first-class sum type built into the language — the compiler, not a hand-maintained tag field, guarantees the switch is exhaustive and each case's payload is correctly typed.
String literal unions vs. simple enums
type Direction = "north" | "south" | "east" | "west"; const heading: Direction = "north"; console.log(heading);
enum Direction { case north, south, east, west } let heading = Direction.north print(heading)
A TypeScript string literal union ("north" | "south" | ...) is the idiomatic way to express a closed set of named options, but it is still just strings under the hood — nothing stops a stray typo'd string from being coerced elsewhere in a loosely typed codebase. A Swift enum is a genuinely distinct type; Direction.north is not a String at all, and the compiler catches any confusion between the two.
Enums with raw values
enum Status { Active = 1, Inactive = 0, Pending = 2 } const current = Status.Active; console.log(current === Status.Active); console.log(Status[1]); // reverse lookup back to the name
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) // failable init round-trips the raw value
TypeScript's numeric enum is really just an object with forward and reverse key mappings baked in by the compiler. Swift's raw-value enum (enum Status: Int) exposes the backing value through .rawValue and offers a failable initializer, Status(rawValue:), that returns an Optional rather than assuming the raw value is valid — safer than TypeScript's implicit reverse mapping, which has no failure case built in.
Exhaustiveness checking
type Shape = { kind: "circle" } | { kind: "square" }; function describe(shape: Shape): string { switch (shape.kind) { case "circle": return "round"; // Forgetting the "square" case compiles fine unless "noImplicitReturns" // and careful typing of the return path catch it — easy to miss. } } console.log(describe({ kind: "circle" }));
enum Shape { case circle, square } func describe(shape: Shape) -> String { switch shape { case .circle: return "round" // case .square: return "boxy" // omitting this is a compile error, not a maybe-warning } } print(describe(shape: .circle))
TypeScript can catch a missing discriminated-union case, but only when the function's return type and a strict compiler configuration line up just right — it is a best-effort analysis, not a language guarantee. Swift's switch exhaustiveness over an enum is enforced unconditionally: the compiler rejects the build outright if any case is unhandled and there is no default. The commented-out lines above illustrate the point being made rather than code meant to execute — actually omitting the case would fail to compile in either language — so this example is marked non-runnable.
Generics
Generic functions
function first<T>(items: T[]): T { return items[0]; } console.log(first([10, 20, 30]));
func first<T>(_ items: [T]) -> T { items[0] } print(first([10, 20, 30]))
The angle-bracket generic syntax <T> is shared between the two languages and the concepts map closely. The crucial difference is underneath: TypeScript generics are fully erased during compilation, while Swift generics are reified — the concrete type argument exists at runtime, which is part of why Swift generic code compiles to efficient, specialized machine code rather than operating on erased, boxed values.
Generic constraints
interface Comparable<T> { compareTo(other: T): number; } function maxOf<T extends Comparable<T>>(first: T, second: T): T { return first.compareTo(second) > 0 ? first : second; } class Money implements Comparable<Money> { constructor(public cents: number) {} compareTo(other: Money): number { return this.cents - other.cents; } } console.log(maxOf(new Money(300), new Money(900)).cents);
func maxOf<T: Comparable>(_ first: T, _ second: T) -> T { first > second ? first : second } print(maxOf(3, 9))
TypeScript constrains a generic parameter with extends; Swift uses a colon in a where-style clause, here shortened to T: Comparable. Swift's standard library already makes the built-in numeric and string types conform to Comparable, so maxOf works on plain Int values directly, without the wrapper class TypeScript's example needed to satisfy a hand-written Comparable<T> interface.
Pattern Matching
Matching on tuples
const x = 0, y = 5; let description: string; if (x === 0 && y === 0) description = "Origin"; else if (x === 0) description = "On Y axis"; else if (y === 0) description = "On X axis"; else description = "Elsewhere"; console.log(description);
let x = 0 let y = 5 let description: String switch (x, y) { case (0, 0): description = "Origin" case (0, _): description = "On Y axis" case (_, 0): description = "On X axis" default: description = "Elsewhere" } print(description)
Swift's switch can match directly on a tuple, using _ as a wildcard for positions to ignore. TypeScript's switch can only compare a single value at a time, so the equivalent logic requires an if/else if chain, as shown here.
Ranges and where clauses
function letterGrade(score: number): string { if (score >= 90) return "A"; if (score >= 80) return "B"; if (score >= 70) return "C"; return "F"; } console.log(letterGrade(75));
func letterGrade(score: Int) -> String { switch score { case 90...100: return "A" case 80..<90: return "B" case let value where value >= 70: return "C" default: return "F" } } print(letterGrade(score: 75))
Swift switch cases accept ranges directly as patterns (90...100 inclusive, 80..<90 exclusive) and a where clause can attach an arbitrary extra condition to a case, as in case let value where value >= 70. TypeScript has no pattern-matching construct that reaches this expressiveness; range and conditional logic both fall back to plain if/else if chains.
Binding associated values in a switch
type Message = | { type: "text"; content: string } | { type: "image"; url: string }; const messages: Message[] = [ { type: "text", content: "Hello" }, { type: "image", url: "photo.jpg" }, ]; for (const message of messages) { if (message.type === "text") console.log("Text:", message.content); if (message.type === "image") console.log("Image:", message.url); }
enum Message { case text(String) case image(url: String) } let messages: [Message] = [.text("Hello"), .image(url: "photo.jpg")] for message in messages { switch message { case .text(let content): print("Text:", content) case .image(let url): print("Image:", url) } }
Switching on a Swift enum with associated values both narrows the case and extracts its payload into a named binding in one step — case .text(let content). The TypeScript discriminated union achieves a similar narrowing effect through the type tag field, but each variant's properties must be accessed separately after the tag check rather than bound directly by the pattern.
Error Handling
TypeScript throw/catch has no typed errors
function divide(numerator: number, denominator: number): number { if (denominator === 0) throw new Error("Division by zero"); return numerator / denominator; } try { console.log(divide(10, 2)); console.log(divide(10, 0)); } catch (error) { // "error" is typed "unknown" — nothing at the throw site is enforced. console.log("Error:", (error as 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") }
TypeScript has no typed-throws mechanism at all: a function can throw any value of any type with no signature-level declaration, and a catch block receives that value typed unknown, requiring a cast to use it safely. Swift requires a throwing function to be marked throws in its signature, requires try at every call site, and lets errors conform to the Error protocol so a catch clause can pattern-match specific error cases directly.
try? and try! — optional-flavored error handling
function parseNumberOrNull(text: string): number | null { const value = Number(text); return Number.isNaN(value) ? null : value; } console.log(parseNumberOrNull("42")); console.log(parseNumberOrNull("oops"));
enum ParseError: Error { case invalid } func parseNumber(_ text: String) throws -> Int { guard let value = Int(text) else { throw ParseError.invalid } return value } let good = try? parseNumber("42") // Optional(42) let bad = try? parseNumber("oops") // nil print(good as Any, bad as Any)
try? converts any throwing call into an Optional — nil on failure, the value otherwise — collapsing a whole do/catch block into one expression. TypeScript has no equivalent shorthand; the closest pattern is a hand-written helper, like the one shown here, that manually converts a failure signal into null. Swift also has try!, which force-unwraps the result and crashes at runtime if an error is actually thrown — use it only when failure is truly impossible.
Structured error cases with payloads
class NetworkError extends Error { constructor(message: string, public code: number) { super(message); } } 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) }
A Swift error enum with associated values bundles structured data directly onto each case, and a catch clause can both match a specific case and bind its payload in one step. TypeScript's custom error classes achieve something similar by attaching extra properties, but require an instanceof check inside a single generic catch block rather than dispatching to a distinct pattern per error kind.
Async & Concurrency
async/await — a genuine bridge point
async function computeValue(): Promise<number> { return 42; } async function main(): Promise<void> { const value = await computeValue(); console.log(value); } main();
func computeValue() async -> Int { 42 } let value = await computeValue() print(value)
This is the smoothest transition in the whole comparison: Swift's async/await surface syntax reads almost identically to TypeScript's. Swift marks a function async after its parameter list rather than before function, and top-level Swift code can await directly with no wrapping async function required, unlike this TypeScript example which needs an async main to use top-level await safely across environments.
Single-threaded event loop vs. structured concurrency
// JavaScript/TypeScript run on a single-threaded event loop. Two "concurrent" // async functions never truly run in parallel — they interleave at await // points, so there is no possibility of a data race on shared memory. let sharedCounter = 0; async function incrementTwice(): Promise<void> { sharedCounter++; await Promise.resolve(); sharedCounter++; } (async () => { await Promise.all([incrementTwice(), incrementTwice()]); console.log(sharedCounter); // deterministic: always 4 })();
import Foundation // Swift's structured concurrency can genuinely run tasks in parallel across // real OS threads, so shared mutable state needs explicit protection. // An actor serializes access to its own state, preventing data races. actor Counter { private var value = 0 func increment() { value += 1 } var current: Int { value } } let counter = Counter() await withTaskGroup(of: Void.self) { group in group.addTask { await counter.increment() } group.addTask { await counter.increment() } } print(await counter.current) // always 2 — the actor serializes access
JavaScript's single-threaded event loop means "concurrent" async functions only ever interleave at await points, so ordinary shared variables can never experience a genuine data race — TypeScript inherits this guarantee for free. Swift's structured concurrency (Task, task groups, async/await) can run work on real OS threads in true parallel, so the language introduces actor types that serialize access to their own mutable state and the compiler enforces those boundaries — a strictness with no TypeScript counterpart, because TypeScript never needed one.
Gotchas for TypeScript Programmers
Gotcha: erased types give zero runtime safety
interface Config { readonly port: number; } function loadConfig(raw: unknown): Config { // Casting past "unknown" with "as" is a compile-time-only promise. // Nothing here actually validates the shape at runtime. return raw as Config; } const config = loadConfig({ port: "8080" }); // wrong type, no error anywhere console.log(config.port + 1); // "80801" — silent string concatenation, not addition
// Swift has no equivalent unsafe cast for arbitrary data shapes. Decoding // untrusted data (JSON, network responses) requires the Codable protocol, // which performs genuine runtime validation and can fail with a thrown error. import Foundation struct Config: Codable { let port: Int } let rawJSON = Data("{\"port\": \"8080\"}".utf8) // wrong type: string, not number do { let config = try JSONDecoder().decode(Config.self, from: rawJSON) print(config.port) } catch { print("Decoding failed, as it should: \(error is DecodingError)") }
Because TypeScript types vanish entirely at runtime, a lying type assertion (as Config) is accepted by the compiler and produces no error anywhere — the mismatch surfaces later as silent, confusing behavior, like string concatenation where arithmetic was expected. Swift's equivalent operation, decoding untrusted data with Codable, genuinely validates the data against the real, runtime-present type and throws a catchable error the moment the shape does not match.
Gotcha: force-unwrap crashes instead of going silent
function getFirstChar(text: string | undefined): string { return text![0]; // "!" is erased; if text is undefined, this throws a // TypeError at the point of use, not where the lie was told } console.log(getFirstChar("hello")); // getFirstChar(undefined); // TypeError: Cannot read properties of undefined
func getFirstCharacter(text: String?) -> Character { text![text!.startIndex] // force-unwrap traps immediately if text is nil } print(getFirstCharacter(text: "hello")) // print(getFirstCharacter(text: nil)) // fatal error: unexpectedly found nil — immediate, precise crash
Both languages let a careless force-unwrap fail at runtime, but the failure modes differ in severity and clarity. TypeScript's ! assertion is erased at compile time; a wrong assertion surfaces later as a generic TypeError at whatever point the value is actually used, potentially far from the mistaken assertion. Swift's ! traps immediately, at the exact unwrap site, with an unambiguous "unexpectedly found nil" message — a design that treats a broken invariant as a bug to fix rather than a value to keep passing along.
Gotcha: value semantics where you expect a reference
interface Player { name: string; score: number; } function addPoint(player: Player): void { player.score += 1; // mutates the caller's object — always, since it's a reference } const player = { name: "Ada", score: 0 }; addPoint(player); console.log(player.score); // 1 — the function's mutation is visible to the caller
struct Player { var name: String var score: Int } func addPoint(player: Player) { var localCopy = player localCopy.score += 1 // mutates only this function's local copy } var player = Player(name: "Ada", score: 0) addPoint(player: player) print(player.score) // 0 — the caller's struct is completely untouched
A TypeScript programmer's instinct — "passing an object into a function lets that function mutate the caller's data" — is simply wrong for a Swift struct passed by value: the function receives an independent copy, and mutating it (even after making the parameter a local var) never affects the original. To let a function intentionally mutate the caller's struct, Swift requires the parameter to be marked inout and the call site to pass it with an explicit & — making the mutation visible and opt-in rather than implicit.
Gotcha: Swift requires full initialization before use
class Account { balance: number; // TypeScript allows this to sit "definitely assigned" // as long as the constructor sets it somewhere — order // and completeness are checked, but the model is lenient // compared to Swift's. constructor(startingBalance: number) { this.balance = startingBalance; } } console.log(new Account(100).balance);
class Account { var balance: Int // Every stored property must have a value by the time init() finishes — // there is no way to construct an Account leaving "balance" unset. init(startingBalance: Int) { balance = startingBalance } } print(Account(startingBalance: 100).balance)
Swift enforces two-phase initialization: every stored property on a class or struct must be assigned a value before the initializer finishes and before self can be used in most contexts, or the code simply does not compile. TypeScript's "definite assignment" checking is comparatively permissive — it primarily verifies a property is set somewhere before first read, without Swift's stricter guarantee that construction is atomic and a half-built instance can never be observed.