Kotlin. How to get specific subclass of sealed class?

2k Views Asked by At

I'm using kotlin sealed class. And I need to retrieve specific subclass. My sealed class:

sealed class Course(
    val type: Type
) {
    data class ProgrammingCourse(val name: String, val detail: String) : Course(Type.PROGRAMMING)
    object LanguageCourse: Course(Type.LANGUAGE)
    .....
}

For example I have function which can return Course:

fun getCourse(): Course { 
    if(...)
      return Course.ProgrammingCourse("test", "test")
    else 
     return Course.LanguageCourse
}

In addition, I have a method that can only work with a specific subclass of the Course class. Fox example:

fun workWithCourse(course: Course.ProgrammingCourse) {
   // here some logic
}

And now I'm trying to get the course using the method getCourse(), and then pass it to the method workWithCourse()

fun main() {
  val course = getCourse()
  workWithCourse(course)

}

Error:

Type mismatch.
Required:
Course.ProgrammingCourse
Found:
Course

But I know the course type - Type, parameter that each course has. Can I, knowing this Type, cast the course (which I retrieve from getCourse() method) to a specific subclass ? Is there such a way ? Please help me

P.S.

I don't need type checks like:

if(course is Course.ProgrammingCourse) {
     workWithCourse(course)
}

I need the subclass to be automatically inferred by the Type parameter, if possible.

P.S.2 The need for such a solution is that I have a class that takes a Course, it doesn't know anything about a particular course, at the same time the class takes the Type that I want to use for identification. This class also receives an interface (by DI) for working with courses, a specific implementation of the interface is provided by the dagger(multibinding) by key, where I have the Type as the key. In the same way I want to pass by the same parameter Type specific subclass of my Course to my interface which working with specific courses.

3

There are 3 best solutions below

1
On

In kotlin you can specify the class explicitly with as.

val course = getCourse()
if (type == Type.PROGRAMMING) {
    workWithCourse(course as Course.ProgrammingCourse)
}

*thanks Joffrey for his comment

0
On

What you seem to be asking for is a compile-time guarantee for something that will only be known at runtime. You didn't share the condition used in getCourse(), but in general it could return both types.

Therefore, you need to decide what will happen in both cases - that's not something the compiler can decide for you via any "inference".

  • If you want the program to throw an exception when getCourse() returns something else than a Course.ProgrammingCourse, you can cast the returned value using as:
val course = getCourse() as Course.ProgrammingCourse
workWithCourse(course)
  • If you don't want to crash, but you only want to call workWithCourse in some cases, then you need an if or when statement to express that choice. For instance, to call it only when the value is of type Course.ProgrammingCourse, then you would write the code you already know:
if (course is Course.ProgrammingCourse) {
    workWithCourse(course)
}

Or with a when statement:

val course = getCourse()
when (course) {
    is Course.ProgrammingCourse -> workWithCourse(course)
    is Course.LanguageCourse -> TODO("do something with the other value")
}

The when is better IMO because it forces you (or other devs in the team) to take a look at this when whenever you (or they) add a new subclass of the sealed class. It's easy to forget with an if.

You can also decide to not test the actual type, and focus on the type property like in @grigory-panov's answer, but that is brittle because it relies on an implicit relationship between the type property and the actual type of the value:

val course = getCourse()
if (type == Type.PROGRAMMING) {
    workWithCourse(course as Course.ProgrammingCourse)
}

The main point of using sealed classes is so you can use their actual type instead of a manually managed type property + casts. So I'd say use only is X and don't set a type property at all. Using a sealed class allows Kotlin to type-check a bunch of things, it's more powerful than using such a property.

0
On

No, there is no way for automatic inference to the best of my knowledge.

You returned a Course, and that's what you have. Being sealed here does not matter at all. Generally what you do here is use the when expression if you want to statically do different things depending on the type, but if it's just one type (ProgrammingCourse) that can be passed to workWithCourse, then an if is probably right, with dispatch using as.

That said, this looks like counter-productive design. If you can only work with one course, why do they even share a top level interface? The way the code is written implies working is a function that can take any course, or should be a method member. Anything else is very confusing. Perhaps workWithCourse should take a Course and use the when expression to dispatch it appropriately?