If you've written Swift for even a short time, you've already used Generics—whether you realized it or not. Types like Array, Dictionary, Set, and even Optional are all built using generics.
Generics are one of Swift's most powerful language features because they help us write reusable, type-safe, and maintainable code.
Let's understand them step by step.
Why Do We Need Generics?
Imagine writing separate functions for every data type.
func printInt(_ value: Int) { ... }
func printString(_ value: String) { ... }
func printDouble(_ value: Double) { ... }
The logic is identical, but we're repeating code simply because the data types are different.
One option is to use Any.
func printValue(_ value: Any) {
print(value)
}
While this works, it comes with a drawback—you lose compile-time type safety and often need runtime type casting (as? or as!).
Generics solve this problem by allowing us to write code once and reuse it with different data types without sacrificing type safety.
Advantages of Generics over Any:
Strong compile-time type safety
No unnecessary runtime downcasting
Cleaner and more reusable code
Better compiler optimizations
What Are Generics?
Generics are placeholders for types that allow us to write flexible code that works with multiple data types.
The placeholder type is called a Type Parameter.
For example:
func printMe<T>(a: T) {
print(a)
}
Here, T is the type parameter.
Swift automatically determines the actual type when the function is called.
printMe(a: 1)
printMe(a: "iPhone")
Output:
1
iPhone
Generic Functions
Generic functions let us write one implementation that works for many different types.
Instead of creating multiple versions of the same function, we simply introduce a type parameter.
func printMe<T>(a: T) {
print(a)
}
The compiler replaces T with the appropriate type during compilation.
Generic Types
Generics are not limited to functions.
Classes, structures, and enums can also be generic.
A common example is a Stack.
struct Stack<Element> {
var items = [Element]()
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
items.removeLast()
}
}
Using the stack:
var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
Here, Element is the type parameter for the entire structure.
Understanding Type Parameters
A Type Parameter is simply a placeholder for a real type.
In our examples:
TElement
are both type parameters.
Swift's standard library uses descriptive names for type parameters.
Examples:
Array<Element>
Dictionary<Key, Value>
Set<Element>
Optional<Wrapped>
Notice how these names clearly describe what each type represents.
Collections Are Generic
One interesting fact is that Swift collections are themselves generic implementations.
Examples include:
Array<Element>
Dictionary<Key, Value>
Set<Element>
This means the same implementation works for:
Array<Int>
Array<String>
Array<User>
without duplicating code.
Generic Functions Inside Generic Types
When writing a generic function inside a generic type, you don't need to declare the generic parameter again.
For example:
extension Stack {
var topItem: Element? {
items.isEmpty ? nil : items[items.count - 1]
}
}
Notice that Element is already available because Stack itself is generic.
Multiple Type Parameters
A generic type can have multiple type parameters.
class AnotherGenericClass<TypeOne, TypeTwo, TypeThree> {
}
Swift places no restriction on how many type parameters you can define.
Extending Generic Types
Extending a generic type works just like extending any normal type.
There is no need to specify the generic parameter again.
extension Stack {
var topItem: Element? {
items.isEmpty ? nil : items.last
}
}
Since Stack already defines Element, the extension automatically has access to it.
Type Constraints
Sometimes we don't want a generic type to accept every possible type.
Instead, we want to restrict it to types that conform to a protocol or inherit from a specific class.
This is called a Type Constraint.
For example:
class MyGenericClass<Type: Equatable> {
}
Now only Equatable types can be used.
Swift uses this concept extensively.
For example, the Dictionary type requires its key to conform to Hashable.
Dictionary<Key: Hashable, Value>
This allows Swift to efficiently find values using keys.
Why This Generic Function Doesn't Compile
Consider the following function:
func findIndex<T>(of valueToFind: T, in array: [T]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}
This won't compile.
Why?
Because Swift doesn't know whether T supports the == operator.
The solution is to add a type constraint.
func findIndex<T: Equatable>(
of valueToFind: T,
in array: [T]
) -> Int? {
...
}
Now Swift knows every possible T conforms to Equatable, making equality comparison valid.
Generic Where Clause
Sometimes a single constraint isn't enough.
Swift allows multiple constraints using a where clause.
func doSomething<T>(
first: T,
second: T
)
where T: Comparable, T: Hashable {
guard first.hashValue == second.hashValue else {
return
}
if first == second {
print("\(first) and \(second) are equal.")
}
}
The same constraints can also be written before Swift 5's preferred syntax.
func doSomething<T where T: Comparable, T: Hashable>(
first: T,
second: T
) {
...
}
Generic Subscripts
Just like functions and types, Swift also supports Generic Subscripts.
They allow subscripts to work with generic types while maintaining compile-time type safety.
Although less commonly used, they follow the same generic principles discussed so far.
Associated Types
Associated types are closely related to generics and are commonly used inside protocols.
Apple defines an associated type as:
"An associated type gives a placeholder name to a type that's used as part of the protocol."
Example:
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
Every type conforming to Container decides what Item should be.
The protocol simply defines the placeholder.
You can also add constraints.
associatedtype Item: Equatable
Now every conforming type must use an Equatable item.
Swift Generics Are Invariant
One advanced concept worth knowing is that Swift generics are invariant.
This means that even if one type inherits from another, their generic wrappers are considered completely different types.
Example:
struct Container<T> {}
var swiftCourse: Container<SwiftOnTheServer>
= Container<SwiftOnTheServer>()
var onlineCourse: Container<OnlineCourse> = swiftCourse
This produces a compiler error because:
Container<SwiftOnTheServer>
is not a subtype of
Container<OnlineCourse>
even if SwiftOnTheServer inherits from OnlineCourse.
Key Takeaways
Generics let you write reusable code that works with multiple types.
They provide compile-time type safety unlike
Any.Generic functions, classes, structures, and enums all use type parameters.
Swift collections such as
Array,Dictionary,Set, andOptionalare built using generics.Type constraints help limit generic types to protocols such as
EquatableorHashable.Associated types bring generic behavior to protocols.
Swift generics are invariant, meaning generic types don't automatically inherit subtype relationships.
Mastering generics is an important step toward writing clean, reusable Swift code. You'll encounter them everywhere—from the Swift Standard Library to modern frameworks, Swift Packages, and Clean Architecture implementations.
