|

Tuples: Group Values Without Creating a Type

Molecules of Swift, episode #001

Swift makes it easy to give data a name and a type. That is usually a strength: a UserID, Coordinate, or Bounds can carry meaning far beyond the values stored inside it. But not every small relationship in a program deserves to become a permanent model.

Sometimes a function simply needs to return two related results. Sometimes an intermediate transformation needs to carry a value together with a little context. Creating a new struct in those places can add a name without adding much meaning.

For example, a function that produces a minimum and maximum could introduce a dedicated type:

struct Bounds {
    let min: Int
    let max: Int
}

Sometimes that is exactly right. But if Bounds exists only because one expression needs to move two values together, Swift has a smaller tool:

let bounds = (min: 3, max: 9)

A tuple gives several values a typed shape without forcing that shape to become a permanent concept in the program. The interesting part is not the parentheses. It is learning where this lightweight structure is enough — and where a real type starts earning its name.

A shape that can stay local

A tuple is a compound value. Its elements can have different types:

let response = (status: 200, message: "OK")

Swift infers the type as (status: Int, message: String). The labels let the code use response.status and response.message, so a small tuple does not have to become a pile of .0 and .1 accesses.

You can also write the type explicitly:

let bounds: (min: Int, max: Int) = (3, 9)

The labels belong to the tuple type. Here the right-hand side does not need to repeat them because the expected type already provides that information.

This is a small detail, but it points to the real role of tuples: they are typed structure, not an untyped bag of values.

Multiple values can still be one result

A common use is a function result:

func extremes(in values: [Int]) -> (min: Int, max: Int)? {
    guard let first = values.first else { return nil }

    var minValue = first
    var maxValue = first

    for value in values.dropFirst() {
        minValue = min(minValue, value)
        maxValue = max(maxValue, value)
    }

    return (minValue, maxValue)
}

The function returns one value: an optional tuple. When it exists, that tuple contains two related results.

That distinction matters. This:

(Int, Int)?

means the pair itself may be absent. This:

(Int?, Int?)

means the pair always exists, but either element may be absent independently.

Those two types can represent different states. Choosing between them is already a small modeling decision.

If “minimum and maximum exist together or not at all” is the rule, the optional tuple expresses that relationship directly.

Destructuring is part of the point

Tuples become more useful when you do not need to keep the group intact:

let user = (id: 42, name: "Mira", isAdmin: true)

let (id, name, isAdmin) = user

Now the three elements are separate local bindings.

If one element is irrelevant, Swift does not require a throwaway name:

let (id, _, isAdmin) = user

That _ is a tiny but useful pattern. It says that the tuple has more information than this piece of code needs, and the unused position is intentional.

The next Molecule will go deeper into tuple patterns and destructuring. For now, the important part is that grouping values into a tuple does not trap them inside that shape.

Tuples compose with pattern matching

The same structure can be matched directly:

let point = (x: 0, y: 4)

switch point {
case (0, 0):
    print("origin")
case (0, _):
    print("y-axis")
case (_, 0):
    print("x-axis")
default:
    print("elsewhere")
}

There is no Point type here because the example does not need one. The tuple provides just enough structure for the two coordinates to move together and just enough shape for switch to inspect them together.

This is where tuples feel especially Swift-like: a small language feature becomes more useful because it composes with another one.

A tuple is not a tiny struct

It is tempting to think of a tuple as a struct that saves a few lines. That model breaks down quickly.

A named type gives a concept a declaration site:

struct Coordinate: Equatable, Hashable {
    let x: Int
    let y: Int
}

Now Coordinate can carry conformances, methods, documentation, access control, and domain meaning. You can search for it across the codebase. An API can say that it accepts a Coordinate, not merely “two integers in this order.”

Tuples deliberately do less.

There is also a subtle current Swift limitation worth knowing. Direct equality syntax works for compatible tuples:

(1, 2) == (1, 2)

But operator support and protocol conformance are different capabilities. In Swift 6.4, a tuple such as (Int, Int) still does not automatically conform to Equatable or Hashable. That means it cannot satisfy a generic T: Equatable requirement:

func requiresEquatable<T: Equatable>(_ value: T) {}

requiresEquatable((1, 2))

and it cannot be used directly as a Dictionary key:

let lookup: [(Int, Int): String] = [
    (1, 2): "value"
]

Tuple conformances have been discussed in Swift Evolution for years. The earlier SE-0283 work never shipped, and a revival proposing automatic Equatable, Comparable, and Hashable conformances was pitched again in 2026. As of Swift 6.4, those conformances are still not part of the language.

So if the grouped value needs to participate in generic APIs or collections that require these conformances, or is becoming a growing public interface, a struct is often the clearer choice.

Where the tuple fits

I reach for a tuple when the relationship is small and local: a function returns two values, an intermediate transformation needs to carry a value plus some context, or a short pattern match naturally works on several values together.

I start considering a named type when the shape crosses several APIs, gains behavior, needs invariants, needs protocol conformances, or simply deserves a name in the domain.

The difference is not “two fields means tuple, three fields means struct.” It is whether the grouping is temporary structure or a concept the program should remember.

Play with it

The companion 001-tuples Playground collects the examples in one executable file. It also includes the optional-tuple contrast, switch matching, ignored destructured values, and a manual compiler experiment for the Equatable boundary.

The shape can stay temporary

A tuple is most useful when the relationship between its values is real but local. It gives the compiler structure to work with, gives the reader meaningful element labels, and composes naturally with optionals, destructuring, and pattern matching — without adding another nominal type to the codebase.

The moment that temporary shape starts carrying behavior, invariants, protocol requirements, or domain meaning, a named type becomes more than ceremony. Until then, letting the shape stay temporary is often the clearer Swift design.

That is the useful mental model: use a tuple to name the parts; create a type when the relationship itself deserves a name.

Sources