KeyPath collection issue where `Value` is an existential

I’m unable to solve this problem. I need a way to loop over a collection of key-path values but the compiler does not let me when the Value is an existentential or a protocol:

protocol A {}
protocol B {}

extension Int : A, B {}
extension String : A, B {}

struct Foo {
  let x: Int = 42
  let y: String = "swift"

let test: [KeyPath<Foo, A & B>] = [
  \.x, // Key path value type 'Int' cannot be converted to contextual type 'A & B'

I also tried:

let test: [AnyKeyPath] = ....
let test2 = test.compactMap { $0 as? KeyPath<Foo, A & B> }

But test2 won’t contain any values.

Key paths are not covariant like this. You can have a collection of [PartialKeyPath<Foo>], though.

Is there no real way to workaround this issue? I really need it to be KeyPath<Foo, A & B>.

You can apply a PartialKeyPath to a base value and get a value of Any, and cast that to A & B.

If you really need this, you can define computed properties on the type itself:

extension Foo {
  fileprivate var x_AB: A & B { return self.x }
  fileprivate var y_AB: A & B { return self.y }

let test: [KeyPath<Foo, A & B>] = [\.x_AB, \.y_AB]

Or you can use closures instead of key paths:

let test: [(Foo) -> A & B] = [
  { $0.x },
  { $0.y }

(note: I didn’t test either of these code samples, so they may have small bugs)


That, or provide an extension property on the protocol composition:

extension A where Self: B {
  var asAB: A & B { return self }

which would let you use \.x.asAB and \.y.asAB. Note that these would no longer be equal to \.x and \.y, if that’s important.


I’d like to try that but I’m already hitting the next wall. One of the path that I need has a subscript which uses an enum as a key.

\BluetoothCharacteristic.cameraSetting[.iso] // Type of expression is ambiguous without more context

What I can do is this, but it’s really verbose and tedious:

let a = \BluetoothCharacteristic.cameraSetting
let b: PartialKeyPath<BluetoothCharacteristic> = a.appending(path: \.[.iso])

Is there a better way of expressing it one line?

That looks like a type-checker bug. Does it work if you explicitly name EnumName.iso?

No, but it seems I can silent the error by adding as PartialKeyPath<BluetoothCharacteristic> at the end. :face_with_head_bandage:

Weird. Does cameraSetting by chance come from a superclass of BluetoothCharacteristic? @rudkx fixed a bug related to that recently. If not, this is definitely worth a bug of its own.

No classes are involved here, but the types are a little complex:

extension CameraSettingCharacteristic {
  public struct Path {
    public subscript (_ kind: Kind) -> ORWCharacteristicContainer<
      CameraSettingCharacteristic, UInt8, Data
    > { ... }

public struct BluetoothCharacteristic {
  public let cameraSetting = CameraSettingCharacteristic.Path()

If I can shrink it down to something small that reproduces the issue, I’ll file an issue tomorrow.

@Joe_Groff other then that, wouldn’t it make sense to make Key Paths also covariant like collections?

This worked for me as a workaround. That way the type information I need won’t be lost, thank youl