Swift Opaque Types (some): Complete Guide and Examples
Swift opaque types let you hide a concrete type while still promising that the value conforms to a protocol. They are most commonly written with the some keyword. This matters because it gives you abstraction without giving up type safety or performance, and it solves real problems that appear when protocols have associated types or when you want to expose less implementation detail.
Quick answer: In Swift, some Protocol means “this value conforms to the protocol, but the exact concrete type is hidden from the caller.” The hidden type is still a single specific type chosen by the function, property, or subscript implementation, and Swift keeps that type information internally for static checking.
Difficulty: Intermediate
Helpful to know first: You’ll understand this better if you already know basic Swift functions, protocols, generics, and the difference between a concrete type and a protocol type.
1. What Is an Opaque Type?
An opaque type is a type written with some followed by a protocol, such as some Equatable or some Collection. It tells callers what capabilities a value has, but not its exact type.
- It hides the underlying concrete type from code outside the declaration.
- It still preserves a single concrete type chosen by the implementation.
- It is checked at compile time, unlike type erasure patterns based on boxed values.
- It is especially useful when returning protocol-conforming values from functions.
- It is not the same as any Protocol, which creates an existential protocol value.
For example, if a function returns some Collection, callers know they can use collection operations, but they do not know whether the actual type is an Array, Set, or a custom collection type.
Opaque types answer the question “what can this value do?” without exposing “what exact type is it?”
2. Why Opaque Types Matter
Opaque types solve a common design problem: you want to return or store something described by a protocol, but the protocol alone may not be enough as a concrete type. This happens often with protocols that use associated types or Self requirements.
They also let you change internal implementation details later. If your API returns some Sequence, you can switch from one concrete sequence type to another in future versions, as long as the external contract still holds.
In practice, opaque types matter because they:
- reduce API coupling to concrete implementation types,
- avoid exposing complex nested generic types,
- keep compile-time type information for optimization and safety,
- make some protocol-based APIs possible without full type erasure.
You should consider opaque types when you want abstraction and a stable interface, but still need Swift to know there is one specific underlying type.
3. Basic Syntax or Core Idea
Returning an opaque type
The most common use is a function that returns some Protocol.
func makeMessage() -> some CustomStringConvertible {
"Hello, Swift"
}This function returns a value that conforms to CustomStringConvertible. In this case, the hidden concrete type is String.
Even though callers do not see String in the signature, Swift still knows the function always returns one concrete type.
Using the returned value
You can use the value through the protocol’s interface.
let message = makeMessage()
print(message.description)This works because CustomStringConvertible requires a description property.
Important rule: one underlying type per declaration
A function returning an opaque type must return the same concrete type from every return path.
func makeValue(flag: Bool) -> some Equatable {
if flag {
return 42
} else {
return 99
}
}This is valid because both branches return Int.
But if one branch returned Int and the other returned String, Swift would reject it because the underlying types do not match.
4. Step-by-Step Examples
Example 1: Hiding a simple concrete return type
This first example shows the basic idea with a protocol from the standard library.
func buildTitle() -> some CustomStringConvertible {
"Opaque Types"
}
let title = buildTitle()
print(title.description)The caller knows the result can be described as a string, but does not rely on the exact concrete type in the API contract. Internally, the function returns a String.
Example 2: Returning a collection without exposing the collection type
Opaque types become more useful when the concrete type is an implementation detail.
func evenNumbers(upTo: Int) -> some Collection {
Array(1...upTo).filter { $0 % 2 == 0 }
}
let numbers = evenNumbers(upTo: 10)
print(numbers.count)Callers can use collection operations like count, but your function signature does not expose that the result is specifically an Array<Int>.
Example 3: Working with protocols that use associated types
Protocols such as Collection are often awkward to return directly as plain protocol types because they carry associated type information. Opaque types handle this neatly.
func makeScores() -> some Sequence {
[90, 85, 100, 95]
}
for let score in makeScores() {
print(score)
}The caller can iterate over the sequence, even though the exact sequence type remains hidden.
Example 4: Opaque parameter types
Swift also allows some in parameter positions. This means the function accepts any single concrete type per call that conforms to the protocol.
func printLength(of text: some StringProtocol) {
print(text.count)
}
printLength(of: "Swift")
printLength(of: Substring("Opaque"))This is similar to using a generic parameter constrained to a protocol. The function works with any conforming concrete type, while preserving static type information for that call.
5. Practical Use Cases
- Returning a sequence, collection, or iterator while hiding whether it is backed by an array, lazy wrapper, or custom type.
- Designing public APIs that should expose behavior without exposing internal implementation details.
- Reducing very long generic return types in function signatures.
- Working with protocols that have associated types, where plain existential protocol values may be limited or inappropriate.
- Creating library interfaces that may change internal concrete types later without breaking source-level expectations.
- Accepting a protocol-constrained parameter with some when you want generic-like behavior in a concise form.
6. Common Mistakes
Mistake 1: Returning different concrete types from different branches
A function returning an opaque type must always produce the same underlying concrete type, even if all returned values conform to the same protocol.
Problem: This code returns Int in one branch and String in another. Both conform to CustomStringConvertible, but an opaque return type still requires one single concrete type.
func badValue(flag: Bool) -> some CustomStringConvertible {
if flag {
return 10
} else {
return "ten"
}
}Fix: Return the same concrete type from every path, or redesign the API to use an existential type or a type-erased wrapper when different types are truly required.
func goodValue(flag: Bool) -> some CustomStringConvertible {
if flag {
return "10"
} else {
return "ten"
}
}The corrected version works because both branches return the same concrete type, String.
Mistake 2: Assuming some Protocol is the same as any Protocol
Opaque types and existential types solve different problems. Many Swift errors come from treating them as interchangeable.
Problem: This code expects an opaque type to behave like a freely swappable protocol box. But some Equatable means one hidden concrete type, not any possible conforming type mixed together.
func firstValue() -> some Equatable {
1
}
func secondValue() -> some Equatable {
2
}
let a = firstValue()
let b = secondValue()
// Do not assume opaque results from different declarations are interchangeable.Fix: Use opaque types when one declaration hides one concrete type. Use any Protocol when you need a true existential value that may hold different conforming types over time.
let values: [any CustomStringConvertible] = [1, "two", 3.0]
for let value in values {
print(value.description)
}The corrected version works because an existential array can store different concrete conforming types behind the protocol.
Mistake 3: Trying to use operations not guaranteed by the protocol
When you return some Protocol, callers only get the interface promised by that protocol, not the hidden type’s full API.
Problem: This code assumes the result is specifically a String, even though the function only promises CustomStringConvertible.
func label() -> some CustomStringConvertible {
"Report"
}
let text = label()
// text.uppercased()Fix: Either use only members defined by the protocol, or return a more specific opaque protocol if the caller should have more capabilities.
func label() -> some StringProtocol {
"Report"
}
let text = label()
print(text.count)The corrected version works because the API promise now matches the operations the caller actually needs.
Mistake 4: Using opaque types when a generic type parameter is clearer
In parameter position, some Protocol often behaves similarly to a generic constraint. Sometimes a named generic is easier when multiple parameters must share the same type.
Problem: This function accepts two values that each conform to Equatable, but they are not required to be the same concrete type, so direct comparison is not possible.
func compare(_ left: some Equatable, _ right: some Equatable) {
// print(left == right)Fix: Use a named generic parameter when the values must be the same type.
func compare<T: Equatable>(_ left: T, _ right: T) {
print(left == right)
}The corrected version works because the generic parameter T guarantees both arguments are the same concrete type.
7. Best Practices
Use opaque return types to hide implementation details, not to hide meaning
If callers only need protocol behavior, returning some Protocol keeps your API flexible. But the protocol should still clearly express what the caller can do.
func sortedNames() -> some Collection {
["Ava", "Mia", "Noah"].sorted()
}This is a good fit because the caller mainly needs collection behavior, not the exact storage type.
Prefer a generic parameter when relationships between types matter
If two parameters, or a parameter and return type, must be tied together as the same type, a named generic is usually clearer than separate opaque types.
func duplicate<T>(_ value: T) -> (T, T) {
(value, value)
}This is better than trying to express a same-type relationship with unrelated some parameters.
Choose any when you truly need heterogeneity
If a variable or collection must hold different concrete conforming types, opaque types are the wrong tool. Use an existential type instead.
let items: [any CustomStringConvertible] = ["Swift", 42, 3.14]This works because the collection is intended to contain mixed concrete types behind one shared protocol interface.
Keep protocol promises as specific as necessary
If callers need collection indexing, mutability, or string-specific features, choose a protocol that exposes those capabilities instead of an overly broad one.
func usernames() -> some RandomAccessCollection {
["ana", "ben", "cara"]
}This is more informative than returning just some Sequence when random access is part of the intended contract.
8. Limitations and Edge Cases
- An opaque return type must have one concrete underlying type for that declaration. Different branches cannot return unrelated concrete types.
- Callers cannot rely on the hidden type’s extra methods unless those methods are guaranteed by the declared protocol.
- some does not mean “unknown at runtime.” It means “hidden from this interface, but fixed and known to the compiler.”
- Two different declarations returning some Protocol do not automatically imply the same hidden type, even if they currently return the same concrete type internally.
- When you need to store values of multiple conforming types together, an opaque type usually will not work; an existential or type-erased wrapper is more appropriate.
- Opaque parameter types can be concise, but named generics are often easier to understand when same-type constraints or multiple related types are involved.
- If you see errors related to opaque return types, they are often caused by mismatched return branches or by attempting to use protocol-inaccessible members.
9. Swift some vs any vs Generics
This is the most important comparison for understanding opaque types correctly.
| Approach | Meaning | Best for | Key limitation |
|---|---|---|---|
| some Protocol | One hidden concrete type that conforms to a protocol | Hiding implementation details while preserving static type information | A single declaration must use one underlying concrete type |
| any Protocol | An existential value that can hold any conforming type | Storing or passing mixed conforming values | Less precise type information and some protocol limitations |
| Generics | A placeholder type chosen by the caller or context | Expressing type relationships and reusable algorithms | Can expose more type complexity in APIs |
When to use some
Use opaque types when the implementation chooses the concrete type and you want to hide it from the caller.
When to use any
Use existential types when the value may vary between different conforming concrete types at runtime, such as in arrays of mixed values.
When to use generics
Use generics when the caller’s type matters, or when multiple values in the same declaration must be tied to one another through a shared type parameter.
A simple rule is: some hides one concrete type, any stores any conforming type, and generics model relationships between types.
10. Practical Mini Project
This small example builds a reporting API that returns an opaque collection instead of exposing the exact container type. That keeps the public interface simple while preserving useful collection behavior.
struct Task {
let title: String
let isDone: Bool
}
func completedTaskTitles(from tasks: [Task]) -> some Collection {
tasks
.filter { $0.isDone }
.map { $0.title }
}
let tasks = [
Task(title: "Write proposal", isDone: true),
Task(title: "Review code", isDone: false),
Task(title: "Ship release", isDone: true)
]
let doneTitles = completedTaskTitles(from: tasks)
print("Completed tasks:")
for let title in doneTitles {
print("- \(title)")
}
print("Count: \(doneTitles.count)")This example returns a value described only as some Collection. The caller can iterate and read the count, but the API does not promise the exact collection type. That gives you freedom to refactor the implementation later.
11. Key Points
- some Protocol means a hidden but fixed concrete type that conforms to the protocol.
- Opaque types are most useful for return values and API abstraction.
- All return paths in an opaque return declaration must use the same concrete type.
- some is not the same as any.
- Use any for mixed conforming values and existential storage.
- Use generics when multiple values must share or relate through the same type parameter.
- Callers can only use members guaranteed by the protocol in the opaque type declaration.
12. Practice Exercise
Build a function that returns a hidden collection of uppercase names.
- Create a function named uppercaseNames(from:).
- It should accept an array of strings.
- It should return some Collection.
- Inside the function, convert each name to uppercase.
- Print each result and the total count.
Expected output: The program should print uppercase names and then the number of names returned.
Hint: Use map on the input array. Return one concrete collection type from the function.
func uppercaseNames(from names: [String]) -> some Collection {
names.map { $0.uppercased() }
}
let results = uppercaseNames(from: ["alice", "ben", "carla"])
for let name in results {
print(name)
}
print("Total: \(results.count)")13. Final Summary
Swift opaque types, written with some, let you expose what a value can do without exposing what the value is. That makes APIs cleaner, more flexible, and easier to evolve. They are especially useful when returning values that conform to protocols with associated types, or when you want to avoid leaking a complicated concrete type into your public interface.
The most important thing to remember is that opaque does not mean dynamic or arbitrary. Each declaration still has one real concrete type underneath, and Swift enforces that rule. If you need a mixed box of different conforming types, use any. If you need relationships between types, use generics. If you want abstraction with static safety, some is often the right choice.
A good next step is to study Swift existential types with any and compare them directly with generics. That will make it much easier to choose the right abstraction tool in real Swift code.