Skip to content

Opaque and Boxed Protocol Types

Swift offers two mechanisms to conceal a value's underlying type: opaque types and boxed protocol types. These are especially handy at module boundaries, where you want to keep the return type private while exposing only the essential interface.

  • Opaque types hide the exact type from callers but let the compiler track it for optimizations. The function specifies what protocols the return value conforms to, preserving type identity.
  • Boxed protocol types (existentials) store any conforming type, but lose type identity—it's resolved at runtime and can vary.

Both approaches let you build flexible APIs without leaking internal implementation details.

The Challenge Opaque Types Address

Imagine a library for generating simple text patterns, like number pyramids or rectangles. Define a Drawable protocol for the core draw() method:

swift
protocol Drawable {
    func draw() -> String
}

struct Pyramid: Drawable {
    var height: Int
    func draw() -> String {
        var result: [String] = []
        for row in 1...height {
            result.append(String(repeating: " ", count: height - row) + String(1...row, joinedBy: ""))
        }
        return result.joined(separator: "\n")
    }
}
let smallPyramid = Pyramid(height: 3)
print(smallPyramid.draw())
//  1
// 12
//123

To flip a pattern vertically, you might use generics:

swift
struct FlippedPattern<T: Drawable>: Drawable {
    var pattern: T
    func draw() -> String {
        let lines = pattern.draw().split(separator: "\n")
        return lines.reversed().joined(separator: "\n")
    }
}
let flippedPyramid = FlippedPattern(pattern: smallPyramid)
print(flippedPyramid.draw())
//123
// 12
//  1

Similarly, for joining two patterns:

swift
struct JoinedPattern<T: Drawable, U: Drawable>: Drawable {
    var top: T
    var bottom: U
    func draw() -> String {
        return top.draw() + "\n" + bottom.draw()
    }
}
let joinedPyramids = JoinedPattern(top: smallPyramid, bottom: flippedPyramid)
print(joinedPyramids.draw())
//  1
// 12
//123
//123
// 12
//  1

The issue? These generics expose internal types like FlippedPattern<Pyramid> or JoinedPattern<Pyramid, FlippedPattern<Pyramid>>. Users see your wrapper details, cluttering the API and risking leaks of private types. Ideally, operations like flip and join should just return a Drawable—hiding the how.

Using Opaque Return Types

Opaque types flip the script on generics: the function chooses the concrete type, abstracted via protocols. Callers get a some Protocol return, knowing only it conforms, not what it is.

Add a Rectangle for variety:

swift
struct Rectangle: Drawable {
    var width: Int
    var height: Int
    func draw() -> String {
        let line = String(repeating: "-", count: width)
        let result = Array(repeating: line, count: height)
        return result.joined(separator: "\n")
    }
}

Now, build a "trapezoid" without exposing internals:

swift
func makeTrapezoid() -> some Drawable {
    let top = Pyramid(height: 2)
    let middle = Rectangle(width: 3, height: 2)
    let bottom = FlippedPattern(pattern: top)
    let trapezoid = JoinedPattern(
        top: top,
        bottom: JoinedPattern(top: middle, bottom: bottom)
    )
    return trapezoid
}
let trapezoid = makeTrapezoid()
print(trapezoid.draw())
// 1
//12
//---
//---
//12
// 1

Here, some Drawable guarantees a drawable value but hides the combo of pyramids and rectangles. You could refactor internals without API changes.

Combine with generics for reusable ops:

swift
func flip<T: Drawable>(_ pattern: T) -> some Drawable {
    return FlippedPattern(pattern: pattern)
}
func join<T: Drawable, U: Drawable>(_ top: T, _ bottom: U) -> some Drawable {
    return JoinedPattern(top: top, bottom: bottom)
}

let opaqueJoined = join(smallPyramid, flip(smallPyramid))
print(opaqueJoined.draw())
//  1
// 12
//123
//123
// 12
//  1

All returns must match one underlying type (e.g., can't mix FlippedPattern and plain types without wrappers). But generics can parameterize it, like returning [T] for repeats:

swift
func repeatPattern<T: Drawable>(_ pattern: T, times: Int) -> some Collection<Drawable> {
    return Array(repeating: pattern, count: times)
}

Boxed Protocol Types (Existentials)

Prefix protocols with any for boxes: any Drawable holds any conforming type, enabling heterogeneous collections at runtime cost (indirection).

Example: Stack patterns vertically:

swift
struct StackedPatterns: Drawable {
    var patterns: [any Drawable]
    func draw() -> String {
        return patterns.map { $0.draw() }.joined(separator: "\n\n")
    }
}

let tallPyramid = Pyramid(height: 4)
let wideRect = Rectangle(width: 4, height: 2)
let stack = StackedPatterns(patterns: [tallPyramid, wideRect])
print(stack.draw())
//   1
//  12
// 123
//1234
//
//----
//----

[any Drawable] mixes types; access only protocol methods (e.g., draw(), not height). Downcast if needed: if let pyr = stack.patterns[0] as? Pyramid { print(pyr.height) }.

Compare options for patterns:

  • Generics ([S] where S: Drawable): Fixed type, visible.
  • Opaque ([some Drawable]): Fixed hidden type.
  • Boxed ([any Drawable]): Mixed hidden types—most flexible.

Key Differences

Both mimic protocol returns, but:

  • Opaque (some): One fixed type (hidden); preserves identity for optimizations, equality, nesting.
  • Boxed (any): Any conforming type; flexible but loses identity—no equality, harder nesting.

Boxed example (looser contract):

swift
func looseFlip<T: Drawable>(_ pattern: T) -> any Drawable {
    if pattern is Rectangle {
        return pattern  // Could return plain or wrapped
    }
    return FlippedPattern(pattern: pattern)
}

This might return different types per call, blocking ops like == or chaining: looseFlip(looseFlip(smallPyramid)) fails (existential ≠ protocol-conforming).

For protocols with associated types (e.g., Container<Item>), only some works:

swift
protocol Container {
    associatedtype Item
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}
extension Array: Container {}

func makeContainer<T>(_ item: T) -> some Container {
    return [item]
}
let container = makeContainer(42)
print(container[0])  // Infers Int

Opaque Parameter Types

some in params is shorthand for unnamed generics—no true opacity:

swift
func drawTwice<Pattern: Drawable>(_ pattern: Pattern) -> String {
    let drawn = pattern.draw()
    return drawn + "\n" + drawn
}

// Equivalent to:
func drawTwiceShort(_ pattern: some Drawable) -> String {
    let drawn = pattern.draw()
    return drawn + "\n" + drawn
}

Multi-params allow different types: func merge(_ top: some Drawable, _ bottom: some Drawable) -> String { ... }. Limits: No where clauses or == constraints.

Released under the MIT License.