Generics in Swift: Write Flexible, Reusable Code


Imagine you build a toy box that only holds Woody action figures. Then Buzz shows up, and you need a completely separate box — same shape, same lid, just a different label. Wouldn’t it be easier to build one box that works for any toy?

That’s exactly what generics do in Swift. You’ll learn how to write generic functions and types, add constraints, and see how Swift’s standard library already uses generics everywhere. We won’t cover associated types or opaque return types — those get their own posts.

What You’ll Learn

What Are Generics?

A generic is a placeholder for a type that gets filled in later. Instead of writing the same code once for Int, once for String, and once for Double, you write it once with a placeholder — and Swift figures out the actual type when you use it.

Apple Docs: Generics — The Swift Programming Language

Think of it like the claw machine at Pizza Planet in Toy Story. The claw doesn’t care whether it grabs a little green alien, a bouncy ball, or a stuffed bear — it works the same way regardless of what’s inside. Generics let your code be that claw: one mechanism, many types.

Generic Functions

A generic function uses a type placeholder (usually called T) inside angle brackets after the function name. This tells Swift: “I don’t know the exact type yet — the caller will decide.”

Here’s a function that swaps two values. Without generics, you’d need separate versions for every type:

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

var first = "Woody"
var second = "Buzz"
swapToys(&first, &second)
print(first)
print(second)
Buzz
Woody

The <T> after the function name declares a type parameter. When you call swapToys with two String values, Swift substitutes T with String. If you called it with two Int values, T would become Int. You write the logic once, and it works for any type.

The inout keyword means the function modifies the original values rather than copies. We covered this in functions.

Multiple Type Parameters

You can use more than one placeholder when your function works with different types:

func pairCharacter<A, B>(_ character: A, with item: B) -> (A, B) {
    return (character, item)
}

let pair = pairCharacter("Woody", with: 42)
print(pair)
("Woody", 42)

Here A is String and B is Int. Swift infers each placeholder independently.

Generic Types

You can also create generic types — structs, classes, or enums that work with any type. Let’s build a toy box that can hold any kind of item:

struct ToyBox<Item> {
    var items: [Item] = []

    mutating func add(_ item: Item) {
        items.append(item)
    }

    func peek() -> Item? {
        items.last
    }
}

Now you can create toy boxes for different types:

var nameBox = ToyBox<String>()
nameBox.add("Woody")
nameBox.add("Buzz")
print(nameBox.peek() ?? "Empty box")
Buzz
var ratingBox = ToyBox<Double>()
ratingBox.add(9.8)
ratingBox.add(8.5)
print(ratingBox.peek() ?? 0.0)
8.5

The <Item> placeholder in the struct definition becomes concrete when you write ToyBox<String> or ToyBox<Double>. The compiler enforces that you can only put String values in a ToyBox<String> — you get type safety without writing duplicate code.

Tip: Swift’s built-in Array<Element>, Dictionary<Key, Value>, and Optional<Wrapped> are all generic types. When you write [String], that’s shorthand for Array<String>.

Type Constraints

Sometimes you need your generic to do more than just hold a value — you need to compare, sort, or print it. Type constraints limit which types can fill the placeholder by requiring them to conform to a protocol.

Here’s a function that finds the best-rated item from an array. It needs to compare items, so T must conform to Comparable:

func bestRated<T: Comparable>(_ items: [T]) -> T? {
    items.max()
}

let scores = [7.2, 9.8, 8.1, 9.5]
if let best = bestRated(scores) {
    print("Best score: \(best)")
}
Best score: 9.8

The syntax <T: Comparable> means “T can be any type, as long as it conforms to Comparable.” If you tried passing a type that isn’t Comparable, Swift would catch it at compile time — not at runtime.

Constraints with where Clauses

For more complex constraints, use a where clause:

func printCollection<T>(_ items: [T]) where T: CustomStringConvertible {
    for item in items {
        print("🎬 \(item.description)")
    }
}

let movies = ["Toy Story", "Finding Nemo", "Up"]
printCollection(movies)
🎬 Toy Story
🎬 Finding Nemo
🎬 Up

The where clause does the same thing as writing <T: CustomStringConvertible>, but it reads more clearly when you have multiple constraints or when the constraint is long.

Common Mistakes

Using Any Instead of Generics

When beginners want flexibility, they sometimes reach for Any — Swift’s type that represents literally anything. But Any erases type information, which means you lose compile-time safety.

// ❌ Don't do this — you lose type safety
func firstItem(_ items: [Any]) -> Any? {
    items.first
}

let result = firstItem(["Woody", "Buzz"])
// result is Any?, not String? — you must cast it
// ✅ Do this instead — the compiler knows the exact type
func firstItem<T>(_ items: [T]) -> T? {
    items.first
}

let result = firstItem(["Woody", "Buzz"])
// result is String? — no casting needed

Generics preserve type information. With Any, the compiler can’t help you catch mistakes. With generics, it can.

Forgetting Type Constraints

If your generic function needs to compare values, you must tell Swift by adding a constraint. Without it, the compiler doesn’t know the type supports comparison:

// ❌ Won't compile — T doesn't guarantee ==
func contains<T>(_ items: [T], _ target: T) -> Bool {
    items.contains { $0 == target }
}
// ✅ Add the Equatable constraint
func contains<T: Equatable>(_ items: [T], _ target: T) -> Bool {
    items.contains { $0 == target }
}

print(contains(["Woody", "Buzz", "Jessie"], "Buzz"))
true

The fix is simple: add : Equatable (or whatever protocol you need) to the type parameter.

What’s Next?

  • Generics let you write code once that works with many types
  • Type parameters (<T>) are placeholders filled in by the caller
  • Generic types (like ToyBox<Item>) create reusable containers
  • Type constraints (<T: Protocol>) limit generics to types with specific abilities
  • Swift’s standard library — Array, Dictionary, Optional — is built on generics

Now that you understand generics, you’re ready to explore Property Wrappers in Swift — a feature that uses generics under the hood to add custom behavior to properties.