Generics
Swift’s generics admit writing functions and types parameterised by other types. The principal forms: generic functions, generic types (struct, class, enum), generic protocols (with associatedtype), generic constraints (via where and :), opaque types (some Protocol), and existential types (any Protocol). The combination — substantial type-level expressiveness, type inference at the call site, the protocol-with-associated-types form (PATs), the some/any distinction (Swift 5.1+/5.6+) — is the substance of Swift’s generic surface. The conventional Swift discipline favours protocols with associated types over concrete generic types; the protocol-oriented programming style admits substantial flexibility.
Generic functions
Type parameters appear in angle brackets after the function name:
func swapValues<T>(_ a: inout T, _ b: inout T) {
let temp = a
a = b
b = temp
}
var x = 1
var y = 2
swapValues(&x, &y) // T inferred as Int
var s1 = "hello"
var s2 = "world"
swapValues(&s1, &s2) // T inferred as String
The inout admits in-place modification; treated in Functions and closures.
For multiple type parameters:
func pair<A, B>(_ a: A, _ b: B) -> (A, B) {
(a, b)
}
let p = pair(1, "hello") // (Int, String)
Generic constraints
Constraints admit “this type must conform to a protocol”:
func max<T: Comparable>(_ a: T, _ b: T) -> T {
a > b ? a : b
}
max(3, 5) // 5
max("a", "b") // "b"
max(3.0, 5.0) // 5.0
The T: Comparable admits using <, >, <=, >= on T.
For substantial constraints, the where clause:
func sum<T: Sequence>(_ s: T) -> T.Element
where T.Element: Numeric {
s.reduce(0, +)
}
sum([1, 2, 3]) // 6
sum([1.0, 2.0, 3.0]) // 6.0
The where admits substantial expressiveness — constraints on associated types, multiple-type relationships, etc.
Generic types
Type parameters in struct/class/enum:
struct Stack<Element> {
private var items: [Element] = []
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element? {
items.popLast()
}
var top: Element? {
items.last
}
}
var s = Stack<Int>()
s.push(1)
s.push(2)
let top = s.pop() // Optional<Int>
var t = Stack<String>()
t.push("hello")
Generic enums:
enum Result<Success, Failure: Error> {
case success(Success)
case failure(Failure)
}
let ok: Result<Int, MyError> = .success(42)
let err: Result<Int, MyError> = .failure(.timeout)
The standard library’s Result<Success, Failure> (introduced in Swift 5.0) follows this pattern.
Type inference for generics
Type arguments are typically inferred from the usage:
func first<T>(_ array: [T]) -> T? {
array.first
}
let n = first([1, 2, 3]) // T inferred as Int; returns Int?
let s = first(["a", "b"]) // T inferred as String
// Explicit type arguments (rarely needed):
let x = first<Int>([1, 2, 3]) // not admitted in this position
let x: Int? = first([1, 2, 3]) // disambiguate via return type
Swift admits substantial inference; explicit type arguments are conventional only when inference fails or for clarity.
Generic protocols and associated types
Protocols admit associated types — placeholders that conformers fill in:
protocol Container {
associatedtype Item
var count: Int { get }
mutating func append(_ item: Item)
subscript(i: Int) -> Item { get }
}
struct IntStack: Container {
typealias Item = Int // explicit (often optional)
var items: [Int] = []
var count: Int { items.count }
mutating func append(_ item: Int) { items.append(item) }
subscript(i: Int) -> Int { items[i] }
}
struct GenericStack<T>: Container {
var items: [T] = []
var count: Int { items.count }
mutating func append(_ item: T) { items.append(item) }
subscript(i: Int) -> T { items[i] }
// Item inferred as T
}
The conformance is satisfied when the type provides the associated types and required members. The Item may be inferred from the methods (append, subscript).
Constraints on associated types
protocol Sequence {
associatedtype Element
associatedtype Iterator: IteratorProtocol where Iterator.Element == Element
}
protocol Numeric: Comparable, ExpressibleByIntegerLiteral {
associatedtype Magnitude: Comparable, Numeric
var magnitude: Magnitude { get }
static func * (lhs: Self, rhs: Self) -> Self
// ...
}
The where admits substantial constraints — associated types must satisfy specific protocols and relationships.
Opaque types some
The some keyword (Swift 5.1+) admits an opaque return type — the function returns some specific type conforming to the protocol, but the caller doesn’t see which type:
func makeShape() -> some Shape { // returns a specific Shape; caller doesn't know which
Circle(radius: 5)
}
let s = makeShape() // type: some Shape
// caller can call Shape methods; not access Circle specifics
The mechanism admits substantial type abstraction without exposing the concrete type. Conventional in SwiftUI:
struct ContentView: View {
var body: some View { // some specific View type
VStack {
Text("Hello")
Button("Click me") { /* ... */ }
}
}
}
The some View admits the compiler optimising the rendering without exposing the concrete view-tree type.
Existential types any
The any keyword (Swift 5.6+) admits an existential type — a value of any type conforming to the protocol:
let shapes: [any Shape] = [Circle(radius: 5), Square(side: 4), Triangle(...)]
for shape in shapes {
print(shape.area()) // calls the conforming type's method
}
The mechanism admits heterogeneous collections of conforming types; each element may be a different concrete type.
The principal differences:
| Form | Description |
|---|---|
some Protocol | Specific (but unnamed) conforming type. Static dispatch. |
any Protocol | Any conforming type, dynamic. Existential. Dynamic dispatch. |
The conventional discipline:
- Use
somewhen the type is fixed but the caller needn’t know it. - Use
anywhen the value may be of different conforming types.
// `some` for "I'm returning some specific Shape":
func makeShape(kind: String) -> some Shape { /* always returns the same type */ }
// `any` for "the parameter may be any Shape":
func process(_ shape: any Shape) { /* ... */ }
// `any` for heterogeneous collection:
let mixed: [any Shape] = [...]
Pre-5.6, Shape (without any) was admitted; the explicit any is the conventional contemporary form.
Generic where clauses
The where admits constraints across the generic parameters:
extension Sequence where Element: Numeric {
func sum() -> Element {
reduce(0, +)
}
}
[1, 2, 3].sum() // 6 (Int)
[1.0, 2.0, 3.0].sum() // 6.0 (Double)
// ["a", "b"].sum() // ERROR: String is not Numeric
The mechanism admits substantial conditional functionality — methods are admitted only when the constraint is satisfied.
Generic subscripts
struct Container<T> {
var items: [T]
subscript<I: Sequence>(indices: I) -> [T] where I.Element == Int {
indices.map { items[$0] }
}
}
let c = Container(items: [10, 20, 30, 40, 50])
let selected = c[[0, 2, 4]] // [10, 30, 50]
Self requirements
The Self keyword in protocols admits “the conforming type”:
protocol Cloneable {
func clone() -> Self
}
struct Point: Cloneable {
let x: Int
let y: Int
func clone() -> Self { // Self = Point
Point(x: x, y: y)
}
}
The mechanism admits substantial fluent APIs — methods that return the conforming type rather than the protocol type.
Common patterns
Generic function with constraint
func findMax<T: Comparable>(in collection: [T]) -> T? {
collection.max()
}
Generic stack
struct Stack<Element> {
private var items: [Element] = []
var isEmpty: Bool { items.isEmpty }
var count: Int { items.count }
var top: Element? { items.last }
mutating func push(_ item: Element) {
items.append(item)
}
@discardableResult
mutating func pop() -> Element? {
items.popLast()
}
}
Generic Result
enum Result<Success, Failure: Error> {
case success(Success)
case failure(Failure)
func map<NewSuccess>(_ transform: (Success) -> NewSuccess) -> Result<NewSuccess, Failure> {
switch self {
case .success(let value): return .success(transform(value))
case .failure(let error): return .failure(error)
}
}
}
Protocol with associated types
protocol Repository {
associatedtype Entity: Identifiable
associatedtype Query
func find(_ id: Entity.ID) async throws -> Entity?
func search(_ query: Query) async throws -> [Entity]
}
struct UserRepository: Repository {
typealias Entity = User
typealias Query = String
func find(_ id: User.ID) async throws -> User? { /* ... */ }
func search(_ query: String) async throws -> [User] { /* ... */ }
}
Conditional conformance
extension Array: Equatable where Element: Equatable {
// [Element] is Equatable when Element is Equatable
}
// Now:
let a: [Int] = [1, 2, 3]
let b: [Int] = [1, 2, 3]
a == b // true
The mechanism admits substantial conditional protocol conformance — synthesised in many cases for Equatable, Hashable, Codable.
some for view return
struct ContentView: View {
var body: some View {
VStack {
Text("Hello")
Button("Click") { /* ... */ }
}
}
}
any for heterogeneous collection
protocol Animal {
func sound() -> String
}
struct Dog: Animal { func sound() -> String { "Woof" } }
struct Cat: Animal { func sound() -> String { "Meow" } }
struct Cow: Animal { func sound() -> String { "Moo" } }
let zoo: [any Animal] = [Dog(), Cat(), Cow()]
for animal in zoo {
print(animal.sound())
}
Generic factory
func make<T: ExpressibleByIntegerLiteral>() -> T {
return 42 // T must admit Int conversion
}
let i: Int = make()
let d: Double = make()
Generic with multiple constraints
func merge<T, U>(_ a: T, _ b: U) -> [String: Any]
where T: Encodable, U: Encodable {
let aData = try! JSONEncoder().encode(a)
let bData = try! JSONEncoder().encode(b)
var aDict = try! JSONSerialization.jsonObject(with: aData) as! [String: Any]
let bDict = try! JSONSerialization.jsonObject(with: bData) as! [String: Any]
aDict.merge(bDict) { _, new in new }
return aDict
}
Generic constraint on associated type
extension Sequence where Element: Hashable {
func uniqued() -> [Element] {
var seen = Set<Element>()
return filter { seen.insert($0).inserted }
}
}
[1, 1, 2, 3, 3].uniqued() // [1, 2, 3]
Type erasure
When some or any doesn’t fit, type erasure admits hiding the concrete type:
struct AnyAnimal: Animal {
private let _sound: () -> String
init<A: Animal>(_ animal: A) {
self._sound = animal.sound
}
func sound() -> String {
_sound()
}
}
let erased: [AnyAnimal] = [AnyAnimal(Dog()), AnyAnimal(Cat())]
Pre-Swift 5.6, type erasure was the conventional substitute for any; today, any Protocol is conventional.
A note on PATs
Protocols with associated types (PATs) are not directly usable as types pre-Swift 5.7:
protocol Container {
associatedtype Item
func first() -> Item?
}
// Pre-5.7:
// let c: Container = ... // ERROR: PATs not usable directly
// Solution 1: generic function:
func process<C: Container>(_ container: C) {
// ...
}
// Solution 2: type erasure (AnyContainer):
struct AnyContainer<T>: Container { /* ... */ }
// Swift 5.7+ via existentials:
let c: any Container = ...
Swift 5.7+ admits PATs via any for substantial flexibility.
A note on the conventional discipline
The contemporary Swift generics advice:
- Use generics for genuinely generic code.
- Use
T: Protocolconstraints where applicable. - Use
whereclauses for substantial constraints. - Use protocols with associated types over concrete generic types.
- Use
somefor opaque return types (SwiftUI body, etc.). - Use
anyfor heterogeneous collections of conforming types. - Use type erasure (
Any...) when neithersomenoranyfits. - Trust type inference — explicit type arguments are rarely needed.
- Use conditional conformance (
extension X: Y where ...) for substantial conformances. - Use
Selfin protocols for “the conforming type”.
The combination — generic functions, types, protocols with associated types, some/any for type abstraction, conditional conformance, the where clause — is the substance of Swift’s generics. The discipline produces substantial type-safe abstraction with substantial flexibility for protocol-oriented design.