Swift from Zero to Expert #14: Generics
One swapTwoValues, one Stack<Element>, one findIndex — written once, working for every type. Generics let the caller pick the concrete type, the compiler specialize the code, and the box from #13 disappear. Array, Dictionary, Optional, and Result were generic all along.
At the end of #13 we left a promise. any Shape and [any TextRepresentable] boxed their values at a runtime cost, and we said the answer was generics — where the caller picks one concrete type, the compiler specializes the code, and the box disappears. We’ve also been quietly using generics for chapters: Array<Element>, Dictionary<Key, Value>, Optional<Wrapped>, and Result<Success, Failure> are all generic types. Every [Int] you wrote was Array<Int>. This article is where that machinery finally gets its name.
Generics let you write flexible, reusable functions and types that work with any type, subject to requirements you define — without duplication, and without erasing type information the way any does. They’re one of the most powerful features of Swift, and much of the standard library is built with them.
any hides the concrete type behind a box and pays for it at runtime. A generic keeps the concrete type — the caller names it, the compiler bakes it in. Same flexibility, no box. That’s the trade the whole chapter turns on.
The problem generics solve
Here’s a perfectly ordinary, nongeneric function that swaps two Int values using the in-out parameters from #6:
func swapTwoInts(_ a: inout Int, _ b: inout Int) { let temporaryA = a a = b b = temporaryA}It works — but only for Int. Want to swap two Strings? Two Doubles? You write the same body again, changing nothing but the type:
func swapTwoStrings(_ a: inout String, _ b: inout String) { let temporaryA = a a = b b = temporaryA}
func swapTwoDoubles(_ a: inout Double, _ b: inout Double) { let temporaryA = a a = b b = temporaryA}The bodies are identical. The only difference is the type. That repetition is exactly the itch generics scratch — and notice the one rule hiding in plain sight: in every version, a and b must be the same type. Swift is type-safe; you can’t swap a String with a Double. Whatever we write next has to preserve that constraint.
Generic functions: a single type parameter <T>
A generic function works with any type. Here’s the generic version of all three functions above, called swapTwoValues(_:_:):
func swapTwoValues<T>(_ a: inout T, _ b: inout T) { let temporaryA = a a = b b = temporaryA}The body is unchanged. Only the first line differs — compare them:
func swapTwoInts(_ a: inout Int, _ b: inout Int)func swapTwoValues<T>(_ a: inout T, _ b: inout T)The brackets are how Swift knows T is a placeholder rather than an actual type named T — it won’t go looking for a type called T somewhere. And because both parameters are typed T, the type-safety rule survives: the caller can pass any type, as long as both arguments are the same type. The type to use for T is inferred from the arguments at each call:
var someInt = 3var anotherInt = 107swapTwoValues(&someInt, &anotherInt)// T is inferred to be Int; someInt is now 107, anotherInt is now 3
var someString = "hello"var anotherString = "world"swapTwoValues(&someString, &anotherString)// T is inferred to be String; someString is now "world", anotherString is now "hello"
Naming type parameters
Where a type parameter has a meaningful relationship to what it stands for, give it a descriptive name: Key and Value in Dictionary<Key, Value>, Element in Array<Element>. When there’s no such relationship — as with swapTwoValues — tradition uses single upper-camel-case letters: T, then U, then V.
Generic types: building Stack<Element> from the ground up
Generics aren’t just for functions — you can define your own generic types: classes, structures, and enumerations that work with any type, exactly like Array and Dictionary. Let’s build one: a stack, an ordered collection where you only ever add to the end (push) and remove from the end (pop) — strict last-in, first-out.
First, the nongeneric version, locked to Int:
struct IntStack { var items: [Int] = [] mutating func push(_ item: Int) { items.append(item) } mutating func pop() -> Int { return items.removeLast() }}It stores values in an [Int], and push/pop are mutating from #8 because they change the struct’s items. Useful — but forever an Int stack. Now the generic version:
struct Stack<Element> { var items: [Element] = [] mutating func push(_ item: Element) { items.append(item) } mutating func pop() -> Element { return items.removeLast() }}It’s the same code, with the actual type Int replaced by a type parameter Element, written in angle brackets after the struct’s name. Element is a placeholder used in three places: the items array’s element type, the push(_:) parameter type, and the pop() return type — all guaranteed to be the same type.
You create an instance by writing the concrete type in angle brackets — Stack<String>() for a stack of strings:
var stackOfStrings = Stack<String>()stackOfStrings.push("uno")stackOfStrings.push("dos")stackOfStrings.push("tres")stackOfStrings.push("cuatro")// the stack now contains 4 strings
let fromTheTop = stackOfStrings.pop()// fromTheTop is "cuatro", and the stack now contains 3 strings
Extending a generic type
When you extend a generic type from #9, you don’t repeat the type parameter list. The original parameter names are simply available inside the extension. Here’s an extension adding a read-only topItem that peeks at the top without popping:
extension Stack { var topItem: Element? { return items.isEmpty ? nil : items[items.count - 1] }}No <Element> after extension Stack — yet Element is in scope, used here as the optional from #11 Element?. topItem returns nil for an empty stack, otherwise the last item:
if let topItem = stackOfStrings.topItem { print("The top item on the stack is \(topItem).")}// Prints "The top item on the stack is tres."Type constraints: requiring conformance
swapTwoValues and Stack accept any type. But sometimes a generic needs its types to be able to do something. Dictionary, for instance, requires its keys to be Hashable from #4 — it can’t tell whether to insert or replace a key without a way to compare keys. That requirement is a type constraint.
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) { // function body goes here}Here T must be a subclass of SomeClass, and U must conform to SomeProtocol. The syntax is identical for generic types.
A findIndex that needs Equatable
Watch a constraint become necessary. Here’s a nongeneric function that finds the index of a String in an array of strings:
func findIndex(ofString valueToFind: String, in array: [String]) -> Int? { for (index, value) in array.enumerated() { if value == valueToFind { return index } } return nil}The principle isn’t string-specific, so let’s make it generic by replacing String with T. But this version does not compile:
func findIndex<T>(of valueToFind: T, in array: [T]) -> Int? { for (index, value) in array.enumerated() { if value == valueToFind { // ❌ error: not every T supports == return index } } return nil}The problem is value == valueToFind. Not every type in Swift can be compared with == — for a custom struct or class, Swift can’t guess what “equal” means. So it can’t guarantee this works for every possible T, and reports an error.
The fix is to promise that T supports ==. The standard library’s Equatable protocol requires exactly that — == and != — and every standard type conforms. Constrain T to Equatable:
func findIndex<T: Equatable>(of valueToFind: T, in array: [T]) -> Int? { for (index, value) in array.enumerated() { if value == valueToFind { return index } } return nil}<T: Equatable> reads “any type T that conforms to Equatable.” Now it compiles, and works for any equatable type:
let doubleIndex = findIndex(of: 9.3, in: [3.14159, 0.1, 0.25])// doubleIndex is nil — 9.3 isn't in the arraylet stringIndex = findIndex(of: "Andrea", in: ["Mike", "Malcolm", "Andrea"])// stringIndex is Optional(2)A type constraint isn’t bureaucracy — it’s the function telling the compiler precisely which capabilities it leans on. T: Equatable means “I’m going to use ==, so only let in types that have it.” The constraint is the contract that makes the body provably safe.
Associated types: a generic protocol
Here’s the bridge back to #13. A protocol can have a placeholder type too — and a protocol with an associated type is a generic protocol. Where a generic type writes <Element> in angle brackets, a protocol declares an associated type with the associatedtype keyword.
Consider a Container protocol: anything that can append an item, report its count, and retrieve items by subscript. But what type of item? That’s left open with an associated type called Item:
protocol Container { associatedtype Item mutating func append(_ item: Item) var count: Int { get } subscript(i: Int) -> Item { get }}Item is a placeholder for whatever element the container holds. The protocol guarantees that the value you append and the value the subscript from #9 returns are the same type — without ever naming that type. Each conformer decides.
A struct can conform and pin Item down. Here’s IntStack adapted — it sets Item to Int with a typealias:
struct IntStack: Container { // original IntStack implementation var items: [Int] = [] mutating func push(_ item: Int) { items.append(item) } mutating func pop() -> Int { return items.removeLast() } // conformance to the Container protocol typealias Item = Int mutating func append(_ item: Int) { self.push(item) } var count: Int { return items.count } subscript(i: Int) -> Int { return items[i] }}The generic Stack<Element> can conform too — and here Swift infers Item to be the type parameter Element:
struct Stack<Element>: Container { // original Stack<Element> implementation var items: [Element] = [] mutating func push(_ item: Element) { items.append(item) } mutating func pop() -> Element { return items.removeLast() } // conformance to the Container protocol mutating func append(_ item: Element) { self.push(item) } var count: Int { return items.count } subscript(i: Int) -> Element { return items[i] }}Conforming via an extension
Swift’s own Array already has append(_:), a count, and an Int subscript — it already meets every Container requirement. So, as with the empty-extension adoption from #13, one empty extension makes it official:
extension Array: Container {}Swift infers Item from Array’s existing members. After this, any Array is a Container.

Constraining an associated type
You can require the associated type itself to conform to a protocol, right where you declare it:
protocol Container { associatedtype Item: Equatable mutating func append(_ item: Item) var count: Int { get } subscript(i: Int) -> Item { get }}Now only types whose Item is Equatable can conform.
Generic where clauses: constraining associated types
Type constraints govern the type parameters in the brackets. But sometimes you need to constrain the associated types of those parameters — or require two associated types to be the same. That’s a generic where clause.
The classic example is allItemsMatch, which checks whether two containers hold the same items in the same order. The two containers needn’t be the same kind of container — but they must hold the same type of item, and that item must be Equatable:
func allItemsMatch<C1: Container, C2: Container> (_ someContainer: C1, _ anotherContainer: C2) -> Bool where C1.Item == C2.Item, C1.Item: Equatable {
// Check that both containers contain the same number of items. if someContainer.count != anotherContainer.count { return false }
// Check each pair of items to see if they're equivalent. for i in 0..<someContainer.count { if someContainer[i] != anotherContainer[i] { return false } }
// All items match, so return true. return true}Read the requirements off the signature: C1 and C2 both conform to Container (in the brackets); their Item types are identical — C1.Item == C2.Item — and that shared Item is Equatable (in the where clause). Those last two are exactly what makes someContainer[i] != anotherContainer[i] legal: the items are the same type, and that type supports !=.
Because of the where clause, a Stack<String> and a plain [String] — different container types, same Item — can be compared:
var stackOfStrings = Stack<String>()stackOfStrings.push("uno")stackOfStrings.push("dos")stackOfStrings.push("tres")
var arrayOfStrings = ["uno", "dos", "tres"]
if allItemsMatch(stackOfStrings, arrayOfStrings) { print("All items match.")} else { print("Not all items match.")}// Prints "All items match."Extensions with a generic where clause
You met this exact idea in #13’s constrained extensions — now you can see it’s a generic where clause. Extend Stack with an isTop(_:) method that only exists when Element is Equatable:
extension Stack where Element: Equatable { func isTop(_ item: Element) -> Bool { guard let topItem = items.last else { return false } return topItem == item }}Without the where, the == inside isTop(_:) wouldn’t compile — Stack doesn’t require equatable elements. The clause adds that requirement only for this extension, so isTop(_:) appears on a Stack<String> but not on a Stack<NotEquatable>:
if stackOfStrings.isTop("tres") { print("Top element is tres.")} else { print("Top element is something else.")}// Prints "Top element is tres."The same works on a protocol extension. Here Container gains startsWith(_:), but only when Item is Equatable:
extension Container where Item: Equatable { func startsWith(_ item: Item) -> Bool { return count >= 1 && self[0] == item }}A where clause can also pin an associated type to a specific type with ==, not just a protocol:
extension Container where Item == Double { func average() -> Double { var sum = 0.0 for index in 0..<count { sum += self[index] } return sum / Double(count) }}print([1260.0, 1200.0, 98.6, 37.0].average())// Prints "648.9"Contextual where clauses
When you’re already inside a generic context — a method or subscript on a generic type — you can attach a where clause to that one declaration, instead of writing a whole separate constrained extension. These are contextual where clauses:
extension Container { func average() -> Double where Item == Int { var sum = 0.0 for index in 0..<count { sum += Double(self[index]) } return sum / Double(count) } func endsWith(_ item: Item) -> Bool where Item: Equatable { return count >= 1 && self[count-1] == item }}let numbers = [1260, 1200, 98, 37]print(numbers.average()) // Prints "648.75"print(numbers.endsWith(37)) // Prints "true"average() is available only when Item == Int; endsWith(_:) only when Item: Equatable — both in the same extension. Without contextual clauses you’d need two separate extensions, one per requirement. They have identical behavior; the contextual form just lets related methods share one extension.
Associated types with their own where clause
An associated type can carry a where clause of its own. This is how the standard library’s Sequence ties its iterator to its elements — and you can do the same, requiring a container’s iterator to traverse the same Item type it stores:
protocol Container { associatedtype Item mutating func append(_ item: Item) var count: Int { get } subscript(i: Int) -> Item { get }
associatedtype Iterator: IteratorProtocol where Iterator.Element == Item func makeIterator() -> Iterator}The where Iterator.Element == Item guarantees the iterator yields exactly the type the container holds, whatever iterator type a conformer chooses. (IteratorProtocol / makeIterator() mirror the stdlib’s Sequence: an iterator is just the object a for-in loop pulls one element at a time from — you don’t need that deep dive to read the where pattern here.) And for a protocol that inherits another, you constrain an inherited associated type with a where clause on the declaration itself:
protocol ComparableContainer: Container where Item: Comparable { }Generic subscripts
Subscripts from #9 can be generic too — with their own type parameters in angle brackets after subscript, and their own where clause. This one takes any sequence of integer indices and returns the items at those positions:
extension Container { subscript<Indices: Sequence>(indices: Indices) -> [Item] where Indices.Iterator.Element == Int { var result: [Item] = [] for index in indices { result.append(self[index]) } return result }}Indices must conform to Sequence, and the where clause requires that sequence’s elements to be Int — so the parameter is provably a sequence of integers. Pass a Range, an Array<Int>, anything that fits.
Recap

- The problem — three identical
swapTwo*functions differ only in type; generics collapse them into one - Generic function —
func swapTwoValues<T>(...);Tis a type parameter (a placeholder), inferred at each call; both arguments must be the sameT - Naming — descriptive (
Element,Key,Value) when there’s a relationship, single letters (T,U,V) when not; always upper camel case - Generic type —
struct Stack<Element>; the placeholder threads through stored properties, parameters, and return types;[String]is justArray<String> - Extending a generic type — no type-parameter list on the extension; the original names (
Element) are already in scope - Type constraint —
<T: SomeClass>/<T: Protocol>;findIndex(of:in:)needsT: Equatableto use== - Associated type —
associatedtype Itemmakes a protocol generic; conformers fill it in (often inferred); the reasonsome(#15) exists - Generic
whereclause — constrains or equates associated types:allItemsMatchneedsC1.Item == C2.Item, C1.Item: Equatable - Constrained / contextual / associated-type
where— add requirements to extensions, single methods, or associated types - Generic subscript —
subscript<Indices: Sequence>(...) where ...— subscripts get type parameters andwhereclauses too
Challenges
Open a playground and try each one before opening the solution.
Challenge 1: Make it generic
This function returns the larger of two Ints. Rewrite it as a generic func maximum<T>(_:_:) that works for any comparable type. (Hint: it needs a constraint to use >.)
func maximumInt(_ a: Int, _ b: Int) -> Int { return a > b ? a : b}Show solution
func maximum<T: Comparable>(_ a: T, _ b: T) -> T { return a > b ? a : b}
print(maximum(3, 9)) // 9print(maximum("apple", "pear")) // pearprint(maximum(2.5, 1.1)) // 2.5The body uses >, which comes from the Comparable protocol — so T must be constrained to Comparable. Without the constraint, the compiler can’t guarantee every T supports > and rejects the code, exactly like the unconstrained findIndex. With it, T is inferred at each call: Int, then String, then Double.
Challenge 2: Why won’t it compile?
This generic Stack extension fails to build. Explain why, and fix it so peekEquals(_:) works.
struct Stack<Element> { var items: [Element] = [] mutating func push(_ item: Element) { items.append(item) } mutating func pop() -> Element { return items.removeLast() }}
extension Stack { func peekEquals(_ item: Element) -> Bool { return items.last == item // error }}Show solution
extension Stack where Element: Equatable { func peekEquals(_ item: Element) -> Bool { return items.last == item }}Stack<Element> places no constraint on Element, so inside a plain extension Stack there’s no guarantee that Element supports ==. The == on items.last == item is therefore illegal. Adding a generic where Element: Equatable clause to the extension supplies the missing requirement — peekEquals(_:) now exists only when the stack’s elements are equatable, which is precisely when == is available. (Note items.last is an Element?, and comparing an optional to a non-optional Element works because Optional itself is Equatable when Wrapped is.)
Challenge 3: Read the constraints
Without running it, what does this print — and why is the where clause necessary?
protocol Container { associatedtype Item var count: Int { get } subscript(i: Int) -> Item { get }}extension Array: Container {}
func firstsMatch<A: Container, B: Container>(_ a: A, _ b: B) -> Bool where A.Item == B.Item, A.Item: Equatable { guard a.count > 0, b.count > 0 else { return false } return a[0] == b[0]}
print(firstsMatch([1, 2, 3], [1, 9, 9]))print(firstsMatch(["x"], ["y"]))Show solution
truefalseThe first call compares 1 == 1 → true; the second compares "x" == "y" → false. The where clause does two essential jobs. A.Item == B.Item forces both containers to hold the same element type — without it, a[0] == b[0] could be comparing an Int to a String, which is meaningless. A.Item: Equatable guarantees that shared type supports == at all. Drop either requirement and the body stops compiling. This is the same machinery as allItemsMatch, trimmed to its first element.
What’s next
We’ve now seen all three doors out of “I don’t want to name the concrete type.” any (#13) boxes it and pays at runtime. Generics (this chapter) let the caller name it, with no box. The third door is opaque types — the some keyword — where a function hides one specific concrete type while keeping it fully concrete, and it’s the clean way to return a value of a protocol that has an associatedtype. That’s #15, next week.
See you then.
A generic is a promise kept twice: “write this once” to you, and “I’ll specialize it per type, with full type safety and no box” to the compiler. Once you see that Array, Dictionary, Optional, and Result were generics all along, the standard library stops being magic and starts being a worked example.
References
Related
-
- swift
- swift-zero-expert
- swift-fundamentals
Swift from Zero to Expert #15: Opaque & Boxed Protocol Types
The third door out of 'I don't want to name the concrete type'. some hides one concrete type while keeping its identity — no box, compile-time. any erases the type into a runtime box. This is how you finally return a protocol that has an associatedtype.
-
- swift
- swift-zero-expert
- swift-fundamentals
Swift from Zero to Expert #13: Protocols
A protocol is a contract, not a class. It says what a type must do without saying what it is — and that single idea powers delegation, synthesized Equatable, existentials, and the protocol extensions that make Swift idiomatic.
-
- swift
- swift-zero-expert
- swift-fundamentals
Swift from Zero to Expert #12: Error Handling
throw, try, do/catch, defer, rethrows, Result, and Swift 6 typed throws. An error isn't a magic exception — it's just a value of a type that conforms to Error, routed through your call stack as cheaply as a return.