Generic property wrappers and inference

I'm trying to create a property wrapper with a few a few different initializers depending on generics, but it seems that generic constraints break delayed initialization.

@propertyWrapper
struct PW<A, B> {
  var wrappedValue: A

  init(wrappedValue: A) where A == B {
    self.wrappedValue = wrappedValue
  }
}

struct X {
  @PW var x: Int // πŸ›‘ Generic parameter 'B' could not be inferred
}

If I remove the B generic from PW entirely, or if I eagerly assign a value:

struct X {
  @PW var x = 1 // PW<Int, Int>
}

The inference works just fine.

Property wrapper initialization is certainly nuanced, so I might be missing what part of the feature prevents this from working, but should it work?

2 Likes

Random old thread revival, I just stumbled onto this while searching for something else.

It makes sense to me that this happens, because without supplying a value you’re free to use any init of the property wrapper itself later, like:

struct X {
  @PW var x: Int

  init() {
    _x = .init(someOtherInitParameter: ...)
  }
}

And that init might have other, or no, constraints among the type parameters. With the inline value the compiler synthesizes the init(wrappedValue:) which is enough to infer the constraint. If you "init" the wrapper with a wrapped value out of line it will also synthesize the init(wrappedValue:) but it will do so where you assign the value. This:

struct X {
  @PW var x: Int

  init() {
    x = 1
  }
}

becomes:

struct X {
  @PW var x: Int

  init() {
    _x = .init(wrappedValue: 1)
  }
}

But the compiler can't use the out of line initializer to infer type parameters so that doesn't compile either.

What you could do instead is use a typealias for the constrained version, like this:

@propertyWrapper
struct GenericPW<A, B> {
  var wrappedValue: A

  init(wrappedValue: A) where A == B {
    self.wrappedValue = wrappedValue
  }
}

typealias PW<A> = GenericPW<A, A>

struct X {
    @PW var x: Int
}

What would be neat is if you could conditionally make a generic type a property wrapper (i.e. something like extension PW: @propertyWrapper where A == B, and then have the compiler infer the constraint from the fact it's used as a wrapper.

My guess here is this is one reason why SwiftData @Query "property wrappers" actually look like they are macros. It's possible there's no clean way to do this directly with a conventional property wrapper.