ExpressibleByStringLiteral with optionals

Consider the following enum

enum Text: Equatable {
    case plain(String)
    case attributed(NSAttributedString)
}

I've made it conform to ExpressibleByStringLiteral

extension Text: ExpressibleByStringLiteral {
    public typealias StringLiteralType = String
    
    public init(stringLiteral value: StringLiteralType) {
        self = .plain(value)
    }
}

With all this, I can do the following, just like I would expect:

let text: Text = "Hello" // .plain("Hello")
let text2: Text? = "Hello" // .plain("Hello")

But I get compiler errors for the following:

let nilString: String? = nil
let text3: Text? = nilString // Cannot convert value of type 'String?' to expected argument type 'Text?'

func foo(text: Text?) { /** foo **/ }
foo(text: "Hello") // Cannot convert value of type 'String' to expected argument type 'Text?'

func bar(text: Text?) { /** bar **/ }
bar(text: nilString) // Cannot convert value of type 'String?' to expected argument type 'Text?'

How can I get those to work ?

I've also tried to extend Optional: ExpressibleByStringLiteral where Wrapped: ExpressibleByStringLiteral, but that didn't help.

String literals are different from Strings. You could treat things like "Hello" as string literals, which are not bound to a specific type yet. If no other information is provide, the compiler will just assume that it is String.

bar(text: nilString) won't work because you're converting from String to Text?, not string literal to Text.

This works fine on my iOS Playground 3.3.1.

1 Like

I see. is there a way to get some sort of ExpressibleByString instead ? :grimacing:

(Un)fortunately no. Conversion between concrete types (String -> Text) incurs some overhead, and Swift makes sure that you notice them. In this scenario, you usually use an initializer. If Text.StringLiteral is String, you can probably just use:

bar(text: .init(stringLiteral: nilString))

Though I'd make a dedicate conversion initializer since value preserving type conversion omits the first label:

extension Text {
  init(_ string: String) { ... }
}

ExpressibleByStringLiteral is usually for types that you know the value at compile time. If it's a type that just work with dynamic strings, I'd just have a type conversion initializer.

5 Likes

Thanks for the detailed explanation. I guess I'll just simply call .plain(someString) then, no need to implement that additional init :slight_smile:

3 Likes