106

I'm trying to test for a generic type in Kotlin.

if (value is Map<String, Any>) { ... }

But the compiler complains with

Cannot check for instance of erased type: jet.Map

The check with a normal type works well.

if (value is String) { ... }

Kotlin 0.4.68 is used.

What am I missing here?

Willi Mentzel
  • 27,862
  • 20
  • 113
  • 121
phil
  • 1,377
  • 2
  • 9
  • 14

6 Answers6

112

The problem is that type arguments are erased, so you can't check against the full type Map, because at runtime there's no information about those String and Any.

To work around this, use wildcards:

if (value is Map<*, *>) {...}
Andrey Breslav
  • 24,795
  • 10
  • 66
  • 61
  • Great! That perfectly works! I just got confused by the example in the documentation: http://confluence.jetbrains.net/display/Kotlin/Type+casts – phil Nov 01 '12 at 17:15
  • 75
    What if you actually want to check that something is a `Collection` to make it auto cast? – beruic Aug 15 '18 at 10:13
  • I have snippet like this `if (it.getSerializable(ARG_PARAMS) is HashMap<*, *>) {it.getSerializable(ARG_PARAMS) as HashMap} else null`. So basically it's gonna try to cast `HashMap` to `HashMap` if I'm checking against generic type. Am I missing something? – Farid Aug 15 '19 at 14:12
  • @FARID Yes, it will, and this kind of cast is not safe – Andrey Breslav Aug 20 '19 at 18:36
30

I think this is more appropriate way

inline fun <reified T> tryCast(instance: Any?, block: T.() -> Unit) {
    if (instance is T) {
        block(instance)
    }
}

Usage

// myVar is nullable
tryCast<MyType>(myVar) {
    // todo with this e.g.
    this.canDoSomething()
}

Another shorter approach

inline fun <reified T> Any?.tryCast(block: T.() -> Unit) {
    if (this is T) {
        block()
    }
}

Usage

// myVar is nullable
myVar.tryCast<MyType> {
    // todo with this e.g.
    this.canDoSomething()
}
Vlad
  • 7,997
  • 3
  • 56
  • 43
  • 2
    Why something like this is not available directly in kotlin stdlib :-( – ATom Aug 05 '20 at 19:36
  • Isn't `something as? String` the same? Note the question mark after `as`? – Dalibor Filus Nov 09 '20 at 19:38
  • 1
    @DaliborFilus nope. This is about Generics and erased types during runtime. If you don´t have to deal with Generics, you can just use `as?`, correct. – stk Nov 17 '20 at 15:44
21

JVM removes the generic type information. But Kotlin has reified generics. If you have a generic type T, you can mark type parameter T of an inline function as reified so it will be able to check it at runtime.

So you can do:

inline fun <reified T> checkType(obj: Object, contract: T) {
  if (obj is T) {
    // object implements the contract type T
  }
}
menno
  • 279
  • 3
  • 5
1

Here's what I use:

// Use this value if it is of type T, or else use defaultValue
inline fun <reified T> Any?.useIfTypeOrDefault(defaultValue: T) = 
    if (this is T) this else defaultValue

Usage (kotest):

val map = mapOf("foo" to listOf("cheese"), "bar" to 666)

map["foo"].useIfTypeOrDefault<List<String>>(emptyList()).firstOrNull() shouldBe "cheese"
map["bar"].useIfTypeOrDefault<List<String>>(emptyList()).firstOrNull() shouldBe null

map["foo"].useIfTypeOrDefault<Number>(-1) shouldBe -1
map["bar"].useIfTypeOrDefault<Number>(-1) shouldBe 666
neu242
  • 15,796
  • 20
  • 79
  • 114
0

I'm gonna give a workaround solution but I think its clean, kind of

try{
  (value as Map<String,Any>?)?.let { castedValue ->
     doYourStuffHere() //using castedValue
  }
}catch(e: Exception){
  valueIsNotOfType() //Map<String,Any>
}
Antoine El Murr
  • 317
  • 1
  • 13
-1

I have tried the solution above with tryCast<Array<String?>> and, I guess, in my specific task in listing with many castings involved it was no so great idea, because it was slowing the performance drastically.

This is the solution I did finally - manually check the entries and call methods, like this:

 fun foo() {
    val map: Map<String?, Any?> = mapOf()
    map.forEach { entry ->
        when (entry.value) {
            is String -> {
                doSomeWork(entry.key, entry.value as String)
            }
            is Array<*> -> {
                doSomeWork(entry.key, (entry.value as? Array<*>)?.map {
                    if (it is String) {
                        it
                    } else null
                }?.toList())
            }
        }
    }
}


private fun doSomeWork(key: String?, value: String) {

}
private fun doSomeWork(key: String?, values: List<String?>?) {

}