What are Swift Generics?
💡 The simplest explanation
Generics allow us to write one reusable implementation that works with many different concrete types while preserving type safety.
Generics allow functions and types to work with different concrete types without requiring us to duplicate the same implementation.
Instead of writing one function for Int, another for String and another for every type we may introduce later, we can write one generic function.
The caller supplies the concrete type when the function is used.
Swift then verifies that every operation remains type-safe.
The Problem Generics Solve
Suppose we want to write a function that swaps two integer values.
func swapIntegers(_ first: inout Int, _ second: inout Int) {
let temporaryValue = first
first = second
second = temporaryValue
}
We can use it like this:
var firstNumber = 10
var secondNumber = 20
swapIntegers(&firstNumber, &secondNumber)
print(firstNumber) // 20
print(secondNumber) // 10
This works perfectly for Int values.
However, suppose we also need to swap two String values.
Our existing function cannot accept them because its parameters are specifically declared as Int.
var firstName = "Milo"
var secondName = "Luna"
// Error:
// swapIntegers(&firstName, &secondName)
We could write another function.
func swapStrings(_ first: inout String, _ second: inout String) {
let temporaryValue = first
first = second
second = temporaryValue
}
However, the implementation is exactly the same.
Only the types have changed.
If we later need to swap Double, Boolean, Dog or Cat values, we would have to continue creating more versions of the same function.
This produces duplicated code.
Replacing the Concrete Type With a Placeholder
A generic function replaces a specific concrete type with a placeholder type.
func swapValues<T>(_ first: inout T, _ second: inout T) {
let temporaryValue = first
first = second
second = temporaryValue
}
The placeholder type is named T.
It does not mean that Swift has created a new concrete type called T.
It means:
This function can work with some concrete type, and that concrete type will be decided when the function is called.
The same implementation can now swap Int values.
var firstNumber = 10
var secondNumber = 20
swapValues(&firstNumber, &secondNumber)
It can also swap String values.
var firstName = "Milo"
var secondName = "Luna"
swapValues(&firstName, &secondName)
It can even swap instances of our own custom types.
struct Dog {
let name: String
}
var firstDog = Dog(name: "Milo")
var secondDog = Dog(name: "Buddy")
swapValues(&firstDog, &secondDog)
We have written the implementation once.
The caller chooses the concrete type through the values passed into the function.
Understanding the Generic Syntax
The generic type parameter appears between angle brackets after the function name.
func swapValues<T>(_ first: inout T, _ second: inout T) {
// Implementation
}
The declaration <T> introduces a generic type parameter named T.
The two parameters then use T as their type.
first: inout T
second: inout T
This tells Swift that both arguments must have the same concrete type.
The function can work with any type, but it cannot swap an Int with a String.
var number = 42
var word = "Swift"
// Error:
// swapValues(&number, &word)
For this call, T would need to be both Int and String.
That is impossible.
Generics Preserve Type Information
Generics do not discard the concrete type.
When the generic function is called with two Int values, T represents Int for that call.
swapValues(&firstNumber, &secondNumber)
Conceptually, Swift can treat that call as though the function were working with Int.
When the same function is called with two String values, T represents String.
swapValues(&firstName, &secondName)
The generic implementation remains the same, but the concrete type is preserved.
Important
A generic type parameter is not an untyped value.
It represents a real concrete type that Swift knows and checks.
Why Not Just Use Any?
We could attempt to make the function flexible by accepting Any.
func swapAnyValues(_ first: inout Any, _ second: inout Any) {
let temporaryValue = first
first = second
second = temporaryValue
}
However, Any removes important relationships between the arguments.
The first value could be an Int while the second is a String.
var first: Any = 42
var second: Any = "Swift"
swapAnyValues(&first, &second)
Swift allows this because both values have the static type Any.
That is not the guarantee we wanted.
Our generic function expresses a stronger rule:
The function may work with any concrete type, but both values must have the same concrete type.
This is one of the most important benefits of generics.
They provide flexibility without abandoning type safety.
Generic Functions
Functions can use generic type parameters in their inputs, outputs or both.
Consider a function that returns the first item from an array.
func firstItem<T>(in items: [T]) -> T? {
items.first
}
The function can work with an array containing any element type.
let firstNumber = firstItem(in: [10, 20, 30])
let firstName = firstItem(in: ["Milo", "Luna"])
let firstFlag = firstItem(in: [true, false])
Swift infers a different result type for each call.
// firstNumber is Int?
// firstName is String?
// firstFlag is Bool?
The relationship between the array element and the return value is preserved.
An array of Int values produces an Int result.
An array of String values produces a String result.
Generic Types
Entire types can also be generic.
Suppose we want to create a stack.
A stack stores values in a last-in, first-out order.
The most recently added value is the first value removed.
We could begin with a stack that stores only String values.
struct StringStack {
private var items: [String] = []
mutating func push(_ item: String) {
items.append(item)
}
mutating func pop() -> String? {
items.popLast()
}
}
This works, but only for String.
A generic stack can store any one concrete element type.
struct Stack<Element> {
private var items: [Element] = []
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element? {
items.popLast()
}
}
Here, the generic type parameter is named Element.
This is more descriptive than T because it represents the type of value stored by the stack.
We can create a stack of String values.
var names = Stack<String>()
names.push("Milo")
names.push("Luna")
let latestName = names.pop()
We can also create a completely separate stack of Int values.
var numbers = Stack<Int>()
numbers.push(10)
numbers.push(20)
let latestNumber = numbers.pop()
Each Stack instance has one fixed Element type.
A Stack<String> can store only String values.
var names = Stack<String>()
names.push("Milo")
// Error:
// names.push(42)
The Stack implementation is reusable, but every individual stack remains type-safe.
Generic Type Parameters Can Have Descriptive Names
Single-letter names such as T are common when the meaning is obvious.
func identity<T>(_ value: T) -> T {
value
}
More descriptive names are often better when the generic parameter has a clear role.
struct Pair<FirstValue, SecondValue> {
let first: FirstValue
let second: SecondValue
}
This generic type has two independent type parameters.
let result = Pair(
first: "Score",
second: 100
)
Swift infers:
Pair<String, Int>
The two stored values do not need to have the same type because they are represented by different generic parameters.
Multiple Generic Type Parameters
A function can also introduce more than one generic type parameter.
func makePair<First, Second>(
_ first: First,
_ second: Second
) -> (First, Second) {
(first, second)
}
The function accepts two values whose concrete types may be different.
let pair = makePair("Age", 42)
For this call:
- First represents String.
- Second represents Int.
The returned tuple therefore has the type:
(String, Int)
The Compiler Must Know Which Operations Are Valid
An unconstrained generic type can represent any concrete type.
This means Swift cannot assume that the type supports a particular operation.
For example, the following function will not compile:
func areEqual<T>(_ first: T, _ second: T) -> Bool {
first == second
}
Not every Swift type supports the equality operator.
Swift therefore cannot guarantee that == is available for every possible T.
We need to place a requirement on the generic type.
Generic Constraints
A generic constraint limits the types that may be used with a generic function or type.
To compare two values using ==, T must conform to Equatable.
func areEqual<T: Equatable>(
_ first: T,
_ second: T
) -> Bool {
first == second
}
The declaration T: Equatable means:
T can represent any concrete type, provided that type conforms to Equatable.
The function now works with types that support equality.
areEqual(10, 10)
areEqual("Swift", "Swift")
areEqual(true, false)
A custom type can also be used when it conforms to Equatable.
struct User: Equatable {
let id: Int
let name: String
}
let firstUser = User(id: 1, name: "Milo")
let secondUser = User(id: 1, name: "Milo")
let matches = areEqual(firstUser, secondUser)
Constraints Explain What the Implementation Needs
A generic constraint should not be added merely to make the declaration look more specific.
It should describe a capability required by the implementation.
The areEqual function needs Equatable because it uses ==.
Consider another function that sorts values.
func sortedValues<T: Comparable>(
_ values: [T]
) -> [T] {
values.sorted()
}
The function requires Comparable because sorting needs to determine whether one value should appear before another.
let numbers = sortedValues([30, 10, 20])
let names = sortedValues(["Milo", "Luna", "Buddy"])
Generics provide the flexibility.
The constraint provides the capabilities required by the implementation.
Using a where Clause
More complex generic requirements can be written using a where clause.
func haveSameElements<First, Second>(
_ first: First,
_ second: Second
) -> Bool
where
First: Sequence,
Second: Sequence,
First.Element == Second.Element,
First.Element: Equatable
{
Array(first) == Array(second)
}
This function has several requirements.
- First must conform to Sequence.
- Second must conform to Sequence.
- Both sequences must contain the same element type.
- The element type must conform to Equatable.
The constraints describe exactly what the implementation requires.
let numbers = [1, 2, 3]
let otherNumbers = [1, 2, 3]
let result = haveSameElements(numbers, otherNumbers)
Generics Express Relationships Between Types
One of the most powerful features of generics is that they express relationships.
Consider this function:
func duplicate<T>(_ value: T) -> (T, T) {
(value, value)
}
The function does not merely accept an unknown value and return two unknown values.
It guarantees that both returned values have the same concrete type as the input.
let numbers = duplicate(42)
// (Int, Int)
let names = duplicate("Swift")
// (String, String)
The same placeholder appears in the input and output positions.
That repeated use of T communicates a type relationship to the compiler.
Generics are not only about reusing code.
They also allow APIs to describe relationships between input types, stored types and return types.
Type Inference
Swift can usually infer the concrete type represented by a generic parameter.
func echo<T>(_ value: T) -> T {
value
}
let number = echo(42)
let message = echo("Hello")
We do not need to tell Swift that T is Int for the first call.
Swift infers it from the argument.
It similarly infers String for the second call.
This allows generic code to remain concise at the point where it is used.
Generic Initializers
An initializer can introduce its own generic type parameter.
struct Measurement {
let description: String
init<Value>(_ value: Value, unit: String) {
description = "\(value) \(unit)"
}
}
The initializer can accept many different value types.
let distance = Measurement(10, unit: "kilometres")
let temperature = Measurement(21.5, unit: "degrees")
let enabled = Measurement(true, unit: "enabled")
The Measurement type itself is not generic.
Only the initializer is generic.
Generic Methods
A method can introduce a generic type parameter even when the surrounding type is not generic.
struct Printer {
func printValue<T>(_ value: T) {
print(value)
}
}
The same method can print different concrete types.
let printer = Printer()
printer.printValue(42)
printer.printValue("Swift")
printer.printValue(true)
Extending Generic Types
A generic type can be extended without repeating its generic parameter declaration.
struct Stack<Element> {
private var items: [Element] = []
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element? {
items.popLast()
}
}
extension Stack {
var top: Element? {
items.last
}
}
The extension automatically has access to the Element parameter declared by Stack.
Conditional Extensions
An extension can be made available only when its generic type satisfies additional requirements.
extension Stack where Element: Equatable {
func contains(_ value: Element) -> Bool {
items.contains(value)
}
}
The contains method is available only when Element conforms to Equatable.
var numbers = Stack<Int>()
numbers.push(10)
numbers.push(20)
let containsTen = numbers.contains(10)
This works because Int conforms to Equatable.
The Stack itself remains usable with types that do not conform to Equatable.
Only the constrained functionality becomes unavailable.
A Practical Example: Loading Data
Generics are frequently used when decoding API responses.
Suppose we create a reusable function that decodes any Decodable type.
func decode<Model: Decodable>(
_ type: Model.Type,
from data: Data
) throws -> Model {
try JSONDecoder().decode(type, from: data)
}
The function can decode a User.
struct User: Decodable {
let id: Int
let name: String
}
let user = try decode(User.self, from: userData)
The same function can decode an Article.
struct Article: Decodable {
let title: String
let body: String
}
let article = try decode(Article.self, from: articleData)
The generic return type ensures that decoding User.self returns a User.
Decoding Article.self returns an Article.
We do not need to cast the returned value.
Swift preserves the relationship between the type supplied to the function and the value returned from it.
A Practical Example: Reusable Network Responses
A server may return several different models inside the same response structure.
struct APIResponse<Payload: Decodable>: Decodable {
let status: String
let payload: Payload
}
The response structure remains the same.
Only the payload changes.
We can decode a user response.
typealias UserResponse = APIResponse<User>
We can also decode an article response.
typealias ArticleResponse = APIResponse<Article>
Without generics, we might need separate response types containing duplicated properties.
struct UserResponse: Decodable {
let status: String
let payload: User
}
struct ArticleResponse: Decodable {
let status: String
let payload: Article
}
Generics allow the common structure to be written once while preserving the concrete payload type.
Are Generics Decided at Runtime?
Generic code is checked using concrete type information known to Swift.
When we call a generic function with Int values, the compiler knows that T represents Int for that use.
When we create a Stack<String>, the compiler knows that Element represents String.
This is different from placing unrelated values inside Any and inspecting their types later.
Generics preserve type relationships at compile time.
Generics Do Not Mean Every Type at Once
It is easy to misread this declaration:
func identity<T>(_ value: T) -> T
It does not mean that one call works with every type simultaneously.
It means that each call chooses one concrete type for T.
let number = identity(42)
// T is Int for this call.
let text = identity("Swift")
// T is String for this call.
Within one use of the function, T consistently represents the same concrete type.
Generic Code Can Still Be Specific
Generics do not mean that an implementation must accept absolutely every possible type.
Constraints allow generic code to be as flexible or as specific as necessary.
func maximum<T: Comparable>(
_ first: T,
_ second: T
) -> T {
first > second ? first : second
}
The function is flexible because it works with any Comparable type.
It is specific because both arguments and the return value must share the same concrete type.
let largestNumber = maximum(10, 20)
let latestName = maximum("Milo", "Luna")
What Generics Give Us
Generics provide several important benefits.
- They remove duplicated implementations.
- They preserve concrete type information.
- They maintain compile-time type safety.
- They express relationships between inputs and outputs.
- They allow reusable collections, algorithms and data structures.
- They allow constraints to describe required capabilities.
What Generics Are Not
Generics are not a way to disable Swift's type system.
They are not equivalent to Any.
They do not mean that a value has no concrete type.
They do not allow unrelated types to be used interchangeably unless the generic declaration explicitly permits them.
Generics make code more reusable by describing types abstractly, while still allowing Swift to reason about the exact type relationships involved.
Interview Questions
What are generics in Swift?
Generics allow functions and types to work with placeholder types that are replaced by concrete types when the generic code is used.
What problem do generics solve?
They prevent duplicated implementations while preserving compile-time type safety.
What does T mean?
T is a generic type parameter.
It represents a concrete type that is determined when the generic function or type is used.
Does T mean Any?
No.
Any can hold values of unrelated concrete types while losing many type relationships.
A generic parameter represents one concrete type consistently within a particular use of the generic declaration.
Why must both parameters of swapValues have the same type?
Both parameters use the same generic type parameter T.
For each call, T must represent one concrete type.
What is a generic constraint?
A generic constraint limits the concrete types that can replace a generic parameter.
It usually requires the type to inherit from a class or conform to a protocol.
Why would a generic function require Equatable?
The Equatable constraint guarantees that values of the generic type can be compared using ==.
Can a generic declaration have more than one type parameter?
Yes.
Functions and types can introduce several independent generic parameters.
What is type inference in generic code?
Type inference allows Swift to determine the concrete generic arguments from the values and context surrounding a call.
Can a non-generic type contain a generic method?
Yes.
A method or initializer can introduce its own generic parameters even when its surrounding type is not generic.
Final Revision Note
Generics allow us to write an implementation without permanently tying it to one concrete type.
The generic declaration introduces placeholder types such as T or Element.
When the code is used, those placeholders represent real concrete types.
Swift preserves those concrete types and verifies that all relationships and operations remain valid.
Remember
Generics do not remove type information.
They allow us to describe a family of type-safe implementations using one reusable declaration.
Write the algorithm once. Preserve the concrete type every time it is used.
