Generics

Generics let you write flexible, reusable code that works with any type, subject to requirements you define. Instead of writing one function per concrete type, you write it once over a placeholder type and the compiler specializes it for every type you use it with — keeping full type safety and zero runtime cost. Generics are among Swift's most powerful features, and much of the standard library (Array, Dictionary, Optional) is built on them.

The problem generics solve

Suppose you need a function that swaps two values. Without generics, you'd write a separate near-identical function for every type, and they'd differ only in the type annotations.

func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temp = a; a = b; b = temp
}
func swapTwoStrings(_ a: inout String, _ b: inout String) {
    let temp = a; a = b; b = temp   // identical body, different type
}

The bodies are word-for-word the same. Duplicating logic like this is tedious and error-prone. Generics express the idea once: "swap two values of the same type, whatever that type is."

Generic functions and type parameters

A generic function declares one or more type parameters in angle brackets after its name. A type parameter is a placeholder — by convention a single capital letter like T — that stands in for an actual type chosen at each call site. Swift infers the concrete type from the arguments you pass.

func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let temp = a; a = b; b = temp
}

var x = 3, y = 107
swapTwoValues(&x, &y)
print(x, y)        // → 107 3   (T inferred as Int)

var hello = "hello", world = "world"
swapTwoValues(&hello, &world)
print(hello, world)  // → world hello   (T inferred as String)

Because both parameters are typed T, the compiler guarantees they're the same type — you can't accidentally swap an Int with a String. Use descriptive names when a placeholder has a clear meaning (Key, Value, Element); otherwise stick with T, U, V.

Note: generics are resolved entirely at compile time. Swift generates specialized, fully type-checked code for each type you use — there is no boxing or dynamic dispatch involved, unlike the any existentials covered in Opaque and Boxed Protocol Types.

Generic types: building a Stack

You can make your own types generic too. A generic type declares its type parameters after the type name, then uses them anywhere a type is expected. The classic example is a stack — a last-in, first-out collection — that should work for any element type.

struct Stack<Element> {
    private var items: [Element] = []

    var count: Int { items.count }
    var isEmpty: Bool { items.isEmpty }

    mutating func push(_ item: Element) {
        items.append(item)
    }
    mutating func pop() -> Element? {
        items.popLast()
    }
    func peek() -> Element? {
        items.last
    }
}

var stack = Stack<String>()
stack.push("uno")
stack.push("dos")
print(stack.count)     // → 2
print(stack.pop() ?? "")  // → dos

Here Element is the type parameter for the whole struct. When you write Stack<String>(), every appearance of Element becomes String. Swift can also infer the element type from context, so var ints = Stack<Int>() and assigning known values both work without restating the type.

Extending a generic type

When you extend a generic type, you do not repeat the type parameter list — the original parameter names (here Element) are already in scope inside the extension.

extension Stack {
    // Element is available without redeclaring it
    var top: Element? { peek() }
}

var numbers = Stack<Int>()
numbers.push(10)
numbers.push(20)
print(numbers.top ?? 0)   // → 20

You can also add a where clause to an extension so its members only exist when the element type meets extra requirements — see contextual where below.

Type constraints

Sometimes a generic function needs to do something with its values — compare them, hash them, add them. A bare type parameter offers no such capabilities. A type constraint requires a type parameter to inherit from a class or conform to a protocol, unlocking the operations that protocol guarantees. Write the constraint after a colon in the parameter list.

// T must be Equatable so we can use ==
func firstIndex<T: Equatable>(of value: T, in array: [T]) -> Int? {
    for (index, item) in array.enumerated() {
        if item == value {
            return index
        }
    }
    return nil
}

print(firstIndex(of: 9, in: [3, 9, 12]) ?? -1)   // → 1
print(firstIndex(of: "b", in: ["a", "b"]) ?? -1)  // → 1

Without the Equatable constraint, item == value wouldn't compile, because not every type supports ==. Constraints let the compiler verify your generic code is valid for every type it could ever be used with.

Associated types

Protocols can't take angle-bracket type parameters; instead they use associated types — placeholder type names declared with associatedtype that each conformer fills in. This is the protocol-side counterpart to a generic parameter. Here's a Container protocol; Stack conforms to it, and Swift infers the associated Item from the implementations.

protocol Container {
    associatedtype Item
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}

extension Stack: Container {
    // Item is inferred as Element
    mutating func append(_ item: Element) { push(item) }
    subscript(i: Int) -> Element { items[i] }
}

For this conformance, make items accessible (e.g. drop private) so the subscript can read it.

Constraints and generic where on associated types

An associated type can carry an inline constraint, and you can add a where clause to relate it to other associated types. This lets the protocol express precise relationships the compiler can rely on.

protocol SuffixableContainer: Container {
    // Suffix is itself a container of the same Item type
    associatedtype Suffix: SuffixableContainer where Suffix.Item == Item
    func suffix(_ size: Int) -> Suffix
}

protocol ComparableContainer: Container where Item: Comparable {
    // every conformer's Item is guaranteed Comparable
}

The where Suffix.Item == Item clause guarantees a container's suffix holds the same kind of element it does — the compiler enforces this for every conforming type.

Generic where clauses

A generic where clause lets you state requirements that can't fit in the parameter list — relationships between type parameters, or constraints on a type parameter's associated types. Write where just before the opening brace. This function compares two containers of possibly different types as long as their items match and are Equatable.

func allItemsMatch<C1: Container, C2: Container>(
    _ first: C1, _ second: C2
) -> Bool
where C1.Item == C2.Item, C1.Item: Equatable {
    if first.count != second.count { return false }
    for i in 0..<first.count {
        if first[i] != second[i] { return false }
    }
    return true
}

The clause reads: "C1 and C2 are both Containers, their Item types are the same, and that shared item type is Equatable." Inside the body the compiler then lets you use != and index both containers freely.

Contextual where clauses

You can attach a where clause to an individual method, property, or subscript inside a generic type's extension, so that member is available only when the condition holds — without writing a separate constrained extension. This is a contextual where clause.

extension Container {
    // only available when Item is Equatable
    func startsWith(_ item: Item) -> Bool where Item: Equatable {
        count >= 1 && self[0] == item
    }

    // only available when Item is an Int-like average can be taken
    func average() -> Double where Item == Int {
        var sum = 0
        for i in 0..<count { sum += self[i] }
        return Double(sum) / Double(count)
    }
}

Both methods live in one extension, but each carries its own requirement. The equivalent without contextual clauses would need two separate extension Container where ... blocks.

Generic subscripts

Subscripts can be generic, and they can carry their own type parameters and where clauses too. This subscript takes any sequence of indices and returns the matching items as an array.

extension Container {
    subscript<Indices: Sequence>(indices: Indices) -> [Item]
    where Indices.Element == Int {
        var result: [Item] = []
        for index in indices {
            result.append(self[index])
        }
        return result
    }
}

var letters = Stack<String>()
["a", "b", "c", "d"].forEach { letters.append($0) }
print(letters[[0, 2]])   // → ["a", "c"]

Tip: the generic parameter Indices belongs to the subscript, not the type, so it goes in angle brackets right after the subscript keyword. The where clause constrains it to sequences of integers.

Wrapping up

Generics let you write code once and reuse it across every type that meets your requirements, with full compile-time safety and no runtime overhead. You've seen the duplication problem they remove; generic functions and type parameters; building and extending a generic Stack; type constraints that unlock protocol operations; associated types with their own constraints and where clauses; generic where clauses relating multiple types; contextual where clauses on individual members; and generic subscripts. Together with protocols, generics are the backbone of the Swift standard library and of expressive, reusable Swift code.