A generic uses type parameters for types that aren’t known yet, letting one function or type declaration serve many concrete types while preserving relationships between positions.
An unconstrained type parameter has only the abilities shared by all types; replacing a generic with Any, adding arbitrary constraints, or confusing some with any changes the API contract.
State which positions must have the same type, add the narrowest constraints for abilities the implementation actually uses, and let context drive type inference.
What it is and why it exists
A generic declaration replaces one or more concrete types with named type parameters . When you call a generic function, its arguments and expected return type determine those parameters together. When you construct a generic type, the type arguments in angle brackets determine its storage and member signatures. Array<Int> and Array<String> share one declaration but are different static types.
Generics don’t mean “accept any value.” They mean “preserve a relationship without knowing the concrete type.” A function declared as choose<T>(_ left: T, _ right: T) -> T says that both parameters and the result use one T. Replacing all three positions with Any removes that guarantee and makes the caller cast the result.
You meet generics throughout the Swift standard library in Array<Element>, Dictionary<Key, Value>, Optional<Wrapped>, and Result<Success, Failure>. Application code also uses them for containers, transformations, data-access boundaries, and reusable algorithms. Code that serves one well-defined domain type doesn’t need to become generic for appearance’s sake.
A type parameter has no comparison, hashing, encoding, or domain-specific members on its own. When an implementation needs an ability, it declares a generic constraint , usually by requiring protocol conformance. The constraint lets the body use protocol members and rejects calls whose concrete types don’t qualify.
A generic abstraction should preserve type information that matters to its callers. A parameter that is only logged or returned unchanged may need no constraint. If an algorithm makes business sense only for Int, a concrete type is more honest. The contract, not the number of angle brackets, determines whether the abstraction is useful.
How it works
A generic function declares its parameter list after the function name, as in <Element>. The compiler collects requirements from every argument, explicit type annotation, and surrounding expression, then solves for a consistent concrete type. Swift doesn’t support call-site syntax such as identity<Int>(42) to specialize a generic function explicitly. When inference needs help, annotate an argument or the receiving result.
A colon constraint expresses protocol conformance or class inheritance. <Element: Comparable> lets the body use the comparison operations guaranteed by Comparable, but it doesn’t grant Hashable or Codable. You can write multiple requirements in the generic parameter list or move them into a where clause.
A where clause can constrain associated types and declare that two type expressions must be equal. Left.Element == Right.Element is a same-type requirement : the collection types may differ, but their elements must match. A generic declaration is available only when the compiler can prove every requirement at the call site.
A generic type includes its type parameters in its type identity. Stack<String> has a push that accepts String and a pop that returns String?; the same members use Int on Stack<Int>. You can’t change one Stack<String> instance into a stack of integers after construction.
An associated type in a protocol is selected by the conforming type. Associated types and generic parameters both express type relationships, but the choice happens in different places: users supply a generic type’s arguments, while conformers determine a protocol’s associated types through their implementations. The compiler can usually infer an associated type from property and method signatures, so an explicit typealias isn’t required.
Read the common forms by asking who declares the placeholder and who supplies its concrete type:
| Syntax | Relationship | Source of the concrete type |
|---|---|---|
func load<T>(_: T) | One named type parameter | Inferred at the call site |
struct Box<Value> | Part of a generic type’s identity | Supplied when naming the type |
T: Hashable | T must satisfy a protocol | Conformance checked by the compiler |
where A.Item == B.Item | Two type expressions are equal | Both proved at the call site |
associatedtype Item | A protocol leaves a type slot | Selected by the conforming type |
Type inference isn’t dynamic typing. Every expression still has a definite static type after compilation; inference merely avoids repeating it in source. When an error becomes long, one meaningful annotation at the call boundary usually exposes the conflict more clearly than a pile of conversions inside the implementation.
Inference collects information in both directions
Type inference doesn’t flow only from arguments to results. An assignment target, closure parameter and result types, overload candidates, and literal defaults all join the same constraint-solving process. In let ids: Set<Int> = [], the left side determines the empty literal’s type; let ids = [] doesn’t provide enough information.
When inspecting one generic call, mark these four sources of information in order:
- The static type of each argument.
- Any expected type declared where the result is received.
- Operations the closure body applies to its parameters and result.
- Requirements introduced by the generic declaration,
whereclause, and selected overload.
Those facts must form one consistent solution. If two arguments imply incompatible choices for T, the compiler doesn’t pick one and doesn’t insert a numeric conversion. Decide which type the domain requires, then perform a named conversion at the boundary.
Constraints belong on the narrowest capability boundary
Constraints can appear on a generic declaration, method, constrained extension, or conditional conformance. Their position determines whether the whole API or one capability is restricted. Moving a requirement outward affects more unrelated members.
| Location | Requirement it should express | Effect on other members |
|---|---|---|
struct Cache<Key: Hashable, Value> | Storage always needs hashable keys | Restricts every instance |
func contains(...) where Element: Equatable | Only this operation needs equality | Leaves other methods available |
extension Box where Value: Codable | A group of encoding members shares a requirement | Gates only that extension’s members |
extension Box: Equatable where Value: Equatable | Conformance is conditional | Doesn’t block construction of the base type |
The narrowest location isn’t always a method. If a type invariant depends on dictionary keys, Key: Hashable belongs on the type. If only a diagnostic export needs encoding, Codable shouldn’t infect the core declaration. Start at the expression that uses the ability and move outward to the first stable API boundary.
Examples
These four standalone programs progress from generic functions to generic types, where constraints, and associated types. This environment has no local Swift toolchain, so each block carries the required not-executed marker. Each displayed output was separately checked with a Swift 6.3.3 compiler.
Inferring type parameters at the call site
The two parameters and result of earlier share Value. Its Comparable constraint makes < available, while repeated doesn’t inspect the value and needs no extra constraint.
// # not executed here: Swift toolchain is not installed.
func earlier<Value: Comparable>(_ first: Value, _ second: Value) -> Value {
first < second ? first : second
}
func repeated<Value>(_ value: Value, count: Int) -> [Value] {
precondition(count >= 0)
return Array(repeating: value, count: count)
}
let earlierNumber = earlier(42, 17)
let earlierWord = earlier("pear", "apple")
let stages = repeated("ready", count: 3)
print(earlierNumber)
print(earlierWord)
print(stages.joined(separator: ","))17
apple
ready,ready,readyThe first call infers Value as Int, and the second infers String. Both arguments in any one call still need the same type. A generic function doesn’t automatically combine Int and Double into a shared numeric type.
The result also participates in inference. When the arguments aren’t enough, write let result: DesiredType = ... or first construct an input with a known type. Don’t let generated code fake an unprovable relationship with a forced cast.
The precondition describes the valid range of count, which is separate from a generic constraint. Generic constraints check type abilities; runtime preconditions check a particular value. Neither can replace the other.
Storing one element type in a generic type
Stack uses the same Element for storage, push, and pop. Its map method introduces a separate Output, so the transformed result can be a different stack type.
// # not executed here: Swift toolchain is not installed.
struct Stack<Element> {
private var storage: [Element] = []
var count: Int { storage.count }
mutating func push(_ element: Element) {
storage.append(element)
}
mutating func pop() -> Element? {
storage.popLast()
}
func map<Output>(_ transform: (Element) -> Output) -> Stack<Output> {
var result = Stack<Output>()
for element in storage {
result.push(transform(element))
}
return result
}
func values() -> [Element] { storage }
}
var jobs = Stack<String>()
jobs.push("build")
jobs.push("deploy")
let lengths = jobs.map { $0.count }
print(jobs.pop() ?? "none")
print(lengths.values())deploy
[5, 6]The type of jobs is fixed as Stack<String>, so push(3) fails at compile time. map doesn’t turn the original stack into an integer stack. It creates a new Stack<Int>, and that result type preserves the relationship across the transformation.
pop returns an optional because an empty stack is a valid state. Generics don’t decide boundary behavior for an API. Element? describes absence more accurately than a force unwrap or a sentinel value that couldn’t work for every possible Element.
This implementation deliberately keeps the interface small. If you add search, require Element: Equatable only on the constrained extension that provides that operation. Don’t make the whole Stack reject non-equatable elements.
Describing cross-type relationships with where
sameElements accepts two different collection types but requires their element types to match and support equality. unique only needs hashable sequence elements and removes duplicates while preserving first-seen order.
// # not executed here: Swift toolchain is not installed.
func sameElements<Left: Collection, Right: Collection>(
_ left: Left,
_ right: Right
) -> Bool where Left.Element == Right.Element, Left.Element: Equatable {
left.elementsEqual(right)
}
func unique<Values: Sequence>(_ values: Values) -> [Values.Element]
where Values.Element: Hashable {
var seen: Set<Values.Element> = []
return values.filter { seen.insert($0).inserted }
}
let states = ["queued", "running", "done"]
let activeStates = states[0...1]
print(sameElements(["queued", "running"], activeStates))
print(sameElements(["running", "queued"], activeStates))
print(unique(["A", "B", "A", "C", "B"]))true
false
["A", "B", "C"]The left side is Array<String> and the right is ArraySlice<String>, so the collection types needn’t match. Left.Element == Right.Element proves that elementsEqual compares one element type, while Equatable supplies equality.
unique uses a Set to track elements it has seen, which requires Hashable. The requirement belongs to this function, not to unrelated operations on the input type. The returned array keeps first-seen order and doesn’t rely on a set’s iteration order.
Constraints are part of the public API. If a later implementation no longer needs hashing, consider removing Hashable. Leaving a stale constraint excludes types for which the function would otherwise work correctly.
Letting a conformer select an associated type
Catalog declares Item as a primary associated type . The angle brackets make it possible to constrain that type with Catalog<Product>, but they don’t turn the protocol itself into a generic type like a Catalog structure would be.
// # not executed here: Swift toolchain is not installed.
struct Product {
let id: Int
let name: String
}
protocol Catalog<Item> {
associatedtype Item
var items: [Item] { get }
}
struct MemoryCatalog<Item>: Catalog {
let items: [Item]
}
func first<C: Catalog>(in catalog: C) -> C.Item? {
catalog.items.first
}
func names(in catalog: some Catalog<Product>) -> [String] {
catalog.items.map(\.name)
}
let products = MemoryCatalog(items: [
Product(id: 1, name: "Keyboard"),
Product(id: 2, name: "Mouse")
])
if let product = first(in: products) {
print("First: \(product.name)")
}
print(names(in: products).joined(separator: ", "))First: Keyboard
Keyboard, MouseMemoryCatalog<Product> lets the compiler infer Catalog.Item == Product from its items property. first preserves the associated type of any conformer and returns C.Item?; it doesn’t degrade the result to Any?.
In parameter position, some Catalog<Product> is a generic parameter that doesn’t need a source-level name. Each call still passes one concrete conforming type, and the body knows that its Item is Product. If two parameters must share exactly the same catalog type, name the type parameter and use it twice.
A primary associated type name must correspond to an associated type declared by the protocol. Its main purpose is concise constraints such as some Catalog<Product> or any Catalog<Product>. It doesn’t mean callers fill an ordinary generic argument on the protocol declaration.
Pitfalls
Supplying explicit type arguments to a function call
Fix: let the arguments and expected result drive inference, as in let value: Int = identity(42). If ambiguity remains, annotate the data boundary instead of covering the problem with as!.
Replacing a type parameter with Any
Fix: use a named type parameter when a relationship exists, such as [Element] -> Element?. Choose Any or an existential only when heterogeneous values are part of the data model and every runtime branch has defined behavior.
Applying a constraint too broadly
Fix: put each requirement on the method, extension, or conditional conformance that uses it, and remove protocols the implementation doesn’t use. Keep one compile test for a supported type and one for a type that should be rejected.
Expecting one type parameter to hold heterogeneous elements
Fix: use T: Shape when you need one concrete type and its static relationships. Use [any Shape] or an appropriate enum when one collection genuinely stores different conformers at runtime. That choice affects available members and type identity, not just spelling.
Treating some and any as interchangeable
Fix: first state who chooses the concrete type and whether it may vary between calls. Use a generic parameter when the caller supplies one concrete type, some when the implementation hides one result type, and any for genuine runtime heterogeneity.
Who selects the concrete type
Generic parameters, some, and any all let source code depend on protocol abilities, but they preserve different information. The useful questions are who selects the concrete type and whether a same-type relationship must cross the API boundary.
| Form | Who selects the concrete type | Preserved relationship |
|---|---|---|
<T: P> | Each call site | Every T position is the same |
Parameter-position some P | Each call site | The parameter has one unnamed concrete type |
Result-position some P | The function implementation | Every result has the same underlying type |
any P | The runtime value | Only conformance to P is guaranteed |
Two separate parameter-position occurrences of some P introduce two unnamed type parameters, so they needn’t match. If the implementation needs to swap, compare, or pass values between them, declare <T: P> and use T in both places. Omitting the name works only when the body doesn’t need to refer to that type again.
An opaque result type is selected by the implementation and hidden from callers, but it still represents one fixed underlying type. An existential permits each value to contain a different conformer, which suits heterogeneous storage and runtime replacement. The latter introduces indirection when needed, but that doesn’t justify a fixed performance multiplier. Measure the real call path.
Associated types form dependent members
A protocol’s associated type depends on its conforming Self. Given C: Catalog, C.Item is a dependent member type. The compiler knows Item only after it determines C or proves another constraint. That is how a generic function connects an input catalog to its output element.
A same-type requirement can connect several dependent members, such as Left.Item == Right.Item. The conforming types may still differ; only the named members are equal. Replacing that requirement with Left == Right narrows the contract to one catalog type and discards interoperability between different implementations.
Primary associated types only provide lightweight constraint syntax. Catalog<Product> constrains the primary associated type Item to Product; every name in the angle brackets must be listed by the protocol and correspond to an associated type declaration. The conformer still has to satisfy the relationship.
When an existential is opened, associated-type information may be usable only within a local scope. If a function needs to pass one value’s associated type into another parameter or expose it in a result, a named generic parameter usually states the relationship more clearly. Don’t erase a type first and try to reconstruct the compiler’s lost proof with a forced cast.
Specialization isn’t a source-level contract
The Swift compiler may specialize generic code for concrete types, or it may share an implementation when doing so preserves observable behavior. Optimization level, module boundaries, visibility, and compiler version all affect that choice. Source code can’t assume that every type gets a distinct machine-code copy, and it can’t derive fixed performance numbers from that assumption.
The primary benefits of generics are static type relationships and reuse. Performance questions require a release build, the target platform, realistic data, and observation of the actual call path. Without measurements, you can describe possible indirection, boxing, or code-size tradeoffs, but not claim that a cost occurred.
Constraints can affect optimization opportunities, but more constraints don’t imply faster code. An unnecessary constraint first changes who may call the API, and it can spread into declarations above it. Write the smallest correct contract, find the bottleneck with profiling, and only then consider a concrete overload or a changed boundary for a hot path.
Overloads and inference need clear boundaries
The overload set also participates in generic constraint solving. When two overloads accept the same argument, the compiler compares which is more specific; complex closures, default arguments, and result context can make that choice hard to anticipate. A public API shouldn’t require readers to guess subtle overload ranking.
If two generic overloads express different business meanings, distinct base names are usually clearer. If one only supplies an optimized path for a more specific type, its behavior and failure rules must match the general version. Call tests should cover values with identical runtime data but different static types.
When an error occurs in a long chain, assign each stage to a typed local constant. This identifies the point that lacks context and prevents an upstream edit from silently choosing another overload. Those names are often worth keeping after diagnosis because they explain the meaning of each transformation.
Keep generic boundaries readable
When a public signature exposes many type parameters, nested associated types, and same-type requirements, callers struggle to see the actual invariant. First ask whether an existing standard-library protocol describes the input, then give business relationships meaningful names. A type alias shortens repeated spelling but doesn’t reduce the underlying complexity.
When compiler diagnostics become hard to read, split a long expression into typed intermediate values or move requirements into a named helper. These changes give both the constraint solver and the reader local boundaries. Adding Any, as!, or unrelated protocols merely moves the problem to runtime or to a higher-level API.
Library evolution also distinguishes semantic constraints from implementation constraints. If a public function declares Element: Hashable, callers may treat that as part of the contract even if a future body no longer hashes anything. Before publishing a requirement, answer why the caller needs to know about it.
Don’t test a generic API only with one happy-path Int. Pick two structurally different types that both satisfy the constraints, showing that behavior comes from the protocol. Keep a separate example that doesn’t compile to prove what the boundary rejects. Runtime tests verify behavior; compile tests verify the type contract.
Further reading
4 questions · 1 predict-the-output · 1 spot-the-bug