Opaque and Boxed Protocol Types
When you want to hide the concrete type behind a protocol, Swift gives you
two tools that look similar but behave very differently:
opaque types, written some Protocol, and
boxed protocol types (also called existentials), written
any Protocol. An opaque type hides one specific type
while preserving its identity; a boxed type can hold any
conforming value but loses that identity. Choosing correctly matters for
both type safety and performance.
The problem each solves
Both features let an API talk about "a value conforming to a protocol" without naming the exact type. But they answer two different questions:
- Opaque (
some): "I return a single, fixed, concrete type — but I don't want callers to depend on which one." The compiler still knows the real type; the caller doesn't. This hides implementation detail while keeping a stable, known type underneath. - Boxed (
any): "I need to store or pass values whose concrete types may differ at runtime, possibly mixed together." The value is wrapped in a box that can hold any conformer, decided dynamically.
The motivating problem is the same one associated types create: a protocol
with associated types or Self requirements (like
Equatable) historically couldn't be used as an ordinary type.
Opaque types solve that for the return-a-known-type case; boxed types
solve it for the hold-many-types case.
Opaque types with some
An opaque return type is declared by writing
some before the protocol. The function returns one specific
concrete type that satisfies the protocol, but that type is hidden from
the caller. Crucially, it must be the same type on every
execution path.
protocol Shape {
func draw() -> String
}
struct Square: Shape {
var size: Int
func draw() -> String {
String(repeating: "*", count: size)
}
}
// Returns a concrete type the caller never has to name
func makeSquare(_ n: Int) -> some Shape {
Square(size: n)
}
let s = makeSquare(4)
print(s.draw()) // → ****
The caller knows s is "some Shape," can call its
protocol methods, and can rely on type identity — but cannot see that it's
a Square. You're free to change the underlying type later
without breaking callers, the same way a private property hides storage.
Watch out: a some function must return one
single concrete type. Returning a Square on one branch and a
different shape on another is a compile error — the opaque type is fixed,
not variable.
Why some preserves type identity
Because an opaque type stands for one specific type, the compiler can keep
track of relationships that any erases. This is why
SwiftUI's body is some View: it returns one
precise (often enormous, generated) view type, and the framework relies on
knowing that type statically.
func doubled() -> some Equatable {
[1, 2, 3] // concrete type is [Int]
}
let a = doubled()
let b = doubled()
print(a == b) // → true — both are the same hidden type, so == is allowed
Comparing a == b works because the compiler knows both are the
identical underlying type. With a boxed any Equatable this
comparison wouldn't even compile, because == needs both sides
to be statically the same type.
Boxed protocol types with any
A boxed protocol type, written any Protocol,
is an existential: a container that can hold a value of any
conforming type, with the concrete type decided at runtime. This is what
lets you mix different types in one variable or collection.
struct Circle: Shape {
func draw() -> String { "O" }
}
// A heterogeneous array — each element may be a different concrete type
let shapes: [any Shape] = [Square(size: 2), Circle(), Square(size: 5)]
for shape in shapes {
print(shape.draw())
}
// → **
// → O
// → *****
The box stores the value plus a pointer to its type's protocol witness
table, so the right draw() is dispatched dynamically at
runtime. You can reassign a var x: any Shape from a
Square to a Circle — the concrete type genuinely
varies. Since Swift 6, using a protocol as a type requires the explicit
any spelling.
Differences from ordinary protocol types
"Boxed protocol type" is the modern name for using a protocol
directly as a type — any Shape is the explicit form of what
older Swift wrote as just Shape. The any keyword
exists to make the cost and semantics visible at the use site rather than
hidden. The real contrast worth internalizing is opaque versus boxed:
some Protocol (opaque) | any Protocol (boxed) | |
|---|---|---|
| Underlying type | One fixed concrete type | May differ per value, at runtime |
| Compiler knows the type? | Yes (just hidden from callers) | No — erased into a box |
| Type identity preserved? | Yes | No |
Works with Self/associated-type protocols? | Yes | Limited (some operations unavailable) |
| Heterogeneous collections? | No | Yes |
| Runtime cost | None (static dispatch, specialized) | Boxing + dynamic dispatch |
A key consequence: protocols with associated types or Self
requirements work freely as some, but as any
some of their members become inaccessible because the box can't supply a
single concrete associated type. The compiler "opens" an existential only
in limited situations.
Note: some and any both
differ from generics. A generic parameter
<T: Shape> lets the caller choose the type;
some Shape as a return type lets the function choose
and hide it. In parameter position, some Shape is in fact
shorthand for an unnamed generic parameter.
some in parameter position
Writing some on a parameter is sugar for a generic type
parameter: the caller picks one concrete type, and the function works with
it statically and efficiently. The two declarations below are equivalent.
func render(_ shape: some Shape) {
print(shape.draw())
}
// is shorthand for:
func render<S: Shape>(_ shape: S) {
print(shape.draw())
}
render(Circle()) // → O
When to use some versus any
A practical rule of thumb:
- Reach for
someby default. Use it when a single value flows through with one concrete type: return values that hide an implementation detail, function parameters, and properties like SwiftUI'sbody. It keeps full type information, preserves identity, and costs nothing at runtime. - Reach for
anyonly when you genuinely need runtime variability: storing mixed concrete types in one array, a property whose type can change after assignment, or passing around values whose type isn't known until runtime. Accept the boxing and dynamic dispatch as the price of that flexibility.
Tip: if your code compiles with both, prefer
some — it's faster and more capable. Switch to
any the moment the compiler tells you the type must vary at
runtime (for example, "use of protocol as a type must be written
any" when building a heterogeneous collection).
You can also convert between them: an any Shape can be passed
where some Shape is expected via "opening" the existential,
and a some Shape value can be stored into an
any Shape box. Going from some to any
is always safe; the reverse may require the box to be opened by the
compiler at a call boundary.
Wrapping up
Opaque types (some) and boxed protocol types
(any) both hide a concrete type behind a protocol, but they
sit at opposite ends of a trade-off. some commits to one fixed
underlying type, keeps its identity, supports associated-type protocols,
and compiles to efficient static code — ideal for return values,
parameters, and stable APIs. any erases the type into a box so
you can hold many different conformers at runtime, at the cost of dynamic
dispatch and some lost capabilities. Default to some, and use
any deliberately when runtime variability is the whole point.