Pitch: Initialization Wrappers
Introduction
Swift provides property wrappers for intercepting access to individual properties and macros for generating code at compile time. However, there is currently no mechanism for intercepting the initialization of an entire value and participating in its initialization process.
This proposal introduces init wrappers, an attribute that can be applied to an initializer declaration. An initialization wrapper acts as a customizable initialization mechanism for a type, allowing library authors to participate in object creation while preserving the language's initialization guarantees. All wrapper dispatch and property coverage verification happens at compile time, with no runtime reflection required.
Motivation
The class constraint imposed by type-safe DI
The most direct motivation for this proposal is a limitation typical of dependency injection use cases: you must accept mutable state, and often reference semantics, as a compromise, especially if you're using property wrappers, and/or key paths to guarantee type safety. We end up using a lot more classes than we might normally in these scenarios as a result.
One reason for that is, Swift property wrappers support an _enclosingInstance subscript that allows the wrapper to reach back into the enclosing type to resolve its value. This is the mechanism that makes scope-aware, key-path-driven DI possible:
public static subscript<Wrapper: ..., Enclosing: ...>(
_enclosingInstance instance: Enclosing,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<Enclosing, Value>,
storage storageKeyPath: ReferenceWritableKeyPath<Enclosing, Wrapper>
) -> Value {
get {
instance[keyPath: storageKeyPath].resolve(
instance: instance,
keyPath: wrappedKeyPath
)
}
set {
instance[keyPath: storageKeyPath].override = .some(newValue)
}
}
Notice the constraint: ReferenceWritableKeyPath. This subscript overload only exists for reference types. The compiler has no equivalent path for value types, and this is not an oversightâa struct passed to a subscript is a copy, so any writes to storageKeyPath would be discarded. The only way to make this pattern work today is:
final class ReportService: Injectable {
var scope: Scope
@Inject var database: Database
@Inject var cache: Cache
// Explicit defaultâused when the scope has no Analytics
@Inject var analytics: Analytics = Analytics(enabled: false)
init(scope: Scope) { self.scope = scope }
}
This works, but the design is dictated by a language limitation rather than by intent. The properties are var not because mutability is desired, but because the _enclosingInstance mechanism requires a reference type and a mutable storage key path. The class is load-bearing infrastructure, not a meaningful semantic choice.
Why inout doesn't rescue structs
A natural question is whether the _enclosingInstance subscript could be extended to support inout passing of mutable structs, avoiding the class requirement. This does not resolve the underlying problem.
Even if the subscript were extended to accept an inout struct, making the struct mutable enough for property wrapper resolution to work during initialization would require var properties throughout. That defeats the central goal: using DI to produce immutable value types whose properties are set exactly once, during initialization, and cannot be modified afterward. A mutable struct with var properties is not an immutable valueâit is a class with value semantics stapled on.
The deeper issue is that property wrapper resolution via _enclosingInstance is fundamentally a post-initialization mechanism. The enclosing instance already exists and is being accessed. Immutable let properties cannot participate in this model at all, because they cannot be written after the initializer exits. Even for var properties, property wrapper resolution is reactiveâit responds to accessârather than constructive. It cannot drive initialization.
What is actually needed
What DI frameworks need is a way to participate in initialization before the value is sealedâa phase where all stored properties can be assigned exactly once, under the framework's control, with full type safety, and without requiring the target type to be a class or to expose mutable state. That is precisely what initialization wrappers provide.
With this proposal, the ReportService example above could be expressed as a struct:
struct ReportService {
let scope: Scope
let database: Database
let cache: Cache
let analytics: Analytics
@Injectable
init(scope: Scope)
}
The properties are let. The type is a struct. The framework still resolves dependencies via key pathsâbut it does so during the initialization phase, not by reaching back into an already-live instance. The class was never the right tool; it was the only tool available.
Other benefits for existing DI libraries
Init Wrappers might also benefit popular DI libraries like PointFree's Dependencies, which must currently maintain mutable properties on structs to provide dependency injection:
struct Feature {
@Dependency(\.apiClient) var apiClient
@Dependency(\.continuousClock) var clock
@Dependency(\.uuid) var uuid
// ...
}
(Source: Dependencies documentation on Github.)
They use a KeyPath-Value store to match dependencies within the property wrapper, but property wrappers can only be used on mutable vars.
Whereas with Init Wrappers, perhaps the following might be preferable:
struct feature {
let apiClient: APIClient
let clock: Clock
let uuid: UUID
@Dependency init()
}
Other motivating use cases
Beyond DI, many frameworks need to initialize immutable value types from external sources:
- Serialization and deserialization systems
- Configuration providers
- Test fixture generators
- Persistence frameworks
Today, such systems typically require one of the following:
- Public memberwise initializers (exposes internals)
- Mutable properties (sacrifices immutability)
- Generated boilerplate (maintenance burden)
- Unsafe memory manipulation (correctness risk)
- Custom code generation tools (build complexity)
In each case, the real constraint is the same one that affects DI: there is no language-supported hook for participating in initialization before a value is sealed, so frameworks are forced to either compromise the type's design or work around the language entirely.
Proposed Solution
Allow attributes to be applied to initializer declarations:
struct User {
let id: UUID
let name: String
@Injectable
init()
}
When User() is invoked, control is transferred to the initialization wrapper.
Conceptually:
let user = User()
becomes:
let user = Injectable.make(User.self)
The wrapper receives a compile-time-generated initialization context describing the target type and is responsible for producing a fully initialized instance.
Compile-Time Property Coverage
Swift's compiler already walks a type's stored properties for several existing features:
- Memberwise initializer synthesis enumerates stored properties to generate parameter lists and assignment bodies
Codablesynthesis walks stored properties to emitencodeanddecodebodies, verifying that all non-optional properties are handled- Definite initialization (DI) analysis already understands what "fully initialized" means for a type and enforces it at every exit point of an initializer
Initialization wrappers build directly on this existing infrastructure. The compiler already has the machinery to enumerate stored properties, know their types and key paths, and determine whether a given initialization path covers all required properties. This proposal exposes that capability to wrapper authors through a structured compile-time context rather than adding new analysis from scratch.
The compiler verifies at the call siteânot at runtimeâthat a wrapper's make implementation satisfies full property coverage for the target type. A wrapper that fails to initialize any non-optional, non-defaulted stored property is a compile-time error.
initialization Context
Initialization wrappers receive a type-specific initialization context generated entirely at compile time:
protocol InitializationWrapper {
static func make<T>(
_ type: T.Type,
using context: initializationContext<T>
) throws -> T
}
The context exposes compiler-generated metadata, including:
- Typed key paths to each stored property
- Static type information for each property
- Default values where declared
- Optional framework-specific metadata supplied via additional attributes
No runtime type introspection is involved. The initializationContext for a given type T is fully described by what the compiler already knows about T at the point of the wrapper attribute.
Immutable Property Initialization
A key goal is enabling frameworks to initialize immutable stored properties during initialization.
Today, WritableKeyPath cannot be used to assign values to let properties after initialization, which is consistent with Swift's immutability guarantees.
Initialization wrappers do not mutate an already-initialized value. Instead, they operate during a dedicated initialization phaseâanalogous to the phase the compiler already enforces for memberwise and synthesized initializersâin which all stored properties are initialized exactly once before the value becomes visible to user code. The compiler's existing definite initialization analysis enforces this: each property path may be written exactly once, and the initialization phase must cover all required properties before the call returns.
Conceptually:
context.initialize(\.id, with: generatedID)
context.initialize(\.name, with: resolvedName)
After initialization completes:
let user = User()
the resulting value is fully immutable and the initialization context is discarded.
Example: Dependency Injection
struct ViewModel {
let logger: Logger
let network: NetworkClient
@Injectable
init()
}
The wrapper resolves dependencies using compile-time key path metadata:
let vm = ViewModel()
without requiring a manually-written initializer or a runtime lookup by string name.
Example: Fixture Generation
struct User {
let id: UUID
let name: String
let age: Int
@FixtureInitialized
init()
}
Tests can generate fully-populated immutable instances without needing memberwise initialization, mutable properties, or runtime introspection.
Non-Wrapped Properties
We'd also propose that Init Wrappers can handle this situation:
struct Foo {
let alice: Int
let bob: String
let bill: Person
@Wrapped init(bob: String) { self.bob = bob }
}
Here, the compiler would only look to the Wrapped property wrapper to satisfy alice and bill.
Concurrency and the Actor Model
Initialization wrappers are designed to compose cleanly with Swift's structured concurrency and actor model.
initialization happens on the caller's executor
Because the wrapper's make method is invoked synchronously at the call siteâconceptually replacing the init callâit inherits the caller's isolation context. If ViewModel() is called on the @MainActor, the wrapper's make call runs on the main actor's executor. No extra annotation is required for the common case.
Global actor-isolated types
When a type is annotated with a global actor, the compiler already enforces that its stored properties are only accessed from that actor. Initialization wrappers respect this: the make method will be required to match the isolation of the type being initialized. A @MainActor-isolated struct with an @Injectable init() will require Injectable.make to be @MainActor or nonisolated with appropriate Sendable constraints on the values it produces.
@MainActor
struct AppState {
let config: Config
let router: Router
@Injectable
init()
}
The compiler enforces that the wrapper satisfies the actor isolation requirements of AppState, just as it would for a hand-written @MainActor init.
async wrappers
Many real DI and persistence use cases are inherently asynchronousâresolving a dependency may require an async lookup. Initialization wrappers can be declared async, mirroring how Swift allows async initializers today:
protocol AsyncInitializationWrapper {
static func make<T>(
_ type: T.Type,
using context: initializationContext<T>
) async throws -> T
}
At the call site, this is handled naturally:
let vm = await ViewModel()
The compiler enforces that an async wrapper is only used from an async context, consistent with how async init works today.
Sendable and cross-actor passing
The initialized value itself is subject to the same Sendable rules as any other value. If a wrapper initializes a value on one actor and the value is then passed to another, the type must conform to Sendable. This is not a special constraint introduced by initialization wrappersâit is the same rule that applies to any value crossing an actor boundary. Wrappers do not weaken or bypass these guarantees.
The initialization context is not Sendable
The initializationContext<T> is scoped to a single initialization call and is not intended to escape. It would be a non-Sendable type, preventing it from being passed across actor boundaries or captured in a closure that outlives the initialization phase. This ensures the context cannot be used to perform deferred or concurrent writes to the properties of a value that has already been sealed.
Relationship to Macros
Swift macros (specifically @attached(member)) can already generate init bodies today. Initialization wrappers are complementary rather than redundant:
- Macros generate source code that is then compiled normallyâthey are a code generation tool
- Initialization wrappers are a dispatch point backed by compile-time-verified coverageâthey allow library types to participate in initialization without the calling module needing to expand or inspect generated source
The two features can work together: a macro could emit the @WrapperName init() declaration itself, with the wrapper handling the actual initialization logic.
Future Directions
Initialization wrappers could integrate naturally with:
- Dependency injection systems
- Serialization and deserialization frameworks
- Test data generation
- SwiftData-style persistence frameworks
Because the feature is grounded in existing compiler infrastructureâmemberwise synthesis, Codable synthesis, and definite initialization analysisâit provides a stable foundation for these directions without requiring new compiler analysis phases.
Related Past Pitches and Proposals
[Pitch] Type Wrappers
Type Wrappers is the closest prior proposal in ambition. It also aims to intercept the initialization of a type and allow a wrapper to participate in initialization. The shared motivation is real: both proposals stem from the observation that frameworks often need to do work during object initialization that the language does not currently support.
The key difference is scope and weight. Type Wrappers proposes a comprehensive mechanism that wraps the entire typeâstorage, access, and initializationâwhich gives it broad power but also introduces significant surface area and complexity. Initialization wrappers are deliberately narrower: they intercept only the initialization phase, leaving access patterns to existing tools like property wrappers. This makes the feature easier to reason about, easier to implement, and less likely to interact unexpectedly with other language features. Where Type Wrappers is a new kind of type-level abstraction, initialization wrappers are closer to a structured extension of existing initializer synthesis.
[ProposalâAccepted] Init Accessors (SE-0400)
Init Accessors is directly relevant and already accepted. It allows a computed property to declare an init accessor, giving it the ability to participate in initialization and enabling property wrappers to work with let-backed storage in init bodies. This solves one specific pain pointâproperty wrappers that need to initialize backing storageâbut it does so at the level of individual properties, not at the level of the whole initialization call.
Initialization wrappers and Init Accessors are complementary. Init Accessors address the case where you have an init body and want individual properties within it to behave specially. Initialization wrappers address the case where you want the entire init to be delegated to an external authorityâno init body at all, or a partial oneâwith the wrapper responsible for covering all stored properties. A DI framework using Init Accessors still needs a written (or generated) init body; one using initialization wrappers does not.
[Pitch] Allow Property Wrappers on Let Declarations
This pitch addresses the limitation that property wrappers cannot currently be applied to let properties. It shares the same underlying frustration as this proposal: the combination of property wrappers and immutability is poorly supported. If this pitch were accepted, it would remove one friction point for DI frameworksâ@Inject let database: Database would be legalâbut it would not resolve the deeper _enclosingInstance / ReferenceWritableKeyPath constraint that forces class usage. Property wrappers on let declarations still resolve their values reactively, post-initialization. They cannot be driven by a wrapper that is given control of the whole initialization phase.
Accepting both proposals would be coherent: this proposal handles the initialization side, and allowing property wrappers on let handles the access side.
[Pitch] Compositional Initialization
An earlier pitch by the same author as this proposal. Compositional Initialization explored letting types declare that their initialization should be composed from smaller, independently-defined pieces, with the compiler assembling the pieces into a complete init. The motivation is closely related: reducing boilerplate, enabling frameworks to participate in init, and supporting immutable types.
This proposal can be seen as a more focused successor. Where Compositional Initialization was concerned with the structure of init bodies, initialization wrappers are concerned with delegating the entire init to a well-typed external construct. The earlier pitch informed the design thinking here, particularly around how coverage verification should work.
[Proposal] Flexible Memberwise Initialization (SE-0018)
SE-0018 is a long-standing accepted-but-not-implemented proposal that would extend Swift's memberwise initializer synthesis to be more configurableâallowing access control over individual parameters, default values, and suppression of certain properties. It addresses the ergonomics of the synthesized memberwise init but does not provide a hook for frameworks to participate in initialization.
SE-0018 and this proposal operate at different levels. SE-0018 is about making the compiler-generated memberwise init more expressive. Initialization wrappers are about replacing or augmenting the init entirely. If SE-0018 were implemented, it would reduce some of the boilerplate that pushes developers toward the patterns initialization wrappers are meant to replace, but it would not make initialization wrappers unnecessary.
[ProposalâAccepted] Property Wrappers (SE-0258)
The direct predecessor to this proposal in spirit. SE-0258 established the _enclosingInstance subscript mechanism that enables scope-aware property wrappersâand in doing so, drew a clear line at the class boundary that this proposal aims to erase. Much of the motivation section of this pitch is a direct consequence of the design choices in SE-0258: the ReferenceWritableKeyPath constraint is not a bug in property wrappers, it is a fundamental consequence of how _enclosingInstance works, and initialization wrappers are the correct place to handle the struct case rather than stretching the property wrapper model further.
[ProposalâAccepted] Attached Macros (SE-0389)
SE-0389 introduced @attached(member) macros, which can generate init bodies. As discussed in the Relationship to Macros section above, macros are a code generation tool while initialization wrappers are a dispatch mechanism. Macros can prototype some of the patterns this proposal targets, and the two features compose naturally. However, macros require the generated source to be valid Swift that passes normal type-checking, which means a macro-generated init body for a DI framework still needs to know at macro-expansion time what dependencies to injectâit cannot defer that decision to runtime or to a separate framework-provided resolver.
Conclusion
Initialization wrappers provide a language-supported mechanism for participating in value initialization while preserving Swift's initialization model and immutability guarantees. The core motivation is concrete: type-safe, key-path-driven DI frameworks are today structurally forced to use classes, because _enclosingInstance property wrapper resolution requires ReferenceWritableKeyPath and operates post-initialization. Neither making structs mutable nor extending the subscript to support inout resolves this, because both approaches sacrifice the immutability that makes value types worth using in the first place.
Rather than introducing new compiler machinery, this feature surfaces capabilities the compiler already exercises for memberwise initializers and Codable synthesis, making them available to wrapper authors in a structured, compile-time-verified way. The result is a powerful framework primitive that enables patterns currently difficult or impossible to express without code generation, unsafe APIs, or mutable stateâand does so entirely at compile time.