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.

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:

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

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

9. Swift some vs any vs Generics

This is the most important comparison for understanding opaque types correctly.

ApproachMeaningBest forKey limitation
some ProtocolOne hidden concrete type that conforms to a protocolHiding implementation details while preserving static type informationA single declaration must use one underlying concrete type
any ProtocolAn existential value that can hold any conforming typeStoring or passing mixed conforming valuesLess precise type information and some protocol limitations
GenericsA placeholder type chosen by the caller or contextExpressing type relationships and reusable algorithmsCan 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

12. Practice Exercise

Build a function that returns a hidden collection of uppercase names.

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.