8

I'm wondering whether it's possible to create a generic class, that accepts an instance of type T, which is limited to (e.g.) an instance of String or an instance of List<String>. See the following pseudo code as an example:

data class Wrapper<T : String | List<String>>(val wrapped: T)

This closely resembles the Union construct, which does not exist in Kotlin (by design). I'm curious whether this can be achieved in a compile time check. The pseudo code class presented above would be used to provide a single object instance of String, and if multiple instances are provided, then it should be a List<String>.

Other options to solve this include:

  1. A separate class for each "variant", i.e.:
    data class Wrapper<T : String>(val wrapped: T)
    data class ListWrapper<T : List<String>>(val wrapped: T)
    
    Downside here obviously is that it's partially duplicated code.
  2. Removing the upper bound, and using an init block to do an instance type check. Downside here is that the check moves to runtime.

Edit: I'm aware of multiple upper bounds using the where keyword, however, that causes a limitation on accepted types which comply to both upper bounds (hence, it's a AND construct)

Robin Trietsch
  • 1,662
  • 2
  • 19
  • 31
  • Not possible unless both classes share an inheritance tree. Note that this looks like a code smell possibly violating single-responsibility principle. – m0skit0 Jan 25 '21 at 09:30
  • Not an answer, but reference: https://discuss.kotlinlang.org/t/union-types/77 – Sid Jan 25 '21 at 09:30
  • As a workaround, you could make the default constructor accept the list, and provide an additional constructor for a unit value. The second constructor would need to turn the unit value into a single element list. It's not exactly what you're after, but depending on your use case it might work. `data class Wrapper(val wrapped: List) { constructor(wrapped: T) : this(listOf(wrapped))}` – Jakub Zalas Jan 25 '21 at 09:41
  • Good one @JakubZalas, however, I'd also like to provide more than one instance of `T`. If I'm interested in providing a single instance of `T`, I could also use `T : String` directly. – Robin Trietsch Jan 25 '21 at 09:44
  • Not sure if I follow, you can still provide more than one instance using the default constructor. What might help is telling us how you intend to instantiate and then use this class. You could also use varargs btw: `data class Wrapper(val wrapped: List) { constructor(vararg wrapped: T) : this(wrapped.toList()) }` – Jakub Zalas Jan 25 '21 at 09:49
  • That could work indeed, it's a nice alternative! – Robin Trietsch Jan 25 '21 at 09:57
  • 1
    I've updated the post according to your usage feedback. – Robin Trietsch Jan 25 '21 at 10:18

1 Answers1

1

While what you're asking for is impossible in Kotlin currenlty, there might be few alternatives.

One option is to provide two constructors for your data class. The default constructor would take the list of elements, while the secondary one would either take a single/variable element(s):

data class Wrapper<T : String>(val wrapped: List<T>) {
    constructor(vararg wrapped: T) : this(wrapped.toList())
}

Uage:

val list = Wrapper(listOf("a", "b", "c"))
val single = Wrapper("a")
val multiple = Wrapper("a", "b", "c")

One drawback is that your wrapped property will always be a list. Although in some use-cases it might be a good thing.

Jakub Zalas
  • 35,761
  • 9
  • 93
  • 125
  • Thanks for the answer. The drawback is not an issue in most cases, though I'd prefer a solution where the `wrapped` property is an instance of `T`, where `T` may be a class, or a list of that class. – Robin Trietsch Jan 25 '21 at 10:49