Skip to content
intermediate Phase 1 · Swift Foundations

Generics

Write generic functions, types, and constrained extensions for type-safe reusable code.

45m
3 problems
Topic Progress 0%

Generic Functions & Types

Generic Functions & Types

Generics allow you to write code that works with any type while maintaining type safety. They eliminate code duplication by abstracting over types.

Generic Functions

Without generics, you might write separate functions for each type:

func swapInts(_ a: inout Int, _ b: inout Int) {
    let temp = a; a = b; b = temp
}

func swapStrings(_ a: inout String, _ b: inout String) {
    let temp = a; a = b; b = temp
}

With generics, one function handles all types:

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

var x = 10, y = 20
swapValues(&x, &y)  // x=20, y=10

var a = "hello", b = "world"
swapValues(&a, &b)  // a="world", b="hello"

T is a type parameter—a placeholder for any type. The compiler infers the concrete type at the call site.

Generic Types

You can make structs, classes, and enums generic:

struct Stack<Element> {
    private var items: [Element] = []
    
    mutating func push(_ element: Element) {
        items.append(element)
    }
    
    mutating func pop() -> Element? {
        return items.popLast()
    }
    
    var count: Int { items.count }
    var isEmpty: Bool { items.isEmpty }
}

var intStack = Stack<Int>()
intStack.push(1)
intStack.push(2)
intStack.pop()  // Optional(2)

var stringStack = Stack<String>()
stringStack.push("hello")

Multiple Type Parameters

func merge<T, U>(_ a: T, _ b: U) -> String {
    return "\(a) + \(b)"
}

merge(1, "hello")  // "1 + hello"

Generic Enums

enum Result<Success, Failure> {
    case success(Success)
    case failure(Failure)
}

let result: Result<Int, String> = .success(42)

Generic Closures

func apply<T>(_ value: T, transform: (T) -> T) -> T {
    return transform(value)
}

let doubled = apply(5) { $0 * 2 }  // 10

Type Inference with Generics

The compiler usually infers generic types automatically:

func first<T>(_ array: [T]) -> T? {
    return array.first
}

let nums = [1, 2, 3]
let result = first(nums)  // type inferred as Int?

Generics are the foundation of Swift's type-safe collections and standard library. Mastering them unlocks powerful abstractions.

Type Constraints

Type Constraints

Type constraints restrict which types can be used as generic parameters, ensuring they have specific capabilities.

Basic Constraints

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

findIndex(of: 3, in: [1, 2, 3])     // Optional(2)
findIndex(of: "b", in: ["a", "b"])  // Optional(1)
// findIndex(of: [1], in: [[1]])     // Compile error — [Int] not Equatable

T: Equatable means T must conform to Equatable.

Multiple Constraints

func process<T: Codable & Hashable>(_ value: T) -> String {
    return "Processed: \(value)"
}

Where Clauses

For complex constraints, use a where clause:

func merge<C1: Container, C2: Container>(
    _ c1: C1, _ c2: C2
) where C1.Item == C2.Item, C1.Item: Equatable {
    // Both containers have the same Equatable item type
}

Constraining to Protocols

protocol Printable {
    func formatted() -> String
}

func printAll<T: Printable>(_ items: [T]) {
    items.forEach { print($0.formatted()) }
}

Constraining to Classes

Use AnyObject or class to restrict to reference types:

func referenceOnly<T: AnyObject>(_ value: T) {
    // Only classes can be passed
}

Default Generic Constraints

You can provide default type arguments:

struct Repository<T: Identifiable> {
    var items: [T] = []
    
    mutating func add(_ item: T) {
        items.append(item)
    }
    
    func find(byId id: T.ID) -> T? {
        items.first { $0.id == id }
    }
}

Opaque Types with Constraints

func makeCollection() -> some Collection where Element == Int {
    return [1, 2, 3]
}

Practical Example

protocol Cacheable: Codable {
    var cacheKey: String { get }
}

class Cache<T: Cacheable> {
    private var storage: [String: T] = [:]
    
    func store(_ item: T) {
        storage[item.cacheKey] = item
    }
    
    func retrieve(forKey key: String) -> T? {
        storage[key]
    }
}

struct User: Cacheable {
    let cacheKey: String
    let name: String
}

let userCache = Cache<User>()
userCache.store(User(cacheKey: "u1", name: "Alice"))
userCache.retrieve(forKey: "u1")  // Optional(User)

Type constraints ensure generic code is both flexible and correct, preventing runtime errors at compile time.

Associated Types

Associated Types

Associated types let protocols define placeholder types that conforming types fill in. They are the protocol equivalent of generic type parameters.

Basic Associated Type

protocol Container {
    associatedtype Item
    var count: Int { get }
    mutating func push(_ item: Item)
    mutating func pop() -> Item?
}

struct IntStack: Container {
    typealias Item = Int  // optional — compiler infers this
    private var items: [Int] = []
    
    var count: Int { items.count }
    mutating func push(_ item: Int) { items.append(item) }
    mutating func pop() -> Int? { items.popLast() }
}

struct StringStack: Container {
    // Item is inferred as String from push/pop signatures
    private var items: [String] = []
    
    var count: Int { items.count }
    mutating func push(_ item: String) { items.append(item) }
    mutating func pop() -> String? { items.popLast() }
}

Where Clauses with Associated Types

extension Container where Item: Equatable {
    func contains(_ item: Item) -> Bool {
        // simplified
        return false
    }
}

extension Container where Item == Int {
    func sum() -> Int {
        // simplified
        return 0
    }
}

Associated Type Constraints

You can constrain associated types to conform to protocols:

protocol IterableContainer {
    associatedtype Item: Equatable
    var items: [Item] { get }
}

Associated Type Inference

The compiler infers associated types from method signatures:

struct NumberCollection: IterableContainer {
    var items: [Int]  // Item inferred as Int
}

When to Use Associated Types vs Generics

Scenario Use
Protocol defining a type relationship associatedtype
Concrete type that can vary Generic <T>
Protocol with flexible return types associatedtype

Practical Example

protocol Repository {
    associatedtype Entity: Identifiable
    func findAll() -> [Entity]
    func findById(_ id: Entity.ID) -> Entity?
    func save(_ entity: Entity)
}

struct UserRepository: Repository {
    typealias Entity = User
    private var users: [User] = []
    
    func findAll() -> [User] { users }
    func findById(_ id: String) -> User? {
        users.first { $0.id == id }
    }
    func save(_ user: User) {
        users.append(user)
    }
}

struct User: Identifiable {
    let id: String
    var name: String
}

Associated types enable powerful abstractions in protocol-oriented design. They let protocols describe relationships between types without committing to specific concrete types.

Quiz

1. What does `<T>` represent in a generic function?

Question 1 options

2. What does `func foo<T: Equatable>(...)` constrain?

Question 2 options

3. What is an associated type in a protocol?

Question 3 options

4. When should you use `associatedtype` vs generic `<T>`?

Question 4 options

5. What does `where T: Codable & Hashable` do?

Question 5 options

Flashcards

Question

What is a generic function?

Answer

A function with a type parameter `<T>` that works with any type while maintaining type safety.

Question

What is a type constraint?

Answer

A restriction on generic parameters, e.g., `<T: Equatable>`, ensuring the type has specific capabilities.

Question

What is an associated type?

Answer

A placeholder type in a protocol that conforming types fill in, like `associatedtype Item`.

Question

How do you constrain an associated type?

Answer

Use `associatedtype Item: Protocol` to require the associated type to conform to a protocol.

Question

What does `typealias Item = Int` do in a conforming type?

Answer

Explicitly satisfies an associated type requirement by mapping it to a concrete type.

Revision Notes

Key Takeaways

  • 1. Generics eliminate code duplication while preserving type safety
  • 2. Type constraints ensure generic types have required capabilities
  • 3. Associated types define placeholder types in protocols
  • 4. Where clauses provide complex generic constraints
  • 5. The compiler infers generic types at the call site

Interview Tips

  • Explain when to use generics vs associated types
  • Know how to write type-constrained generic functions
  • Be able to design a generic Stack or Queue
  • Understand associated type constraints with where clauses

Cheat Sheet

Generics Cheat Sheet

Generic Function:

func swap<T>(_ a: inout T, _ b: inout T) { ... }

Generic Type:

struct Stack<Element> { ... }

Type Constraint:

func foo<T: Equatable>(...) { ... }
func bar<T: Codable & Hashable>(...) { ... }

Where Clause:

func baz<T>() where T: Comparable, T: Numeric { ... }

Associated Type:

protocol Container {
    associatedtype Item
    func push(_ item: Item)
}

Constraining Associated Type:

extension Container where Item: Equatable { ... }