1.7 — Generics and the Swift type system
Opening scenario
You’re reading the standard library and notice:
public struct Array<Element> : RandomAccessCollection { /* … */ }
public func + <T>(lhs: [T], rhs: [T]) -> [T]
public protocol Sequence {
associatedtype Element
associatedtype Iterator: IteratorProtocol where Iterator.Element == Element
/* … */
}
This is the language talking to itself in the abstract. Generics, associated types, where clauses, protocols-with-Self-constraints — Swift’s type system is closer to Haskell or Rust than to Java or Kotlin. You don’t have to write generic algorithms from scratch on day one. You do have to read them confidently. By the end of this chapter, you will.
Concept → Why → How → Code
Concept: a generic type is a type with type-parameters
struct Stack<Element> {
private var items: [Element] = []
mutating func push(_ x: Element) { items.append(x) }
mutating func pop() -> Element? { items.popLast() }
}
var s = Stack<Int>()
s.push(1); s.push(2)
print(s.pop() ?? -1) // 2
Stack<Int> and Stack<String> are two distinct types generated from one template. The compiler specializes generic code per concrete type — there’s no boxing, no runtime dispatch (unlike Java’s erased generics).
Why this is essential
Without generics, Array<Int> and Array<String> would either:
- Be two separate hand-coded types (DRY violation, maintenance hell), or
- Use
Anyinternally (no type safety, runtime crashes).
Generics give you one implementation that’s type-safe at every call site, with zero runtime overhead because the compiler monomorphizes.
How: generic functions
func swapTwo<T>(_ a: inout T, _ b: inout T) {
let tmp = a; a = b; b = tmp
}
var x = 1, y = 2
swapTwo(&x, &y) // T inferred as Int
var s1 = "hello", s2 = "world"
swapTwo(&s1, &s2) // T inferred as String
How: constraints
Bare T lets you assign and store but not much else. You can’t compare, hash, add — the compiler doesn’t know what T supports. Constraints tell the compiler what to assume:
func minimum<T: Comparable>(_ xs: [T]) -> T? {
guard var best = xs.first else { return nil }
for x in xs.dropFirst() where x < best { best = x }
return best
}
minimum([3, 1, 4, 1, 5]) // 1
minimum(["banana", "apple"]) // "apple"
<T: Comparable> means “T must conform to Comparable.” Now < is legal inside the function.
Multiple constraints with where:
func deduplicate<S: Sequence>(_ seq: S) -> [S.Element]
where S.Element: Hashable {
var seen = Set<S.Element>()
return seq.filter { seen.insert($0).inserted }
}
How: associated types in protocols
Generics on a protocol are spelled differently — using associatedtype:
protocol Container {
associatedtype Item
var count: Int { get }
mutating func append(_ item: Item)
subscript(i: Int) -> Item { get }
}
struct IntBag: Container {
private var xs: [Int] = []
var count: Int { xs.count }
mutating func append(_ item: Int) { xs.append(item) }
subscript(i: Int) -> Int { xs[i] }
}
// Item is INFERRED as Int from the append signature
The reason Sequence.Element exists as associatedtype Element and not protocol Sequence<Element> is historical (associated types predate primary-associated-type syntax). Both are now valid forms.
How: primary associated types (Swift 5.7+)
protocol Collection<Element>: Sequence {
associatedtype Element
/* … */
}
func process(_ items: any Collection<Int>) { … }
// ^^^^^^^^^^^^^^^ — uses the primary associated type
Before Swift 5.7, you had to write where Items.Element == Int. Now Collection<Int> is shorthand. This is one of the biggest recent quality-of-life upgrades to the type system.
How: some and any — the two erasures
This is the part where the conceptual model is most important.
// (1) Concrete type — no abstraction
func makeCircle() -> Circle { Circle(radius: 5) }
// (2) Opaque return type with `some` — "I return ONE specific concrete type
// that conforms to View, but I won't tell you which one"
func makeShape() -> some View {
Circle().fill(.red)
}
// (3) Existential type with `any` — "I return SOME type conforming to View,
// possibly different on each call; box it"
func makeShapes() -> [any View] {
[Circle(), Rectangle(), Triangle()]
}
| Form | Compiler knows the concrete type? | Runtime cost | When |
|---|---|---|---|
Circle | yes, exactly | none | concrete is fine |
some View | yes, one fixed concrete per call site | none | opaque return (SwiftUI everywhere) |
any View | no — boxed existential | one indirection per call | heterogeneous collections |
Rule of thumb: prefer some for single returns (think SwiftUI bodies); reach for any only when you genuinely need heterogeneous collections.
In the wild
- SwiftUI’s entire view system is built on
some View. Everyvar body: some View { … }returns an opaque generic-shaped view tree. CombineandAsyncSequenceuse heavy generics —Publisher<Output, Failure>is one of the more advanced uses.Result<Success, Failure>— the standard library’s generic return type for fallible operations.Codablesynthesis is generic:JSONDecoder().decode(MyType.self, from: data)works for anyDecodabletype.
Common misconceptions
-
“
someandanyare the same thing.” Profoundly different.somepreserves type identity (the compiler knows it’s all the same concrete type);anyerases it (the value is boxed, each instance might be different). Misusing them is the #1 source of “why won’t this compile?” frustration in modern SwiftUI. -
“Generics make code slower because of runtime dispatch.” Wrong for Swift specifically. The compiler specializes generic code at compile time per concrete type. There’s no boxing, no v-table dispatch (unlike
any Pwhich does box). -
“You should make every function generic to maximize reuse.” Generics have a cost: longer compile times, more complex error messages, harder onboarding. Use them when you have at least two concrete types the function should accept. Single-use “generic” code is just abstraction theater.
-
“Associated types are the same as type parameters.” Conceptually similar, syntactically different, and crucially: you can’t have a function like
func foo<C: Container>(...)where C has multiple associated types withoutwhereclauses spelling them out. -
“
anyis deprecated; you should never use it.” False.anyis correct (and required by the compiler in Swift 5.7+ for clarity) when you need a heterogeneous collection or a runtime-determined type. The cost is real but usually negligible.
Seasoned engineer’s take
Generics in Swift are like a sharp knife. You don’t need one to make a sandwich, but the moment you start cooking dinner for a family you’ll wish you had it.
Beginner mistake: never reaching for generics, copying functions for [String] and [Int]. Senior mistake: making everything generic from day one, drowning compile times in 12-second error messages.
A good progression:
- Start with concrete types. Write the function for
[User]. - When you find yourself copy-pasting that function for
[Post], then make it generic. - When the generic version starts attracting
whereclauses three lines long, ask whether you need a protocol instead. Often the right abstraction is “things that have anid” (Identifiable), not “Things that look like User and Post.”
For protocols specifically: the modern Swift trend is to use protocols when you need polymorphism, generics when you need type-parameter abstraction. Protocols compose horizontally (a type conforms to several); generics compose vertically (a function or type takes a parameter).
The single most empowering thing you can do for your Swift career: read the standard library declarations in Xcode (Cmd-click → “Show in Standard Library”). Sequence, Collection, Result, Array — the way these are written is the canonical idiom you should model your own generic code on.
TIP: Compiler errors for generics are notoriously long. If Xcode complains about a constraint, split the call into two lines (assign intermediate values to typed variables). The error will collapse from 40 lines to a clear “expected
String, gotInt.”
WARNING: Do not write
func foo(x: any Sequence<Int>)when you meanfunc foo<S: Sequence>(x: S) where S.Element == Int. Both compile; the first boxes every call, the second specializes. For hot paths the difference is measurable.
Interview corner
Question: “Explain the difference between some View and any View in SwiftUI.”
Junior answer: “some is opaque, any is existential. They both mean ‘returns a View.’” → Definitions, no insight. Pass a screen.
Mid-level answer: “some View means the function returns a single concrete type conforming to View — the type is hidden from the caller but fixed at the compiler level. any View is a box that can hold any View at runtime; different instances can have different underlying types. SwiftUI’s body uses some View because that lets the framework’s diffing algorithm see the type structure and reuse views efficiently.” → Strong.
Senior answer: Plus: “The performance distinction is real but often misunderstood. some allows full monomorphization — no boxing, all method calls statically dispatched. any requires an existential container, witness tables, dynamic dispatch on every protocol method. For SwiftUI specifically, if you wrap your body in any View you defeat SwiftUI’s whole diffing strategy because the framework can’t see the type identity of subtrees — it has to assume every update changes the type and rebuild more aggressively. That’s why you’ll see @ViewBuilder and result builders return some View everywhere. Outside SwiftUI, any is the right tool when you genuinely need heterogeneous collections, but I’d default to some and only reach for any when I can’t otherwise satisfy the type system.” → Senior signal: understands the cost and the framework consequence.
Red-flag answer: “I just put some in front of every return type because that’s what Xcode autocompletes.” → Cargo-cult code.
Lab preview
Lab 1.C (Protocol-oriented calculator) and Lab 1.D (Async fetcher) both lean on generics — the calculator builds a generic Operation<Operand> protocol, the fetcher uses URLSession with Decodable generics.
Next: when things go wrong — Swift’s distinctive error-handling model. → Error handling