# \[Pitch\] Init Wrappers

**URL:** <https://forums.swift.org/t/pitch-init-wrappers/87095>\
**Category:** Pitches\
**Created:** [June 3, 2026, 8:20pm UTC](https://forums.swift.org/t/pitch-init-wrappers/87095 "2026-06-03T20:20:28Z")\
**Posts on this page:** 7\
**Page:** 1

<div class="post-metadata">

**Author:** ![vitamin](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/vitamin/32/11098_2.png) [@vitamin](https://forums.swift.org/u/vitamin)\
**Post date:** [June 3, 2026, 8:20pm UTC](https://forums.swift.org/t/pitch-init-wrappers/87095/1 "2026-06-03T20:20:28Z")

</div>

# 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:

```swift
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:

```swift
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:

```swift
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](https://github.com/pointfreeco/swift-dependencies/), which must currently maintain mutable properties on structs to provide dependency injection:

```swift
struct Feature {
  @Dependency(\.apiClient) var apiClient
  @Dependency(\.continuousClock) var clock
  @Dependency(\.uuid) var uuid
  // ...
}

```

(Source: [Dependencies documentation on Github](https://swiftpackageindex.com/pointfreeco/swift-dependencies/main/documentation/dependencies/dependency).)

They use a KeyPath-Value store to match dependencies within the property wrapper, but property wrappers can only be used on mutable `var`s.

Whereas with Init Wrappers, perhaps the following might be preferable:

```swift
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:

```swift
struct User {
    let id: UUID
    let name: String

    @Injectable
    init()
}

```

When `User()` is invoked, control is transferred to the initialization wrapper.

Conceptually:

```swift
let user = User()

```

becomes:

```swift
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
- **`Codable` synthesis** walks stored properties to emit `encode` and `decode` bodies, 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:

```swift
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:

```swift
context.initialize(\.id, with: generatedID)
context.initialize(\.name, with: resolvedName)

```

After initialization completes:

```swift
let user = User()

```

the resulting value is fully immutable and the initialization context is discarded.

## Example: Dependency Injection

```swift
struct ViewModel {
    let logger: Logger
    let network: NetworkClient

    @Injectable
    init()
}

```

The wrapper resolves dependencies using compile-time key path metadata:

```swift
let vm = ViewModel()

```

without requiring a manually-written initializer or a runtime lookup by string name.

## Example: Fixture Generation

```swift

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:

```swift
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.

```swift
@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:

```swift
protocol AsyncInitializationWrapper {
    static func make<T>(
        _ type: T.Type,
        using context: initializationContext<T>
    ) async throws -> T
}

```

At the call site, this is handled naturally:

```swift
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](https://forums.swift.org/t/pitch-type-wrappers/60019/12)

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)](https://github.com/hborla/swift-evolution/blob/init-accessors/proposals/0400-init-accessors.md)

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](https://forums.swift.org/t/pitch-allow-property-wrappers-on-let-declarations/61750)

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](https://forums.swift.org/t/compositional-initialization/29535)

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)](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0018-flexible-memberwise-initialization.md)

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)](https://github.com/apple/swift-evolution/blob/main/proposals/0258-property-wrappers.md)

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)](https://github.com/apple/swift-evolution/blob/main/proposals/0389-attached-macros.md)

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.

---

<div class="post-metadata">

**Author:** ![ktoso](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/ktoso/32/44526_2.png) [@ktoso](https://forums.swift.org/u/ktoso)\
**Post date:** [June 4, 2026, 5:36am UTC](https://forums.swift.org/t/pitch-init-wrappers/87095/2 "2026-06-04T05:36:08Z")

</div>

This doesn't read to me as something that needs an entirely new mechanism and the section about why can't it be done with macros is somewhat light and not detailed -- you might want to expand on this, or rather, work backwards -- what are macros missing that would enable you to write such macros if you wanted to? Consider combining a macro on the type level and on the initializer as well perhaps.

---

<div class="post-metadata">

**Author:** ![vitamin](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/vitamin/32/11098_2.png) [@vitamin](https://forums.swift.org/u/vitamin)\
**Post date:** [June 4, 2026, 9:20pm UTC](https://forums.swift.org/t/pitch-init-wrappers/87095/3 "2026-06-04T21:20:18Z")

</div>

# Why Init Wrappers Cannot Be Implemented as a Macro

It's because macros are governed by the same compiler limitation as writing an init by hand.

The macro is just automating what you could type yourself. Init wrappers provide something you _cannot_ express by hand or by macro: a generic construction protocol that the compiler verifies covers all stored properties without knowing which type is being constructed at the time the protocol is written.

That's because a macro is a compile-time text transformation. Macros run before type-checking and produce Swift source code that then gets compiled normally and type-checked normally.

Init wrappers on the other hand are a new compiler feature that enables something you can't code today, a **runtime dispatch point** that the compiler verifies but does not eliminate.

In detail...

## 1. Macros must know the answer at expansion time

A macro expands once, producing static source code. For a DI macro to generate an `init` body, it must decide at expansion time which dependencies to inject and how. It can inspect the stored properties of the type syntactically, but it cannot defer the question of _where the values come from_ to the framework at runtime.

Compare:

**What a macro can generate:**

```swift

// Macro sees `let apiClient: APIClient` and emits:

init() {

self.apiClient = Container.shared.resolve(APIClient.self)

}

```

**What an init wrapper does:**

```swift

// The wrapper's construct() is called at runtime with a live context:

static func construct(_ type: ViewModel.Type, using context: ConstructionContext<ViewModel>) -> ViewModel {

context.initialize(\.apiClient, with: self.scope.resolve(context.keyPath(\.apiClient)))

}

```

The macro has to hard-code the resolution strategy into the generated source. The wrapper receives a live scope object and can make runtime decisions—consult a registry, check a cache, apply overrides set up in a test, resolve asynchronously. A macro cannot express any of that because it does not exist at the time the macro runs.

## 2. Macros cannot participate in the caller's execution context

When you write `let vm = ViewModel()`, an init wrapper's `construct` method is invoked at that call site with whatever context the caller has established—a specific DI scope, a test override environment, an actor's executor. The call site is live code.

A macro-generated init body is just an init body. It runs with no special relationship to the caller beyond what a normal init has. There is no mechanism for the caller to pass a live context _into_ a macro-generated init without that context being explicitly threaded through as a parameter—which defeats the point of transparent construction.

## 3. The verification problem is different in kind

An init wrapper requires the compiler to verify that `construct` will fully initialize all stored properties of `T` for _any_ `T` it might be applied to. This is a constraint on a generic method across all future uses.

A macro generates a specific init body for a specific type, and the compiler just type-checks that body normally. The macro itself is not responsible for proving coverage—the generated code either compiles or it doesn't. But that means the macro has to get it right for each type individually, with no language-enforced contract that the macro author has to satisfy.

In other words: with a macro, if you forget to handle a property, you get a compile error in the _generated code_ for _that type_. With an init wrapper, the protocol contract `construct` must satisfy is expressed once and verified for every application of the wrapper. The wrapper author writes a proof once; the macro author has to get it right every time.

## 4. Macros cannot introduce new initialization semantics for `let` properties

A macro-generated init body is subject to exactly the same rules as a hand-written one. It can assign `let` properties during init—that is normal Swift. But it cannot express the construction-phase semantics that init wrappers require: the idea that an external entity is performing those assignments through key paths, with the compiler tracking that each path is written exactly once.

If a macro generates `self.apiClient = wrapper.resolve(\.apiClient)`, that is a normal property assignment in a normal init body. It works for `var` and for `let` (in an init). But if the wrapper needs to conditionally assign, or assign in a loop over property descriptors, or build the assignments programmatically at runtime—the macro cannot generate that. The macro has to know the structure statically and emit explicit assignments for each property.

Again, it's the same limitation as if you wrote it yourself.

## 5. Macros can't intercept the call site

A macro applied to a type or to an `init` declaration transforms the declaration. It does not change what `ViewModel()` means at the call site—that still resolves to the generated init, which runs synchronously in the normal Swift init dispatch path.

An init wrapper _replaces_ the call site's semantics. `ViewModel()` becomes `Injectable.make(ViewModel.self, using: context)`. That distinction matters for:

- **Async initialization** —`await ViewModel()` only works if the call site understands the init is async. A macro cannot make an existing `init()` async at the call site without changing the declaration, which changes every call site.

- **Throwing construction** —same issue.

- **Scope injection** —the wrapper receives a live scope object. A macro-generated init has no way to receive one without an explicit parameter.

## 6. Macros at a function body can't see the instance variables

A macro might be able to do this:

```swift
@Injectable
struct ViewModel {
    let logger: Logger
    let network: NetworkClient
}

```

But it can't do this:

```swift
struct ViewModel {
    let logger: Logger
    let network: NetworkClient
    @Injectable
    init()
}

```

And there are key differences between what you can do with the two:

- With the former, the resolution strategy is baked in at expansion time.
- There is no way to pass a different scope, container, or context at the call site. This is precisely the limitation described in point 1.
- There's no way in the type wrapper/macro model to have a `let` that is instance-scoped—i.e., resolved from a specific object owned by this instance rather than the ambient task-local.
- If you wrap the whole type, but you need to add an additional property to the object later, with an initializer that supplies that value, you're going to need a bigger macro. So there are scalability concerns there.
- An init wrapper's construct method receives a live context object instead, which is the thing the macro model simply cannot express.

Hope that helps

---

<div class="post-metadata">

**Author:** ![sisoje](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/sisoje/32/6120_2.png) [@sisoje](https://forums.swift.org/u/sisoje)\
**Post date:** [June 26, 2026, 5:18pm UTC](https://forums.swift.org/t/pitch-init-wrappers/87095/4 "2026-06-26T17:18:32Z")

</div>

Container-style dependency injection, resolving dependencies at runtime from an ambient registry, is an OOP-ecosystem pattern, and I don't think that machinery belongs in a value-semantic language. Bolting it on with a new language feature is the last thing Swift needs.

The pitch sells this as "immutable value types," but that framing hides where the state went. `ViewModel()` comes out immutable, fine. But `make` has to resolve `logger` and `network` from somewhere: a container, a task-local, a global registry. That somewhere is mutable, shared, long-lived, and retains the services so it can hand them out again. The struct is immutable; the system it's born from is not. The proposal doesn't remove shared mutable state, it relocates it behind a wrapper, out of sight at the call site. That relocation is the whole problem with dragging injection into value-oriented code.

And in exchange we give up the one thing explicit initializers buy us: a call site you can read. `ViewModel(logger:network:)` tells you exactly what it does. `@Injectable init()` makes construction perform arbitrary framework-defined work you can't see. For every motivating example here, init parameters with defaults already cover the use case: no registry, no attribute, no new dispatch point. I'd rather pay the explicitness than hide construction behind a resolver.

---

<div class="post-metadata">

**Author:** ![vitamin](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/vitamin/32/11098_2.png) [@vitamin](https://forums.swift.org/u/vitamin)\
**Post date:** [June 28, 2026, 3:59pm UTC](https://forums.swift.org/t/pitch-init-wrappers/87095/5 "2026-06-28T15:59:59Z")

</div>

Thanks sisoje-

This pitch is simply to allow using a `KeyPath` subtype to initialize `let` properties during `init`, and to add compiler guarantees about completeness.

Fundamentally then, Init Wrappers are not directly related to dependency injection. DI is simply the main real-world use case that came to mind when writing the pitch.

The benefit of Init Wrappers is to enable static, performant, compositional initialization. It resolves performance and safety issues with dynamic implementations like [swift-compositional-init](https://github.com/gistya/swift-compositional-init), a library of mine that evolved from a 2019 pitch I made to allow initializing an instance from a set of `WritableKeyPath`-value pairs.

Whether you like or hate DI, the reality is that DI is one of the main use-cases where people are using `WritableKeyPath` in production code today, e.g. in [PointFreeCo's Dependencies](https://github.com/pointfreeco/swift-dependencies):

```swift
    public func dependency<V>(
      _ keyPath: WritableKeyPath<DependencyValues, V>,
      _ value: V
    ) -> some Scene {
      environment((\.dependencies as WritableKeyPath).appending(path: keyPath), value)
    }

```

I don't want this thread to derail into an argument about whether or not we should use DI in Swift, but please let me respectfully address your points about DI so we can move on to discussing the merits and implementation feasibility of the pitch itself, and other use-cases besides DI.

> [@sisoje](#):
>
> resolving dependencies at runtime from an ambient registry, is an OOP-ecosystem pattern, and I don't think that machinery belongs in a value-semantic language

Swift is a mixed value/reference-semantic language, and DI in Swift neither requires an "ambient registry" nor a mutable one.

> [@](#):
>
> Bolting it on with a new language feature is the last thing Swift needs

I agree 100%... but this proposal doesn't suggest bolting on the kind of DI implementation that you take issue with.

> The pitch sells this as "immutable value types," but that framing hides where the state went.

I disagree with this assessment. We're just talking about being able to use `WritableKeyPath` during init on a `let`, instead of being restricted to only using keypaths with stored vars or property wrappers.

Given that many DI systems use `WriteableKeyPath` to add better type-safety to their injection systems, this proposal could benefit those systems by increasing static guarantees and compatibility with `let` properties. (But it doesn't change where the state lives nor is DI the only use-case this proposal benefits.)

```swift
`make` has to resolve `logger` and `network` from somewhere: a container, a task-local, a global registry. That somewhere is mutable, shared, long-lived, and retains the services so it can hand them out again.

```

This is a straw-man argument -- your straw man is a particular kind of DI system that uses a mutable, shared container to provide retained instances of services. I don't like those kinds of DI systems either.

My preferred kind of Swift DI system will inject each consumer directly at init time with an immutable DI container that uses closures to vend unique-per-injection, value-type dependencies wherever possible. For example:

```swift
struct MyNetworkShim: Injectable {
    @Inject var provider: NetProvider
    let scope: DIScope
    init(scope: DIScope) {
        self.scope = scope
    }
}

```

Here, `scope` uses `\MyNetworkShim.provider` to lookup a `() -> NetProvider` closure, which generates a fresh value instance of `NetProviderShim`. There's no ambient, stateful, or mutable container here. This pattern lets us inject a unique Logger or NetProviderShim as a value-type instance, tagged per-injection-site for improved debugging.

The downside is, we either have to generate a fresh value every time `provider` is accessed, or use the `Inject` wrapper to store the value. Either way, on the first access, `provider` has to generate its value; it's not set at init time.

Under this proposal, we improve that situation:

```swift
struct MyNetworkShim: Injectable {
    let provider: NetProvider

    @Inject
    init(scope: DIScope) {
       // does something like this: provider = scope[\Self.provider]
    }
}

```

Whether or not that is technically feasible is what I'd like us to debate here, ideally.

And maybe I'm being an idiot and there is already a way to do this... but I think we at least need a `LetKeyPath` so we know a given path is at least writable during init.

> The proposal doesn't remove shared mutable state, it relocates it behind a wrapper, out of sight at the call site. That relocation is the whole problem with dragging injection into value-oriented code.

You're entitled to any gripes about DI, but I think your issues are with a particular style of DI that often gets implemented. However that style doesn't universally apply to all DI implementations in Swift. Also, some folks simply don't like DI in general -- and that's OK -- but I think there are other use cases for Init Wrappers besides improving DI.

Maybe it would help if I can write up an example DI library that doesn't suffer the issues you're concerned with. I'll admit it's been on my TODO list for awhile... but it had looked like there were plenty of examples of this kind of thing already.

---

<div class="post-metadata">

**Author:** ![0xTim](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/0xtim/32/27968_2.png) [@0xTim](https://forums.swift.org/u/0xTim)\
**Post date:** [June 28, 2026, 11:51pm UTC](https://forums.swift.org/t/pitch-init-wrappers/87095/6 "2026-06-28T23:51:03Z")

</div>

> [@vitamin](#):
>
> Under this proposal, we improve that situation:
> 
> ```swift
> struct MyNetworkShim: Injectable {
> let provider: NetProvider
> 
> @Inject
> init(scope: DIScope) {
> // does something like this: provider = scope[\Self.provider]
> }
> }
> 
> ```

What I'm missing with this proposal is why you can't do

```swift
struct MyNetworkShim {
    let provider: NetProvider

    init(scope: DIScope) {
       self.provider = scope.provider
    }
}

```

I'm really struggling to see what benefits the proposal gives over this

---

<div class="post-metadata">

**Author:** ![vitamin](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/vitamin/32/11098_2.png) [@vitamin](https://forums.swift.org/u/vitamin)\
**Post date:** [June 29, 2026, 10:17pm UTC](https://forums.swift.org/t/pitch-init-wrappers/87095/7 "2026-06-29T22:17:05Z")

</div>

> [@0xTim](#):
>
> What I'm missing with this proposal is why you can't do
> 
> ```swift
> struct MyNetworkShim {
> let provider: NetProvider
> 
> init(scope: DIScope) {
> self.provider = scope.provider
> }
> }
> 
> ```
> 
> I'm really struggling to see what benefits the proposal gives over this

Because in `self.provider = scope.provider` you're just getting whatever "NetProvider" happens to be in the scope. It's the same `NetProvider` instance that anything else would get if they reach in that bag and grab its `scope.netprovider`. Perhaps it's a fresh instance generated by a function, but the scope has no control to target the injection specifically to `MyNetworkShim` instead of `StevesNetworkShim` or `BobsNetworkShim` because no keypaths are used in the injection.

Meanwhile, with `provider = scope[\Self.provider]`, a keypath is used to assign the value, so that the `NetProvider` is assigned via a `WritableKeyPath<MyNetworkShim,NetProvider>`, ensuring that the `provider` that `MyNetworkShim` gets is the one that was specifically intended for that specific injection site.

Currently that is only possible with `ReferenceWritableKeyPath` via the `_enclosingInstance` type of property wrapper, so essentially, right now, you simply can't use property wrappers of any kind to set the value of property via `KeyPath`-based resolution. Even if you could, it would require the property you're setting to be a `var` not a `let` stored property, meaning that the property is dynamically generated at least on the first access, not at init time.

Right now the best we can do is something like:

```swift
struct Foo {
    let bar: Int
    let baz: String
    
    init?(_ props: [PartialKeyPath<Self>: Any]) {
        guard let bar = props[\.bar] as? Int, let baz = props[\.baz] as? String 
        else {
            return nil
        }
        
        self.bar = bar
        self.baz = baz
    }
}

```

... but this is obviously just bad. First of all, since we cannot use a `WritableKeyPath` for this purpose, we have no guarantee that \Foo.bar isn't a computed that cannot even be injected. Secondly, it's a failable init with no compiler guarantees. It's just exactly the description of why this pitch is wanted.

What would be so much better is:

```swift
struct Foo {
    let bar: Int
    let baz: String
    
    @Injected
    init(_ scope: Scope) {}
}

```

and then:

```swift
@initWrapper
struct Injected: InitializationWrapper {
    typealias Arguments = Scope

    static func construct<T>(
        _ type: T.Type,
        arguments scope: Scope,
        using context: inout ConstructionContext<T>
    ) {
        for property in context.properties {
            context.initialize(property, with: scope.resolve(property.keyPath))
        }
    }
}

```

Now, whether it's feasible to actually have the compiler make guarantees about whether the scope contains the needed stuff to initialize the type is another question, but the proposal doesn't really depend on that being possible. Just having a way to inject into `let` properties via WritableKeyPaths (or equiv.) at init time with at least the guarantee that these are stored properties, is the main goal here.
