BS
BleepingSwift
Published on
5 min read
Intermediate

> Module selectors in Swift 6.3

Share:

If you have ever hit a compiler error because two modules exported the same type name, or because your own module defined a type that happened to shadow one you imported, you know that Swift's story around name disambiguation has had some awkward corners. Qualifying with the module name sometimes worked, sometimes did not, and in a few cases (like macros) was not even syntactically expressible.

Swift 6.3 ships SE-0491: Module Selectors, which introduces a dedicated :: operator for pointing at a declaration from a specific module. It looks like this.

Swift
import RocketEngine

struct RocketEngine { }

let fuel = RocketEngine::Fuel()

Without the ::, the compiler would see RocketEngine.Fuel and look for a Fuel nested inside the local RocketEngine struct. With the selector, it knows to skip the local struct entirely and look up Fuel as a top-level declaration of the RocketEngine module.

Why . was not enough

Dot-qualified lookup in Swift traverses whatever RocketEngine happens to resolve to in the current scope. If a local type, variable, or imported type shadows the module name, dotted access silently picks the wrong thing. The compiler cannot fall back to "well, maybe they meant the module" because dot syntax is overloaded with member access, nested type lookup, and implicit member expressions. It is genuinely ambiguous.

:: is a dedicated module-qualification operator. Its only job is to say "look up this name as a top-level declaration of this module." That removes the ambiguity and also lets the compiler give you a better error message when the declaration is not actually in the module you named.

Resolving extension ambiguity

The more common case in day-to-day code is two modules adding methods with the same name to a shared type. Consider two analytics libraries that both extend String with an event helper.

Swift
import AnalyticsA
import AnalyticsB

"checkout_started".event()  // which one?

You can now pin the call to a specific module.

Swift
AnalyticsA::String.event("checkout_started")

Selectors also work mid-path, so member types of extension members can be qualified where they live rather than at the root.

Swift
func makeEngine() -> Spacecraft.IonThruster::Engine { }

That reads as "the Engine type contributed by the IonThruster module as a nested type of Spacecraft." Previously you had to rely on the compiler picking the right one, or rename one of the conflicting extensions.

Macros finally get qualified

Macro invocations use the #name syntax, which did not have a slot for a module qualifier. If two modules exported a macro with the same name, you had to rely on import ordering or alias the module. Module selectors slot in cleanly.

Swift
#MyMacros::stringify(value)

The same applies to implicit member expressions on enums and option sets, which show up everywhere in SwiftUI modifiers.

Swift
.ModuleName::caseName

What you cannot do with it

A few limitations are worth knowing up front. Module selectors only disambiguate between declarations that lookup would otherwise consider. They do not grant access to internal declarations from outside a module, and they do not override access control. If a declaration is not visible, :: will not make it visible.

You also cannot use a selector on the name side of a new declaration.

Swift
struct NASA::Scrubber { }  // not allowed

Declarations belong to the module that is compiling them. The selector is a lookup tool, not a way to inject symbols into other modules.

Two smaller rules worth internalizing. Type parameters cannot have their member types qualified, because the actual type is not known until the generic is instantiated. And no newline is allowed between :: and the identifier that follows, which keeps parsing unambiguous.

When to reach for it

Most Swift codebases will never need module selectors. The common path is that your module names and your type names do not collide, and import order sorts out anything else. Where :: earns its place is in three specific situations: SDKs or frameworks where a type has the same name as its module (XCTest.XCTest), cases where two dependencies extend a shared Foundation type with matching method names, and macro-heavy codebases that pull in multiple macro packages. If you hit one of those, the alternative used to be renaming your own code or aliasing imports. Now you can just point at the module you meant.

One backwards-compatibility note. Older compilers cannot parse ::, so any source file using module selectors needs the Swift 6.3 toolchain. If you ship a library that needs to build on older Xcode versions, guard selector usage behind #if swift(>=6.3) or avoid it entirely in public headers until your minimum compiler is high enough.

Sample Project

Want to see this code in action? Check out the complete sample project on GitHub:

View on GitHub

The repository includes a working Xcode project with all the examples from this article, plus unit tests you can run to verify the behavior.

subscribe.sh

// Stay Updated

Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.

>

By subscribing, you agree to our Privacy Policy.