Swift Generics — Type-Safe, Reusable Code with Examples
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
Over-constraining type parameters: Add constraints only when needed.
func printValue<T>(_ value: T)is fine if you only print it. AddT: CustomStringConvertibleonly when you need custom description.Using Any instead of generics:
Anybypasses type safety. Use generics to preserve type information:func Process<T>(_ items: [T])instead offunc process(_ items: [Any]).Forgetting that associated types cannot be used with existential types directly: You cannot write
let container: ContainerbecauseContainerhas an associated type. Use generic functions or opaque types (some Container).Creating unnecessary type parameters: If a type parameter is only used once or not constrained, consider whether the complexity is worth it.
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
- What is the difference between a generic function and a generic type?
- How do type constraints limit what types can be used with a generic?
- What is an associated type in a protocol?
- When would you use a generic where clause?
- 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)methoddequeue() -> T?methodpeek: T?computed propertycount: Intproperty- Conformance to
CustomStringConvertible - An extension that adds
filter(_ predicate: (T) -> Bool) -> [T]whereT: Equatable - Test with
Int,String, and a customstruct Tasktype
FAQ
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