Array expression trailing closures in Swift


Greetings, traveler!

Swift’s syntax around trailing closures has steadily evolved over the years. For most types, writing Type { ... } feels natural and predictable. Until recently, however, arrays and dictionaries were a special case. If you tried to attach a trailing closure directly after an array or dictionary type expression, the parser refused to cooperate.

SE-0508 changes that. The proposal, now accepted, removes a long-standing parsing restriction and allows trailing closures after array and dictionary expressions. At first glance this may look like a minor syntactic tweak. In practice, it removes friction that many library authors have worked around for years.

Let’s look at the problem it solves and why it matters.

The original limitation

Swift treats [T] and [K: V] in a special way. These forms are ambiguous during parsing because square brackets may represent either:

  • a type expression ([String])
  • a literal (["a", "b"])

Historically, the parser avoided attaching trailing closures to array and dictionary expressions. Consider this example:

let values = [String] {
    "a"
}

Before SE-0508, this produced confusing diagnostics. The compiler often interpreted the closure as a separate expression or as something resembling a computed property body. In some contexts, you would see errors such as “closure expression is unused.” In others, parsing would fail earlier.

The behavior felt inconsistent because the same syntax worked perfectly with other types:

struct Builder {
    init(_ build: () -> String) {
        print(build())
    }
}

let value = Builder {
    "Hello"
}

For custom types, Type { ... } worked as expected. For [T], it did not.

Why this was painful in real projects

This limitation became noticeable once developers started extending Array and Dictionary with custom initializers that accept closures. A common pattern looks like this:

extension Array {
    init(_ build: () -> Element?) {
        var result: [Element] = []
        while let element = build() {
            result.append(element)
        }
        self = result
    }
}

With this initializer, the most natural call site would be:

let numbers = [Int] {
    generateNextNumber()
}

Before SE-0508, this syntax was unavailable. Developers had to fall back to one of these alternatives:

let numbers = [Int].init {
    generateNextNumber()
}

let numbers = Array<Int> {
    generateNextNumber()
}

Both forms work, but neither reads as cleanly as the square-bracket type form. In builder-style APIs, this difference becomes more visible.

Another scenario involves @resultBuilder-based APIs. If you define something like:

@resultBuilder
struct ArrayBuilder<Element> {
    static func buildBlock(_ components: Element...) -> [Element] {
        components
    }
}

extension Array {
    init(@ArrayBuilder<Element> _ build: () -> [Element]) {
        self = build()
    }
}

The intuitive call site is:

let items = [String] {
    "First"
    "Second"
}

Again, this was blocked by parsing rules, even though the underlying type system could support it.

Why the compiler struggled

The core issue lies in how Swift parses square brackets. At the parsing stage, [String] could be:

  • an array type
  • an array literal with a single element if String were a variable in scope

The parser does not perform full type checking at this stage. It makes syntactic decisions first and resolves meaning later. To avoid ambiguous or misleading parses, Swift previously disallowed trailing closures after array and dictionary expressions.

This restriction made sense when trailing closures were primarily associated with function calls and when callAsFunction did not exist. Over time, the language gained more expressive power, and the restriction began to feel arbitrary.

How SE-0508 changes the behavior

SE-0508 allows trailing closures after array and dictionary expressions. That means the following is now valid:

let values = [String] {
    "A"
    "B"
}

If Array has an initializer that accepts a closure, the expression is treated as a normal call:

[Element](closure)

In other words, [String] { ... } is parsed as a constructor invocation on the array type expression.

The same applies to dictionaries:

let dict = [String: Int] {
    ("one", 1)
    ("two", 2)
}

As long as a matching initializer exists, the syntax works naturally.

Interaction with callAsFunction

There is another interesting side effect. Swift allows types to define callAsFunction. With SE-0508, you can now write code such as:

extension Array {
    func callAsFunction(_ transform: (Element) -> Element) -> [Element] {
        map(transform)
    }
}

let result = ["a", "b", "c"] {
    $0.uppercased()
}

Here, the array literal is followed by a trailing closure, and the compiler interprets it as a callAsFunction invocation. Previously, this form would not parse.

This opens room for expressive APIs, although it also means code reviewers should pay closer attention to array literals followed by closures. That pattern may now represent a real call rather than an unused closure.

What this means for library authors

If you maintain utility extensions around Array or Dictionary, you can now design APIs that feel consistent with the rest of Swift’s initializer style.

For example, a streaming initializer:

extension Array {
    init(sequence: () -> Element?) {
        var elements: [Element] = []
        while let value = sequence() {
            elements.append(value)
        }
        self = elements
    }
}

Call site:

let generated = [Int] {
    nextRandomValue()
}

Or a builder-based DSL for configuration:

let layout = [Constraint] {
    Constraint.leading(8)
    Constraint.trailing(8)
    Constraint.height(44)
}

The syntax now matches the mental model many developers already had.

Source compatibility considerations

The proposal acknowledges a narrow source compatibility impact. In some rare cases, developers relied on the fact that a closure after an array literal would be treated as a separate expression, especially inside result builders. With SE-0508, that closure may now bind to the preceding array expression.

In practice, most of those patterns either failed to compile or behaved inconsistently depending on formatting. The new rule reduces that ambiguity.

It is also worth noting that this syntax was already available for InlineArray. You could write an InlineArray type with a trailing closure, and the parser accepted it without issue. That made the restriction for Array and Dictionary feel even more arbitrary. SE-0508 removes that inconsistency and brings collection types back into alignment with the rest of the language surface.

Conclusion

SE-0508 does not introduce new types or runtime behavior. It refines how the parser understands a specific pattern. Yet the effect is tangible in DSL-heavy Swift codebases and libraries that lean on closure-based initializers.

The square-bracket type form has always been the idiomatic way to express arrays and dictionaries. Allowing trailing closures there removes a special case that developers had to remember and work around. The language becomes slightly more uniform, and expressive APIs around collections feel more natural.

Sometimes progress in a language comes from bold new features. Other times it comes from smoothing a rough edge that has been there for years. This proposal belongs to the latter category, and Swift feels more consistent because of it.