- Published on
- 5 min read Intermediate
> Module selectors in Swift 6.3
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.
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.
import AnalyticsA
import AnalyticsB
"checkout_started".event() // which one?
You can now pin the call to a specific module.
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.
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.
#MyMacros::stringify(value)
The same applies to implicit member expressions on enums and option sets, which show up everywhere in SwiftUI modifiers.
.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.
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:
The repository includes a working Xcode project with all the examples from this article, plus unit tests you can run to verify the behavior.
// Continue_Learning
Data Races Swift 6 Still Can't Catch
Swift 6's data-race safety is real, but it has blind spots. Here are the places the compiler can't see, and how to stop treating a clean build as proof your code is thread safe.
Async defer in Swift 6.4
SE-0493 finally lets you write defer { await cleanup() } in async functions, without spawning a detached task or threading cleanup logic through every return path.
Task Cancellation Shields in Swift 6.4
Swift 6.4's withTaskCancellationShield lets cleanup code run to completion even after a task has been cancelled, without spawning extra unstructured tasks.
// Stay Updated
Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.