Hello World & Basics
Hello, World
print("Hello, World!") console.log("Hello, World!"); Like Swift, TypeScript runs top-level statements — no
main() ceremony. Output goes through console.log, and semicolons are optional but conventional.Type inference
let city = "Lisbon" // String, inferred
let population = 545_923 // Int, inferred
print("\(city): \(population)") const city = "Lisbon"; // string, inferred
const population = 545_923; // number, inferred
console.log(`${city}: ${population}`); Both languages infer aggressively, and both accept the
_ digit separator. The primitive type names are lowercase — string, number, boolean — because the capitalized String/Number name legacy wrapper objects you never want.Variables & Types
let/var → const/let
let fixed = 10 // immutable
var counter = 0 // mutable
counter += 1
print(fixed, counter) const fixed = 10; // immutable binding
let counter = 0; // mutable
counter += 1;
console.log(fixed, counter); The keywords shift one notch: Swift’s immutable
let is TypeScript’s const, and TypeScript’s let is Swift’s var. Beware that const freezes only the binding — a const object or array can still be mutated (see the Value vs Reference section).Type annotations
let greeting: String = "hi"
let ratio: Double = 0.5
let flags: [Bool] = [true, false]
print(greeting, ratio, flags) const greeting: string = "hi";
const ratio: number = 0.5;
const flags: boolean[] = [true, false];
console.log(greeting, ratio, flags); The annotation position is identical (
name: Type after the variable). Array types are written boolean[] or Array<boolean> rather than Swift’s [Bool] brackets-around syntax.No Int — one number type
let quotient = 7 / 2 // integer division: 3
let precise = 7.0 / 2.0 // 3.5
print(quotient, precise) const quotient = 7 / 2; // 3.5 — there is no integer division
const truncated = Math.trunc(7 / 2); // 3
const enormous = 9_007_199_254_740_993n; // bigint: exact integers beyond 2^53
console.log(quotient, truncated, enormous); TypeScript has exactly one
number type — a 64-bit float. 7 / 2 is 3.5, integer division needs Math.trunc, and integer math silently loses precision past 253, where bigint (the n suffix) takes over. There is no Int8/UInt32 family at all.An Erased, Structural Type System
Structural, not nominal
protocol Named {
var name: String { get }
}
struct Person: Named { // conformance is declared
let name: String
}
let person: Named = Person(name: "Ada")
print(person.name) interface Named {
name: string;
}
// No conformance declaration anywhere: any value with the
// right shape satisfies the interface.
const person: Named = { name: "Ada" };
console.log(person.name); Swift types conform nominally — a type is a
Named because it says so. TypeScript types match structurally: shape is identity, and a plain object literal with a name: string field is already a Named. Nothing anywhere declares the relationship.Types are erased at runtime
let mystery: Any = "text"
if let text = mystery as? String { // a real runtime check
print("It is a String: \(text)")
} const mystery: unknown = "text";
// Interfaces and type aliases do not exist at runtime — only
// typeof/instanceof checks on JavaScript values survive compilation.
if (typeof mystery === "string") {
console.log(`It is a string: ${mystery}`);
} Compilation strips every type annotation — the running program is plain JavaScript with no type information.
typeof narrows primitives and instanceof narrows class instances, but an interface can never be checked at runtime, because it no longer exists.any vs unknown
// Swift has no equivalent of `any` — even Any requires a
// runtime-checked cast before use.
let value: Any = 42
if let number = value as? Int {
print(number + 1)
} const dangerous: any = "not a number";
// `any` silences the checker entirely — this next line would
// compile fine and only misbehave at runtime:
// dangerous.toFixed(2)
const honest: unknown = "still not a number";
if (typeof honest === "string") {
console.log(honest.toUpperCase()); // must narrow before use
} any is an off switch for the type checker — Swift deliberately has nothing like it. unknown is the honest counterpart of Swift’s Any: it accepts every value but forbids every operation until you narrow it.as? / as! → the unchecked as
let value: Any = "text"
let number = value as? Int // checked at runtime → nil
print(number == nil ? "not an Int" : "an Int") const value: unknown = "text";
const claimed = value as number; // NOT checked — a compile-time-only claim
console.log(typeof claimed); // still "string" at runtime Swift’s
as? and as! actually test the cast when the program runs. TypeScript’s as is erased: it changes the checker’s mind and never touches the value. A wrong as produces no error at the cast — the mistyped value simply flows on.Optionals Become Unions
String? → string | undefined
var nickname: String? = nil
nickname = "Zed"
print(nickname ?? "none") let nickname: string | undefined = undefined;
nickname = "Zed";
console.log(nickname ?? "none"); There is no
Optional<T> wrapper type — absence is expressed as a union with undefined. Nothing is wrapped, so nothing is unwrapped: narrowing (below) plays the role of unwrapping.Two absence values
// Swift has exactly one absence value: nil.
let missing: Int? = nil
print(missing as Any) let notYetSet: number | undefined; // "was never assigned"
const deliberatelyEmpty: number | null = null; // "explicitly no value"
console.log(notYetSet, deliberatelyEmpty); JavaScript’s historical accident, inherited whole:
undefined (never assigned, missing property, no return value) and null (deliberate emptiness) are distinct values with distinct types. Most codebases standardize on one — usually undefined — and ?./?? treat both alike.Optional chaining & ??
struct Address { let city: String }
struct Customer { let address: Address? }
let customer = Customer(address: nil)
print(customer.address?.city ?? "unknown") interface Address { city: string }
interface Customer { address?: Address }
const customer: Customer = {};
console.log(customer.address?.city ?? "unknown"); The two operators Swift developers reach for most transfer verbatim:
?. short-circuits on absence and ?? supplies the fallback. A property marked address?: is optional — its type quietly becomes Address | undefined.The ! that checks nothing
let numbers = [1, 2, 3]
let first: Int? = numbers.first
print(first!) // traps immediately and predictably if nil const numbers: number[] = [1, 2, 3];
const first: number | undefined = numbers.at(0);
console.log(first!); // the ! is ERASED — no runtime check happens Swift’s force-unwrap traps deterministically at the unwrap site. TypeScript’s non-null assertion is erased along with every other annotation: if the value really is
undefined, nothing stops at this line — the bad value flows on and detonates somewhere else. Treat ! as a code-review smell, not a tool.guard let → early-return narrowing
func describe(_ input: String?) -> String {
guard let text = input else {
return "nothing"
}
return "got \(text)"
}
print(describe("hello"))
print(describe(nil)) function describe(input: string | undefined): string {
if (input === undefined) {
return "nothing";
}
return `got ${input}`; // input is narrowed to string here
}
console.log(describe("hello"));
console.log(describe(undefined)); There is no
guard keyword and no new binding: an early return does the job, and control-flow narrowing means that after the check, the same variable simply has the narrower type string for the rest of the function.if let → if narrowing
let stored: String? = "token"
if let stored {
print("found \(stored)")
} else {
print("empty")
} const stored: string | undefined = "token";
if (stored !== undefined) {
console.log(`found ${stored}`); // narrowed — no unwrap step
} else {
console.log("empty");
} Same shape, no shadow binding — the branch narrows the variable in place. The tempting shorthand
if (stored) also rejects the empty string "" and 0, so prefer the explicit !== undefined comparison when those are legitimate values.Strings
String interpolation
let name = "Grace"
let score = 97
print("\(name) scored \(score), average \(Double(score) / 10.0)") const name = "Grace";
const score = 97;
console.log(`${name} scored ${score}, average ${score / 10}`); Interpolation moves from
\(expr) in ordinary quotes to ${expr} — but only inside backtick template literals. Regular single- and double-quoted strings do not interpolate at all.Multiline strings
let letter = """
Dear Ada,
The engine works.
"""
print(letter) const letter = `Dear Ada,
The engine works.`;
console.log(letter); The backtick literal is multiline and interpolating at the same time. Unlike Swift’s
""", it performs no indentation stripping — every leading space lands in the string verbatim, so multiline literals are usually written flush left.Common string operations
let phrase = "stitch in time"
print(phrase.uppercased())
print(phrase.contains("time"))
print(phrase.split(separator: " ").count)
print(phrase.replacing("time", with: "space")) const phrase = "stitch in time";
console.log(phrase.toUpperCase());
console.log(phrase.includes("time"));
console.log(phrase.split(" ").length);
console.log(phrase.replaceAll("time", "space")); The everyday operations all exist under slightly different names:
uppercased() → toUpperCase(), contains → includes, replacing → replaceAll. Beware the classic trap that replace (singular) replaces only the first occurrence.String length is not what you think
let family = "👨👩👧👦"
print(family.count) // 1 — user-perceived characters
print(family.unicodeScalars.count) // 7 — scalars under the hood const family = "👨👩👧👦";
console.log(family.length); // 11 — UTF-16 code units
console.log([...family].length); // 7 — code points
console.log([...new Intl.Segmenter().segment(family)].length); // 1 grapheme Swift’s
count counts grapheme clusters — what a person sees. JavaScript’s .length counts UTF-16 code units, spreading counts code points, and only Intl.Segmenter reproduces Swift’s user-perceived answer. Indexing text[i] slices code units, so it can land mid-emoji.Collections
Arrays
var fruits = ["apple", "banana"]
fruits.append("cherry")
fruits.remove(at: 0)
print(fruits, fruits.count) const fruits: string[] = ["apple", "banana"];
fruits.push("cherry");
fruits.shift(); // removes from the front
console.log(fruits, fruits.length); append becomes push, count becomes length; shift/unshift work the front and splice edits anywhere. Note the mutations happen on a const — the binding is fixed, the array is not.map / filter / reduce
let numbers = [1, 2, 3, 4, 5]
let result = numbers
.filter { $0.isMultiple(of: 2) }
.map { $0 * 10 }
.reduce(0, +)
print(result) const numbers = [1, 2, 3, 4, 5];
const result = numbers
.filter((value) => value % 2 === 0)
.map((value) => value * 10)
.reduce((total, value) => total + value, 0);
console.log(result); The same trio chains the same way. There is no
$0 shorthand and no operator-as-function trick like reduce(0, +) — every callback is a written-out arrow, and reduce takes its initial value last.sorted() → toSorted()
let scores = [30, 100, 4]
let ordered = scores.sorted() // non-mutating; scores untouched
print(ordered, scores) const scores = [30, 100, 4];
const ordered = scores.toSorted((left, right) => left - right);
console.log(ordered, scores); // toSorted leaves the original alone Two traps in one method: the classic
sort() mutates the array in place, and without a comparator it sorts lexicographically — [30, 100, 4].sort() yields [100, 30, 4], because the numbers are compared as strings. toSorted (ES2023) is the non-mutating version a Swift developer expects, and the numeric comparator is never optional in practice.Dictionary → Map
var stock = ["apples": 5, "pears": 2]
stock["plums"] = 7
for (fruit, quantity) in stock.sorted(by: { $0.key < $1.key }) {
print("\(fruit): \(quantity)")
} const stock = new Map<string, number>([["apples", 5], ["pears", 2]]);
stock.set("plums", 7);
for (const [fruit, quantity] of stock) {
console.log(`${fruit}: ${quantity}`);
} Map is the general-purpose dictionary: any key type, .size, and — unlike Swift’s unordered Dictionary — guaranteed insertion-order iteration, which is why no sorting is needed for a deterministic loop.The other dictionary: plain objects
let translations: [String: String] = [
"hello": "hola",
"goodbye": "adiós",
]
print(translations["hello"] ?? "?") const translations: Record<string, string> = {
hello: "hola",
goodbye: "adiós",
};
console.log(translations["hello"] ?? "?"); For JSON-shaped, string-keyed data the everyday dictionary is a plain object, typed as
Record<string, string>. One honesty gap: indexing types as string, not string | undefined, unless the noUncheckedIndexedAccess compiler option restores Swift-style truthfulness.Sets — with algebra at last
let evens: Set = [2, 4, 6, 8]
let small: Set = [1, 2, 3, 4]
print(evens.intersection(small).sorted())
print(evens.union(small).count) const evens = new Set([2, 4, 6, 8]);
const small = new Set([1, 2, 3, 4]);
const shared = [...evens.intersection(small)];
console.log(shared.toSorted((left, right) => left - right));
console.log(evens.union(small).size); ES2025 finally gave
Set the algebra Swift always had: union, intersection, difference, isSubsetOf. Sets iterate in insertion order; spread into an array to sort.Tuples
let pair: (name: String, score: Int) = (name: "Ada", score: 99)
print(pair.name, pair.score)
let (name, score) = pair
print(name, score) const pair: [string, number] = ["Ada", 99];
const [name, score] = pair; // destructure to get names
console.log(name, score); Tuple types exist but their elements have positions, not labels — destructuring assigns the names. Under the hood a TypeScript tuple is just an array with a length-and-types contract. For genuinely labeled data, reach for an object.
Value vs Reference Semantics
No structs: assignment aliases
struct Point { var x: Int; var y: Int }
var original = Point(x: 1, y: 1)
var copy = original // a value copy
copy.x = 99
print(original.x, copy.x) // 1 99 const original = { x: 1, y: 1 };
const alias = original; // a reference — no copy happens
alias.x = 99;
console.log(original.x, alias.x); // 99 99
const copy = { ...original, x: 1 }; // spread makes a (shallow) copy
console.log(original.x, copy.x); // 99 1 The single biggest mental-model shift on this page: there are no value types. Every object is a reference, so assignment aliases — mutating through one name is visible through the other. The spread operator
{ ...original } is the explicit, shallow copy.Arrays are references too
var first = [1, 2, 3]
var second = first // copied (copy-on-write)
second.append(4)
print(first.count, second.count) // 3 4 const first = [1, 2, 3];
const second = first; // the same array
second.push(4);
console.log(first.length, second.length); // 4 4
const separate = [...first];
separate.push(5);
console.log(first.length, separate.length); // 4 5 Swift arrays copy on assignment; JavaScript arrays never do. Spread copies one level deep — nested objects inside are still shared. For a true deep copy,
structuredClone(value) is built in.Immutability is a type-level fiction
struct Config { let retries: Int }
let config = Config(retries: 3)
// config.retries = 5 // compile error: a let struct is deeply immutable
print(config.retries) interface Config { readonly retries: number }
const config: Config = { retries: 3 };
// config.retries = 5; // compile error: readonly
const options = [1, 2, 3] as const; // readonly [1, 2, 3]
console.log(config.retries, options.length); readonly properties, ReadonlyArray, and as const give compile-time immutability — but they are erased like every other annotation, so a determined runtime caller can still mutate. Object.freeze is the only runtime enforcement.No Equatable: === compares references
struct Point: Equatable { let x: Int; let y: Int }
print(Point(x: 1, y: 2) == Point(x: 1, y: 2)) // true — synthesized == const firstPoint = { x: 1, y: 2 };
const secondPoint = { x: 1, y: 2 };
console.log(firstPoint === secondPoint); // false — different references
console.log(
firstPoint.x === secondPoint.x && firstPoint.y === secondPoint.y
); For objects,
=== asks "same reference?", never "same contents?" — and there is no Equatable to synthesize structural equality. Compare fields by hand, or serialize, or use a library helper.Control Flow
Truthiness and ===
let items: [Int] = []
if items.isEmpty {
print("empty")
}
// if items { } // compile error: conditions must be Bool const zero = 0;
if (!zero) {
console.log("0 is falsy");
}
const empty = "";
console.log(empty ? "truthy" : "falsy"); // falsy
console.log(Boolean([])); // true — an empty array is truthy! Swift conditions must be
Bool; JavaScript conditions accept anything. The falsy set is 0, "", null, undefined, NaN, and false — while [] and {} are truthy. Always compare with ===; the two-equals == coerces types (0 == "0" is true).for‑in → for…of (never for…in)
let colors = ["red", "green", "blue"]
for color in colors {
print(color)
}
for (index, color) in colors.enumerated() {
print(index, color)
} const colors = ["red", "green", "blue"];
for (const color of colors) { // OF iterates values
console.log(color);
}
for (const [index, color] of colors.entries()) {
console.log(index, color);
} Swift’s
for‑in is TypeScript’s for…of, and enumerated() is .entries(). The trap: for…in also exists but iterates keys — for an array, the index strings "0", "1", "2" — which is almost never what you want.No range literals
for count in 1...3 {
print(count)
}
let squares = (1..<4).map { $0 * $0 }
print(squares) for (let count = 1; count <= 3; count += 1) {
console.log(count);
}
const squares = Array.from({ length: 3 }, (unused, index) => (index + 1) ** 2);
console.log(squares); There is no
1...3 or 1..<4 — the C-style for loop returns for counting, and Array.from with a length builds the mapped-range idiom. Exponentiation is the ** operator.switch: fallthrough returns
let status = 404
switch status {
case 200: print("ok")
case 301, 302: print("redirect")
case 400..<500: print("client error")
default: print("other")
} const status = 404;
switch (status) {
case 200:
console.log("ok");
break; // forget break and execution falls through
case 301:
case 302:
console.log("redirect");
break;
default:
if (status >= 400 && status < 500) {
console.log("client error");
} else {
console.log("other");
}
} TypeScript’s
switch is C’s: no ranges, no patterns, no value binding, and fallthrough unless every case ends in break (stacked bare cases replace Swift’s comma list). The pattern-matching power Swift packs into switch lives in discriminated-union narrowing instead — see the Enums section.Functions & Closures
Argument labels → options objects
func resize(width: Int, height: Int, preserveAspect: Bool = true) -> String {
"\(width)x\(height) preserve=\(preserveAspect)"
}
print(resize(width: 800, height: 600)) function resize(
{ width, height, preserveAspect = true }:
{ width: number; height: number; preserveAspect?: boolean }
): string {
return `${width}x${height} preserve=${preserveAspect}`;
}
console.log(resize({ width: 800, height: 600 })); TypeScript has no argument labels — every argument is positional. The options-object pattern (a destructured object parameter) recreates labeled, order-free, defaultable arguments, and call sites read almost like Swift’s.
Defaults & variadics
func total(_ values: Int..., bonus: Int = 0) -> Int {
values.reduce(0, +) + bonus
}
print(total(1, 2, 3, bonus: 10)) function total(bonus: number = 0, ...values: number[]): number {
return values.reduce((sum, value) => sum + value, 0) + bonus;
}
console.log(total(10, 1, 2, 3)); Default parameters look the same; the variadic becomes a rest parameter
...values, which must come last (Swift’s can sit anywhere because labels disambiguate the call).Closures → arrow functions
let double = { (value: Int) -> Int in value * 2 }
let doubled = [1, 2, 3].map { $0 * 2 }
print(double(21), doubled) const double = (value: number): number => value * 2;
const doubled = [1, 2, 3].map((value) => value * 2);
console.log(double(21), doubled); Arrows are closures with the
in replaced by => before the body; a single-expression body returns implicitly. There is no $0 shorthand and no trailing-closure syntax — callbacks sit inside the parentheses like any argument. A function type reads (value: number) => number, parameter names required.Captures without [weak self]
func makeCounter() -> () -> Int {
var count = 0
return {
count += 1
return count
}
}
let nextCount = makeCounter()
print(nextCount(), nextCount(), nextCount()) function makeCounter(): () => number {
let count = 0;
return () => {
count += 1;
return count;
};
}
const nextCount = makeCounter();
console.log(nextCount(), nextCount(), nextCount()); Closures capture variables identically. The difference is invisible: a tracing garbage collector handles reference cycles, so the entire
[weak self] capture-list discipline evaporates — there is no capture-list syntax and no retain-cycle leak to defend against.Enums → Discriminated Unions
Simple enums → literal unions
enum Direction: String {
case north, south, east, west
}
let heading = Direction.north
print(heading.rawValue) type Direction = "north" | "south" | "east" | "west";
const heading: Direction = "north";
console.log(heading); Idiomatic TypeScript models a simple enum as a union of string literal types: no wrapper, no
.rawValue — the value is the string, and the checker rejects anything outside the set. An enum keyword exists but predates unions and is now unidiomatic (numeric by default, and it generates runtime code the modern erasableSyntaxOnly option bans).Associated values → discriminated unions
enum Shape {
case circle(radius: Double)
case rectangle(width: Double, height: Double)
}
let shape = Shape.circle(radius: 2)
switch shape {
case .circle(let radius):
print("circle area \(Double.pi * radius * radius)")
case .rectangle(let width, let height):
print("rectangle area \(width * height)")
} type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number };
const shape: Shape = { kind: "circle", radius: 2 };
switch (shape.kind) {
case "circle":
console.log(`circle area ${Math.PI * shape.radius ** 2}`);
break;
case "rectangle":
console.log(`rectangle area ${shape.width * shape.height}`);
break;
} The discriminated union is the enum with associated values: a literal
kind field plays the case name, and switching on it narrows the payload — inside the "circle" branch, shape.radius exists and shape.width does not compile.Exhaustiveness via never
enum Beverage {
case coffee, tea
}
let order = Beverage.tea
switch order { // no default: the compiler enforces exhaustiveness
case .coffee: print("brewing coffee")
case .tea: print("steeping tea")
} type Beverage = "coffee" | "tea";
function describe(order: Beverage): string {
switch (order) {
case "coffee": return "brewing coffee";
case "tea": return "steeping tea";
default: {
const impossible: never = order; // compile error if a case were missing
return impossible;
}
}
}
console.log(describe("tea")); switch is not exhaustive on its own; the never-typed default is the idiom that recruits the checker. Once every case is handled, order narrows to never in the default, so the assignment type-checks — and stops compiling the day someone adds "matcha" to Beverage.Classes & Protocols → Interfaces
Classes & computed properties
class Rectangle {
let width: Double
let height: Double
init(width: Double, height: Double) {
self.width = width
self.height = height
}
var area: Double { width * height }
}
print(Rectangle(width: 3, height: 4).area) class Rectangle {
readonly width: number;
readonly height: number;
constructor(width: number, height: number) {
this.width = width;
this.height = height;
}
get area(): number {
return this.width * this.height;
}
}
console.log(new Rectangle(3, 4).area); init becomes constructor, instantiation requires new, and a computed property is a get accessor. this. is mandatory for member access — there is no Swift-style implicit self. The shorthand constructor(readonly width: number) declares and assigns the field in one stroke.Inheritance & override
class Animal {
func speak() -> String { "..." }
}
class Dog: Animal {
override func speak() -> String { "Woof" }
}
let animals: [Animal] = [Animal(), Dog()]
for animal in animals {
print(animal.speak())
} class Animal {
speak(): string {
return "...";
}
}
class Dog extends Animal {
override speak(): string {
return "Woof";
}
}
const animals: Animal[] = [new Animal(), new Dog()];
for (const animal of animals) {
console.log(animal.speak());
} : becomes extends, and dynamic dispatch works as expected. The override keyword exists but is optional unless the noImplicitOverride compiler option enforces it — Swift’s always-required marker is opt-in here. There is no final; every method is open.Protocols → interfaces
protocol Greeter {
func greet(name: String) -> String
}
struct FriendlyGreeter: Greeter {
func greet(name: String) -> String { "Hello, \(name)!" }
}
print(FriendlyGreeter().greet(name: "Ada")) interface Greeter {
greet(name: string): string;
}
// A class MAY declare conformance...
class FriendlyGreeter implements Greeter {
greet(name: string): string {
return `Hello, ${name}!`;
}
}
// ...but any matching shape qualifies, declared or not:
const casualGreeter: Greeter = {
greet: (name) => `hi ${name}`,
};
console.log(new FriendlyGreeter().greet("Ada"), casualGreeter.greet("Zed")); implements is optional documentation — structural typing makes conformance automatic for anything with the right shape, including a bare object literal. There is no protocol-extension mechanism for default method implementations; abstract classes or standalone functions fill that role.Extensions have no equivalent
extension Int {
var doubled: Int { self * 2 }
}
print(21.doubled) // There is no extension mechanism. Patching built-in prototypes
// is possible but strongly discouraged:
// (Number.prototype as any).doubled = ...
// The idiom is a plain function instead:
function doubled(value: number): number {
return value * 2;
}
console.log(doubled(21)); Swift extensions — retroactively adding methods to any type — have no TypeScript counterpart. Monkey-patching prototypes is community-shunned (name collisions broke the web often enough that a proposed method was renamed
flat over it), so the idiom is standalone helper functions.Generics
Generic functions
func firstAndLast<Element>(_ items: [Element]) -> (Element, Element)? {
guard let first = items.first, let last = items.last else { return nil }
return (first, last)
}
if let bounds = firstAndLast([1, 2, 3]) {
print(bounds.0, bounds.1)
} function firstAndLast<Element>(items: Element[]): [Element, Element] | undefined {
if (items.length === 0) return undefined;
return [items[0], items[items.length - 1]];
}
const bounds = firstAndLast([1, 2, 3]);
if (bounds !== undefined) {
console.log(bounds[0], bounds[1]);
} Same angle brackets, same inference at the call site —
firstAndLast([1, 2, 3]) infers Element = number in both languages. Like everything else, generics are fully erased at runtime.Constraints: extends, not protocols
func largest<Value: Comparable>(_ values: [Value]) -> Value? {
values.max()
}
print(largest([3, 1, 4]) ?? 0) function pluckNames<Item extends { name: string }>(items: Item[]): string[] {
return items.map((item) => item.name);
}
console.log(pluckNames([
{ name: "Ada", age: 36 },
{ name: "Alan", age: 41 },
])); The constraint clause is
extends, and — true to structural typing — it constrains on a shape, not a protocol. There is no Comparable to bound by: < works on numbers and strings natively, and custom ordering is always an explicit comparator function.Type-Level Power Swift Lacks
Literal types
// The closest Swift gets to a literal type is an enum —
// a separate nominal type with cases.
enum LogLevel: String {
case debug, info, warning
}
func log(_ level: LogLevel, _ message: String) {
print("[\(level.rawValue)] \(message)")
}
log(.info, "started") // Any specific VALUE can be a type, and unions combine them freely.
function log(level: "debug" | "info" | "warning", message: string): void {
console.log(`[${level}] ${message}`);
}
log("info", "started");
// log("silly", "?"); // compile error — not one of the three literals Literal types make plain strings and numbers checkable without declaring a nominal type anywhere — the union is the enum, and ordinary string values flow in and out with no wrapping or raw-value conversion.
keyof & mapped types
// Swift has no type-level operation that derives one type from
// another — a "partial Settings" must be written out by hand.
struct Settings {
var theme: String
var fontSize: Int
}
struct SettingsPatch {
var theme: String?
var fontSize: Int?
}
let patch = SettingsPatch(theme: "dark", fontSize: nil)
print(patch.theme ?? "unchanged") interface Settings {
theme: string;
fontSize: number;
}
// Derived mechanically — stays in sync when Settings changes:
const patch: Partial<Settings> = { theme: "dark" };
type SettingName = keyof Settings; // "theme" | "fontSize"
const changed: SettingName = "theme";
console.log(patch.theme ?? "unchanged", changed); Mapped types compute whole types from other types:
Partial, Pick, Omit, Record are one-liners in the standard library, and keyof extracts property names as a literal union. This type-level computation is the capability gap running in TypeScript’s favor — Swift’s generics cannot express any of it.Template literal types
// No Swift equivalent: a type cannot describe a string PATTERN.
let endpoint = "GET /users"
print(endpoint) type Method = "GET" | "POST";
type Route = `${Method} /${string}`; // the type describes a string pattern
const endpoint: Route = "GET /users";
// const bad: Route = "FETCH /users"; // compile error
console.log(endpoint); Template literal types check string shapes at compile time — route patterns, event names like
on\${Capitalize<string>}, CSS units. The checker even distributes the union: Route here accepts any string starting with "GET /" or "POST /".Error Handling
throws / do-catch → untyped try/catch
enum FileError: Error {
case notFound(path: String)
}
func read(path: String) throws -> String {
throw FileError.notFound(path: path)
}
do {
let text = try read(path: "/tmp/missing")
print(text)
} catch FileError.notFound(let path) {
print("not found: \(path)")
} class FileError extends Error {
path: string;
constructor(path: string) {
super(`not found: ${path}`);
this.name = "FileError";
this.path = path;
}
}
function read(path: string): string {
throw new FileError(path);
}
try {
console.log(read("/tmp/missing"));
} catch (error) {
if (error instanceof FileError) {
console.log(error.message);
}
} Nothing in a TypeScript signature says "this throws" — there is no
throws keyword, no try marker at the call site, and no typed catch clauses. Any function may throw anything; one catch receives it, and instanceof narrowing inside the block replaces Swift’s pattern-matched catch arms.Anything can be thrown
// Every Swift error is a value conforming to Error — what you
// catch is always an Error.
enum LoaderError: Error { case offline }
do {
throw LoaderError.offline
} catch {
print("caught: \(error)")
} // Anything can be thrown — not just Error instances.
try {
throw "a bare string";
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
console.log(`caught: ${message}`);
} JavaScript lets code throw strings, numbers, or anything else, so the caught value is typed
unknown (under the useUnknownInCatchVariables strictness default) and must be narrowed before use. The instanceof Error check with a String(error) fallback is the standard incantation.Result → a discriminated union
struct ParseError: Error { let text: String }
func parse(_ text: String) -> Result<Int, ParseError> {
if let value = Int(text) {
return .success(value)
}
return .failure(ParseError(text: text))
}
switch parse("42") {
case .success(let value): print("parsed \(value)")
case .failure(let error): print("failed on \(error.text)")
} type ParseOutcome =
| { ok: true; value: number }
| { ok: false; text: string };
function parse(text: string): ParseOutcome {
const value = Number(text);
return Number.isNaN(value) ? { ok: false, text } : { ok: true, value };
}
const outcome = parse("42");
if (outcome.ok) {
console.log(`parsed ${outcome.value}`);
} else {
console.log(`failed on ${outcome.text}`);
} There is no standard
Result type, but the discriminated-union pattern is the same idea, and narrowing on .ok replaces the switch over cases. This errors-as-values style is how TypeScript codebases recover the honesty the untyped throw gives up.Concurrency: One Thread
async/await
func fetchGreeting() async -> String {
"hello from an async function"
}
let greeting = await fetchGreeting()
print(greeting) async function fetchGreeting(): Promise<string> {
return "hello from an async function";
}
async function main(): Promise<void> {
const greeting = await fetchGreeting();
console.log(greeting);
}
main(); async/await transfers directly — TypeScript is where the pattern Swift adopted was popularized. The visible difference: an async function’s return type is spelled Promise<string>, the future made explicit. (The browser runner compiles examples as scripts, so the await sits inside an async function here rather than at top level.)Task → Promise
let task = Task {
"computed in a task"
}
print(await task.value) async function main(): Promise<void> {
const promise = new Promise<string>((resolve) => {
setTimeout(() => resolve("computed later"), 10);
});
console.log(await promise);
}
main(); A
Promise is Task.value without the task: a handle to a value that will exist. Both start eagerly. The constructor’s resolve callback bridges callback-style APIs (like setTimeout) into the await world.async let → Promise.all
func square(_ value: Int) async -> Int { value * value }
async let first = square(3)
async let second = square(4)
let results = await [first, second]
print(results) async function square(value: number): Promise<number> {
return value * value;
}
async function main(): Promise<void> {
const results = await Promise.all([square(3), square(4)]);
console.log(results);
}
main(); Promise.all covers both async let and withTaskGroup: start everything, await the batch. Promise.allSettled keeps individual failures instead of rejecting wholesale, and Promise.race takes the first to finish.No actors — there is one thread
actor BankAccount {
private var balance = 0
func deposit(_ amount: Int) -> Int {
balance += amount
return balance
}
}
let account = BankAccount()
print(await account.deposit(10))
print(await account.deposit(5)) // No actors, no Sendable, no data races: JavaScript runs your
// code on ONE thread. Interleaving happens only at await points.
class BankAccount {
private balance = 0;
deposit(amount: number): number {
this.balance += amount;
return this.balance;
}
}
const account = new BankAccount();
console.log(account.deposit(10));
console.log(account.deposit(5)); The entire actor/
Sendable/isolation apparatus dissolves, because there is nothing to isolate: one thread runs all your code, and synchronous stretches between await points can never be interleaved. Real parallelism means Worker threads, which share nothing and communicate by message passing.