Skip to content

Swift Generics — Type-Safe, Reusable Code with Examples

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about Swift Generics. We cover key concepts, practical examples, and best practices to help you master this topic.

Swift generics enable you to write flexible, reusable functions and types that can work with any type while maintaining compile-time type safety, eliminating the need for duplication and runtime type casting.

What You'll Learn

  • Generic functions and type parameters
  • Generic types (structs, classes, enums)
  • Type constraints with protocols
  • Associated types in protocols
  • Generic where clauses for complex constraints
  • Real-world generic patterns

Why It Matters

Generics are central to Swift's standard library. Array<T>, Dictionary<K, V>, Optional<T>, and Result<T, E> are all generic types. Without generics, you would write separate versions of every collection and algorithm for every data type you work with. Mastering generics lets you build abstractions that are both flexible and completely type-safe.

Real-World Use

A networking library uses a generic function fetch<T: Decodable>(from url: URL) -> T that can fetch any Decodable type from an API endpoint. The same function handles User, Product, or Order JSON deserialization without any code duplication, catching type mismatches at compile time.

Learning Path

flowchart LR
  A[Extensions
Lesson 14] --> B[Generics
You are here] B --> C[Enums
Lesson 16] B --> D[Protocols
Lesson 13] style B fill:#f90,color:#fff

Generic Functions

A generic function uses placeholder type parameters instead of concrete types. The actual type is determined when the function is called.

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

var x = 10
var y = 20
print("Before swap: x = \(x), y = \(y)")
swapValues(&x, &y)
print("After swap: x = \(x), y = \(y)")

var firstName = "Alice"
var lastName = "Bob"
swapValues(&firstName, &lastName)
print("Names: \(firstName), \(lastName)")

Output:

Before swap: x = 10, y = 20
After swap: x = 20, y = 10
Names: Bob, Alice

The <T> declares a generic type parameter. The function works with any type as long as both parameters are the same type. Swift infers T as Int for the first call and String for the second.

Generic Types

You can create entire types (structs, classes, enums) that operate on generic type parameters.

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

    mutating func push(_ item: T) {
        items.append(item)
    }

    mutating func pop() -> T? {
        return items.popLast()
    }

    var peek: T? {
        return items.last
    }

    var count: Int {
        return items.count
    }

    var isEmpty: Bool {
        return items.isEmpty
    }
}

var intStack = Stack<Int>()
intStack.push(1)
intStack.push(2)
intStack.push(3)
print("Stack count: \(intStack.count)")
print("Peek: \(intStack.peek ?? 0)")
while let value = intStack.pop() {
    print("Popped: \(value)")
}

var stringStack = Stack<String>()
stringStack.push("Hello")
stringStack.push("World")
print("String stack peek: \(stringStack.peek ?? "")")

Output:

Stack count: 3
Peek: 3
Popped: 3
Popped: 2
Popped: 1
String stack peek: World

The same Stack implementation works for any type. You can create stacks of Int, String, User, or any other type without writing separate implementations.

Type Constraints

Type constraints specify that a generic type parameter must inherit from a specific class or conform to a specific protocol.

func findIndex<T: Equatable>(of valueToFind: T, in array: [T]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}

let numbers = [10, 20, 30, 40, 50]
if let index = findIndex(of: 30, in: numbers) {
    print("Found 30 at index \(index)")
}

let names = ["Alice", "Bob", "Charlie"]
if let index = findIndex(of: "Bob", in: names) {
    print("Found Bob at index \(index)")
}

struct Person: Equatable {
    let name: String
    let age: Int
}

let people = [Person(name: "Alice", age: 30), Person(name: "Bob", age: 25)]
if let index = findIndex(of: Person(name: "Alice", age: 30), in: people) {
    print("Found Alice at index \(index)")
}

Output:

Found 30 at index 2
Found Bob at index 1
Found Alice at index 0

The constraint T: Equatable ensures the == operator is available. Without this constraint, Swift would not allow value == valueToFind.

Associated Types

Protocols use associated types with the associatedtype keyword to define generic placeholders that conforming types specify.

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

struct IntBox: Container {
    typealias Item = Int
    private var items: [Int] = []

    var count: Int {
        return items.count
    }

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

    subscript(index: Int) -> Int {
        return items[index]
    }
}

struct GenericBox<T>: Container {
    private var items: [T] = []

    var count: Int {
        return items.count
    }

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

    subscript(index: Int) -> T {
        return items[index]
    }
}

var intBox = IntBox()
intBox.append(42)
print("IntBox: \(intBox[0])")

var stringBox = GenericBox<String>()
stringBox.append("Generics")
print("GenericBox: \(stringBox[0])")

Output:

IntBox: 42
GenericBox: Generics

IntBox explicitly sets typealias Item = Int, while GenericBox<T> lets Swift infer that Item is T.

Generic Where Clauses

Where clauses add more specific constraints beyond simple protocol conformance.

func allItemsEqual<T: Container>(_ container1: T, _ container2: T) -> Bool
    where T.Item: Equatable {
    guard container1.count == container2.count else { return false }
    for i in 0..<container1.count {
        if container1[i] != container2[i] {
            return false
        }
    }
    return true
}

extension Array: Container where Element: Equatable {
    // Container already works for Array
}

let array1 = [1, 2, 3]
let array2 = [1, 2, 3]
let array3 = [1, 2, 4]
print("Array 1 == Array 2: \(allItemsEqual(array1, array2))")
print("Array 1 == Array 3: \(allItemsEqual(array1, array3))")

extension Container where Item: Hashable {
    func uniqueItems() -> [Item] {
        var seen = Set<Item>()
        var result: [Item] = []
        for i in 0..<count {
            let item = self[i]
            if !seen.contains(item) {
                seen.insert(item)
                result.append(item)
            }
        }
        return result
    }
}

let numbers = [1, 2, 2, 3, 1, 4, 5, 3]
print("Unique: \(numbers.uniqueItems())")

Output:

Array 1 == Array 2: true
Array 1 == Array 3: false
Unique: [1, 2, 3, 4, 5]

Where clauses let you add methods that are only available when the generic type meets specific conditions.

Generic Enum with Associated Values

Enums can be generic, which is especially useful for result types.

enum Result<T, E: Error> {
    case success(T)
    case failure(E)

    func map<U>(_ transform: (T) -> U) -> Result<U, E> {
        switch self {
        case .success(let value):
            return .success(transform(value))
        case .failure(let error):
            return .failure(error)
        }
    }
}

enum NetworkError: Error {
    case notFound
    case timeout
    case serverError(String)
}

let result: Result<String, NetworkError> = .success("Data received")
let mappedResult = result.map { $0.count }
// mappedResult is Result<Int, NetworkError>

switch mappedResult {
case .success(let count):
    print("Data length: \(count)")
case .failure(let error):
    print("Error: \(error)")
}

Output: Data length: 13

The generic Result type works with any success type and any error type.

Real-World Generic Networking

Here is a practical generic networking pattern used in production apps:

import Foundation

protocol APIRequest {
    associatedtype Response: Decodable
    var path: String { get }
    var method: String { get }
}

struct GetUserRequest: APIRequest {
    typealias Response = User

    let userID: Int
    var path: String { return "/users/\(userID)" }
    var method: String { return "GET" }
}

struct User: Decodable {
    let id: Int
    let name: String
    let email: String
}

class APIClient {
    let baseURL = "https://api.example.com"

    func perform<T: APIRequest>(_ request: T) async throws -> T.Response {
        let url = URL(string: "\(baseURL)\(request.path)")!
        var urlRequest = URLRequest(url: url)
        urlRequest.httpMethod = request.method

        let (data, _) = try await URLSession.shared.data(for: urlRequest)
        let decoded = try JSONDecoder().decode(T.Response.self, from: data)
        return decoded
    }
}

// Usage (would need actual server):
// let client = APIClient()
// let user = try await client.perform(GetUserRequest(userID: 42))
// print(user.name)

The generic perform method works with any APIRequest and automatically decodes the response into the correct type. Adding a new API endpoint only requires a new struct conforming to APIRequest.

Common Mistakes

  1. Over-constraining type parameters: Add constraints only when needed. func printValue<T>(_ value: T) is fine if you only print it. Add T: CustomStringConvertible only when you need custom description.

  2. Using Any instead of generics: Any bypasses type safety. Use generics to preserve type information: func Process<T>(_ items: [T]) instead of func process(_ items: [Any]).

  3. Forgetting that associated types cannot be used with existential types directly: You cannot write let container: Container because Container has an associated type. Use generic functions or opaque types (some Container).

  4. Creating unnecessary type parameters: If a type parameter is only used once or not constrained, consider whether the complexity is worth it.

  5. Not using where clauses for conditional functionality: Add methods that only exist when the generic parameter meets certain conditions to keep APIs clean and focused.

Practice Questions

  1. What is the difference between a generic function and a generic type?
  2. How do type constraints limit what types can be used with a generic?
  3. What is an associated type in a protocol?
  4. When would you use a generic where clause?
  5. Challenge: Create a generic Cache<Key: Hashable, Value> type that stores key-value pairs, has a configurable maximum size, and automatically evicts the least recently used entry when full.

Mini Project

Build a generic Queue<T> type with:

  • enqueue(_ item: T) method
  • dequeue() -> T? method
  • peek: T? computed property
  • count: Int property
  • Conformance to CustomStringConvertible
  • An extension that adds filter(_ predicate: (T) -> Bool) -> [T] where T: Equatable
  • Test with Int, String, and a custom struct Task type

FAQ

Can I use generics with protocols?

Yes. Protocols can have associated types that work like generic type parameters. You can also use generic functions that constrain type parameters to protocol conformances.

What is the difference between generics and Any?

Generics preserve type information at compile time, giving you type safety and better performance. Any erases type information and requires runtime casting.

Can a generic type have multiple type parameters?

Yes. Write struct Pair<T, U> or func compare<T, U>(_ a: T, _ b: U) with as many type parameters as needed.

What is an opaque type (some) vs generics?

Opaque types (some View) hide the concrete type from the caller while preserving type identity. Generics let the caller specify the type. Use some when you want to hide implementation details.

Can I constrain a generic to be a class?

Yes. Use T: AnyObject to restrict to reference types, but prefer protocol constraints for most use cases.

What's Next

With generics mastered, learn about Enums and how they work with associated values and pattern matching for expressive, type-safe code.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro