ES EN

Arrays with different data types in Swift

2025-04-27

Third older post recovered, originally published in July 2022.

I have added an addendum at the end, generated by GPT o3, commenting on the changes introduced in Swift over the last three years that affect the material discussed in the article.

While exploring SwiftUI and using it to understand Swift better, one of the first things that catches the eye is the reserved word some:

struct LandmarkRow: View {
    var landmark: Landmark

    var body: some View {
        HStack {
            landmark.image
                .resizable()
                .frame(width: 50, height: 50)
            Text(landmark.name)
            Spacer()
        }
    }
}

What does it mean, in the code above, that the variable body contains some view?

I do not know whether it happens to you too, but whenever I try to understand something new, I always feel as if I am following the clues of a case to be solved, as if I were a kind of Sherlock Holmes or Hercules Poirot. One question leads to another, and then to another, until in the end I manage to untangle the thread, or part of it, and connect all the new concepts I am finding with those I already know. And afterwards, when you explain something, you have to follow the path in reverse. You start from what you already know and from there build and explain the new material.

In our case, the path to understanding some is going to begin with a curious question: in a strongly typed language like Swift, is it possible to define an array containing values of different types?

At first sight, that seems contradictory. If we have to specify the type of the array strictly, then we must also specify the type of its components:

var miArray: [Int] = []

The type of the array above is [Int]. That means all its elements must be of type Int. We could define other arrays whose elements are of type String or Double, but in every case the arrays would be homogeneous and all their elements would have the same type.

Is that always how it works in Swift? It seems too rigid. It might be that, to solve a certain problem, the simplest solution would be to store integers, strings, and floating-point numbers together in a single array.

If we were designing a new language, we might be tempted to define something like:

var arrayMisc: [Int | String | Double] = [1, "Hola", 2.0, 3.0, 2]

That is, arrayMisc would be heterogeneous and could contain elements that are Int or String or Double.

It sounds interesting to be able to express something like that. But it must not be such a good idea, because I do not know any language that has a construct of this kind. For example, we would immediately run into the problem of how to treat the elements of the array. What happens when we use a loop and traverse its elements?

for thing in arrayMisc {
   // process the array element
}

What type would the variable thing have? It could be an Int, a String, or a Double, depending on the array element it happened to be holding at that point. We would need to introduce some language construct to let us work with the elements of that heterogeneous array.

Luckily, we are not designing a new language: we are studying Swift. We are going to see that Swift is a modern and flexible language that provides several strategies that allow us, up to a point, to group different kinds of data inside a single array.

Weakly typed languages

In weakly typed languages such as Python, it is very easy to define an array with different kinds of data:

miArray = [1, "hola", 3.0]
print(miArray)

# prints: [1, 'hola', 3.0]

This prints:

[1, 'hola', 3.0]

Because Python is weakly typed, it has no problem doing things like:

print(miArray[0] + miArray[2])

# prints: 4.0

This may seem like an advantage until we realize that the compiler is not really checking anything and allows expressions such as the following, which will produce a runtime error because an integer and a string cannot be added:

print(miArray[0] + miArray[1])

# runtime error

That is the problem with weakly typed languages. The compiler cannot detect many errors, so those errors appear at runtime.

Strongly typed languages

In a strongly typed language, every variable, parameter, return value of a function, and so on must have a perfectly specified type. This has many advantages: the compiler warns us about errors when we compile the program, the IDE gives us hints while we write it, and the resulting code is more readable and easier to understand.

However, the fact that everything has to have a predetermined type sometimes removes a lot of flexibility, forces us to write excessively rigid and repetitive code, and sometimes prevents us from doing things that would make our program much simpler. For example, the idea we are exploring in this article: storing instances of different types in an array.

Designers of modern programming languages such as Swift have realized that being too rigid is not a good idea, and they have devised strategies that make the type system more flexible. For example, polymorphism, function overloading, or generics. These strategies obviously make languages more complex, both in terms of learning them and in the internal workings of the compiler. But in the end they are appreciated by developers because they allow the code to be more expressive and simpler.

We can view the problem we are discussing in this article as a concrete example of that trade-off, that search for flexibility inside a strongly typed language.

Let us now explain the different ways Swift provides for solving the main question we are asking.

The special type Any

The special type Any allows a variable to be of any type. For example, we can declare a variable with an integer and then assign a string to it:

var x: Any = 10
x = "Hola"

Although this may look equivalent to the way weakly typed languages behave, the Swift compiler is still doing its job. We cannot do much with a variable of type Any. For example, the following code produces a compilation error:

let x: Any = 10
let y: Any = 5

print(x+y)

// Error: binary operator '+' cannot be applied to two 'Any' operands

We could do the addition by downcasting:

let x: Any = 10
let y: Any = 5

print((x as! Int) + (y as! Int))

// Prints: 15

The as! operator returns the value with the indicated type. If the variable is not compatible with that type, a runtime error occurs.

Arrays of Any

So a first way to allow arrays with multiple types is to use the special type Any.

var miArray: [Any] = [1, "Hola", 3.0]

This array is similar to the Python array. The advantage is that, as we saw earlier, the Swift compiler will not let us directly operate on its values:

print(miArray[0] + miArray[1])

// error: binary operator '+' cannot be applied to two 'Any' operands

We can, however, use downcasting to process the elements of the array. We can use a switch to determine the type of each element:

for thing in miArray {
    switch thing {
    case let algunInt as Int:
        print("Un entero con valor de \(algunInt)")
    case let algunDouble as Double:
        print("Un double con valor de \(algunDouble)")
    case let algunString as String:
        print("Una cadena con valor de \"\(algunString)\"")
    default:
        print("Alguna otra cosa")
    }
}

It prints:

Un entero con valor de 1
Una cadena con valor de "Hola"
Un double con valor de 3.0

It seems that we now have a strategy that solves our problem. What is the downside? Precisely the need to use downcasting and the excessive freedom it gives us. Downcasting makes the code somewhat more confusing. And the ability to store anything in the array makes the code more error-prone. Developers may be tempted to use the as! operator, making the code less robust and more likely to break at runtime.

Arrays built from enums with associated values

Could we limit the types included in the array to a specific set? Suppose, for example, that I only need my array to contain integers, strings, and floating-point numbers. Is there any Swift feature that allows that?

There is. One way to do it is through enum types. In Swift, enum types are very powerful. It is possible to associate tuples of values with concrete instances of the type. We can, for example, define a type that can be an integer, a string, or a real number, and associate a value of that type with each enum case:

enum Miscelanea {
    case entero(Int)
    case cadena(String)
    case real(Double)
}

And we can create an array of instances of that type:

var miArray: [Miscelanea] = [.entero(1), .cadena("Hola"), .real(2.0)]

To traverse the array, we will need to use a switch again:

for thing in miArray {
    switch thing {
        case let .entero(algunInt):
            print(algunInt)
        case let .cadena(algunaCadena):
            print(algunaCadena)
        case let .real(algunDouble):
            print(algunDouble)
    }
}

This prints the same as before:

1
Hola
2.0

The advantage now is that the code is completely safe. We cannot add anything to the array that is not one of the enum variants, and the language correctly controls all the possible options we can have inside the array.

But this solution also has some problems. First of all, it is excessively rigid. What happens if in the future we want to expand the types included in the array? For example, to add booleans. We could not do it additively; we could not extend the code’s functionality just by adding new pieces. We would have to rewrite the Miscelanea type to include the new case and recompile the application.

The second problem is that this solution does not let us include instances of structures or classes in the array. Suppose we are designing an application for geometric figures and want to store a collection with different kinds of shapes: rectangles, squares, triangles, and so on. We would not be able to do it this way.

That brings us to the next solution.

Arrays of a protocol type

Another solution, a more flexible one, for storing different types in an array is to use a protocol, or a superclass.

In general, if we want to group several items into a collection, it is because they all share some property. We can specify that property in a protocol and make all the types we store in the array conform to that protocol.

In the example of the array of geometric shapes, we would need to look for some property shared by all those shapes and define a Figura protocol with that property or properties. The concrete types Rectangulo, Cuadrado, Triangulo, and so on would then need to conform to the Figura protocol. And then we could declare an array of Figuras.

Let us look at a simple example. Suppose that all the items we store in the array are items that have a name, a String. We can define a protocol with that property:

protocol Nombrable {
    var nombre: String {get}
}

Once we have created this protocol, we can make the types we add to the array fulfill that property.

Instead of creating new types for the example, Swift lets us extend the existing types Int, String, and Double with the nombre property and make them conform to the Nombrable protocol:

extension Int: Nombrable {
    var nombre: String {String(self)}
}

extension String: Nombrable {
    var nombre: String {self}
}

extension Double: Nombrable {
    var nombre: String {String(self)}
}

And now we can create the array of nameable things and add instances of the previous types to it:

var miArray: [Nombrable] = [1, "Hola", 2.0]

for thing in miArray {
    print(thing.nombre)
}

This prints:

1
Hola
2.0

This solution of using a protocol or a superclass to define the array is the most flexible and the most widely used. It is more advisable to use a protocol, because both structures and classes can conform to it. If we define a superclass, we could only use it with classes, since in Swift structures do not support inheritance.

Unlike enums, if in the future we want to expand the array to include new types, all we would have to do is make those new types conform to the protocol on which the array is defined.

For example, we could include booleans in our array:

extension Bool: Nombrable {
    var nombre: String {
        self ? "true" : "false"
    }
}

var miArray: [Nombrable] = [1, "Hola", 2.0, false]

for thing in miArray {
    print(thing.nombre)
}

This prints:

1
Hola
2.0
false

The problem with generics

The previous solution, defining a protocol for the array’s components, seems like the perfect solution. It provides flexibility and extensibility. Although we did not see it in the example, it also allows downcasting and obtaining instances of the concrete type of the data by using a switch statement.

But there is one aspect we have not considered. One of the most important features of Swift is its commitment to generic types. Since the beginning of the language, there has been a roadmap, in the form of a manifesto, that has been implemented gradually in each new version of the language.

In the case of protocols, we can make some element of the protocol generic by using an associated type. In fact, in SwiftUI a view is a generic protocol with an associated type, see the reference for the View protocol in Apple’s documentation.

What happens, then, if as the array type we use a generic protocol, a protocol that has an associated type? What happens if we create an array of SwiftUI views? It turns out that things become a little more complicated and the compiler produces an error.

var array: [View] = []

// Error: Protocol 'View' can only be used as a generic constraint
// because it has Self or associated type requirements

What is going on? Better to leave that for another post, since this one has already grown too long.

References

Addendum (April 2025) - What has happened in Swift over these three years?

1. The new any prefix for existential types

Situation Before Now (>= Swift 5.6)
Existential variable / property var x: Codable var x: any Codable
Array of protocols (Codable, etc.) [Codable] [any Codable]

Updated example:

var miArray: [any Nombrable] = [1, "Hola", 2.0]

The compiler still accepts the old syntax, but it emits the warning: “Implicit use of ‘Any’ for existential types is deprecated.”

2. some now works in more places

Since Swift 5.7, opaque types can also be used in:

  • Function parameters
    func wrap(_ builder: () -> some View) -> some View { ... }
    
  • Stored properties with an initial value
    let cache: some Hashable = Set<Int>()
    

3. Arrays of View: still not allowed, use AnyView

[View] or [any View] still do not compile because View has an associatedtype Body.

// 'View' has Self or associated type requirements
var vistas: [any View] = []

Official pattern, the type eraser:

var vistas: [AnyView] = [
    AnyView(Text("Hola")),
    AnyView(Image(systemName: "star"))
]

4. The special type Any does not change

Any does not take the any prefix. All the examples using Any remain valid.

5. Changes that do not affect this article

  • buildPartialBlock and parameter packs affect result builders, not heterogeneous arrays.
  • Strict Concurrency only affects you if you mix async with existentials, which you are not doing here.

6. References for further reading

  • SE-0335 - Introduce existential any
  • The Swift Programming Language -> Macros -> Existential Types
  • WWDC22 - “Embrace type abstraction with opaque types”