Question

Kotlin List Casting: Safely Converting List<*> to List<Waypoint> Without Unchecked Cast Warnings

kotlinlistgenericscasting

Question

I want to write a Kotlin function that returns all items in a list except the first and last items, treating those middle items as via points. The function receives a generic List<*> as input, and it should only return a result if every element in the list is of type Waypoint.

Here is the code:

fun getViaPoints(list: List<*>): List<Waypoint>? {
    list.forEach { if (it !is Waypoint) return null }

    val waypointList = list as? List<Waypoint> ?: return null

    return waypointList.filter {
        waypointList.indexOf(it) != 0 && waypointList.indexOf(it) != waypointList.lastIndex
    }
}

When casting List<*> to List<Waypoint>, Kotlin shows this warning:

Unchecked cast: kotlin.collections.List<*> to kotlin.collections.List<Waypoint>

What is the correct way to implement this function without getting that warning?

Short Answer

By the end of this page, you will understand why Kotlin warns about unchecked casts with generic collections, how type erasure affects List<*>, and how to safely convert or validate a list of unknown elements into a List<Waypoint>. You will also learn a cleaner way to return all middle elements without using inefficient index lookups.

Concept

Kotlin allows you to check the runtime type of each element in a collection, but not the full generic type of the collection itself.

For example, Kotlin can tell whether a value is a Waypoint:

if (item is Waypoint) { ... }

But at runtime, Kotlin cannot fully verify whether a list is really a List<Waypoint> because generic type information is erased. This is called type erasure.

That is why this cast produces a warning:

list as? List<Waypoint>

Kotlin is saying:

  • "I can cast this to List,"
  • "but I cannot prove at runtime that its elements are really Waypoint values."

Why this matters

If you ignore unchecked cast warnings, your code may compile but fail later when you read values as the wrong type.

The safe way is usually one of these:

  • validate every element before treating the list as typed
  • build a new typed list using filterIsInstance<T>()
  • avoid the cast entirely by designing the function to accept List<Waypoint> if possible

In your specific case, you do not need to cast the original list at all. Once you know every element is a Waypoint, you can create a proper typed list safely.

Mental Model

Think of List<*> as a box of items with the label "some kind of list".

You can open the box and inspect each item one by one:

  • this item is a Waypoint
  • this item is a String
  • this item is null

But the box itself does not carry a reliable runtime label saying "this is definitely a List".

So Kotlin lets you inspect the contents, but it warns you when you try to relabel the entire box based only on a generic cast.

A safer approach is:

  1. inspect the contents
  2. create a new box containing only the validated Waypoint items
  3. work with that new typed list
Take Quiz

Syntax and Examples

In Kotlin, these are the most common approaches.

1. Validate all elements, then build a typed list

data class Waypoint(val name: String)

fun getViaPoints(list: List<*>): List<Waypoint>? {
    if (!list.all { it is Waypoint }) return null

    val waypoints = list.map { it as Waypoint }
    return waypoints.drop(1).dropLast(1)
}

Why this works

  • list.all { it is Waypoint } checks every element first
  • after that, map { it as Waypoint } is logically safe
  • drop(1).dropLast(1) removes the first and last elements cleanly

This approach avoids casting the entire list to List<Waypoint>.

2. Use filterIsInstance when partial matches are acceptable

val waypoints = list.filterIsInstance<Waypoint>()

This returns only the elements that are Waypoint objects.

However, this is not correct for your requirement if you want to return null when even one element is not a Waypoint.

Example:

val list: List<*> = listOf(Waypoint("A"), "not a waypoint", Waypoint("B"))
val waypoints = list.filterIsInstance<Waypoint>()
// Result: [Waypoint("A"), Waypoint("B")]

That silently removes invalid items instead of rejecting the whole list.

3. Best version for your use case

data class Waypoint(val name: String)

fun getViaPoints(list: List<*>): List<Waypoint>? {
    if (!list.all { it is Waypoint }) return null

    val waypoints = list.map { it as Waypoint }
    return waypoints.drop(1).dropLast(1)
}

Example

val points: List<*> = listOf(
    Waypoint("Start"),
    Waypoint("Mid 1"),
    Waypoint("Mid 2"),
    Waypoint("End")
)

println(getViaPoints(points))

Result:

[Waypoint(name=Mid 1), Waypoint(name=Mid 2)]

Even better API design

If callers are already supposed to provide waypoints, the best signature is often:

fun getViaPoints(list: List<Waypoint>): List<Waypoint> =
    list.drop(1).dropLast(1)

This is simpler, safer, and avoids runtime checks entirely.

Step by Step Execution

Consider this example:

data class Waypoint(val name: String)

fun getViaPoints(list: List<*>): List<Waypoint>? {
    if (!list.all { it is Waypoint }) return null

    val waypoints = list.map { it as Waypoint }
    return waypoints.drop(1).dropLast(1)
}

Now run it with:

val input = listOf(
    Waypoint("Start"),
    Waypoint("Via 1"),
    Waypoint("Via 2"),
    Waypoint("End")
)

Step 1: Check all elements

list.all { it is Waypoint }

Kotlin checks each element:

  • Waypoint("Start") -> true
  • Waypoint("Via 1") -> true
  • Waypoint("Via 2") -> true
  • Waypoint("End") -> true

Since all are true, execution continues.

Step 2: Convert to a typed list

val waypoints = list.map { it as Waypoint }

Each element is converted to Waypoint, producing:

[Start, Via 1, Via 2, End]

as a List<Waypoint>.

Step 3: Remove first item

waypoints.drop(1)

Result:

[Via 1, Via 2, End]

Step 4: Remove last item

.dropLast(1)

Result:

[Via 1, Via 2]

If the input contains a wrong type

val badInput = listOf(
    Waypoint("Start"),
    "wrong",
    Waypoint("End")
)

Then:

list.all { it is Waypoint }

becomes:

  • Waypoint("Start") -> true
  • "wrong" -> false

The function immediately returns:

null

Real World Use Cases

This pattern appears often when working with dynamic or loosely typed data.

1. Parsing external API data

An API may return mixed or uncertain values. Before treating a list as a typed model list, you validate all elements.

2. Processing data from generic libraries

Some frameworks return List<*>, Any, or deserialized objects. You often need to verify the contents before using them.

3. Route planning and map applications

Your exact example fits here:

  • first item = start point
  • last item = destination
  • middle items = via points

4. Reading from configuration or JSON

If raw data is converted into generic structures first, you may need to confirm every item is the expected type.

5. Defensive programming in shared code

Library code often validates inputs carefully instead of assuming callers passed the right generic type.

Take Quiz

Real Codebase Usage

In real projects, developers usually avoid unchecked casts where possible and prefer one of these patterns.

Guard clause for validation

A guard clause exits early when data is invalid:

if (!list.all { it is Waypoint }) return null

This keeps the rest of the function clean.

Transform after validation

After checking the elements, convert them into a typed list:

val waypoints = list.map { it as Waypoint }

This is much safer than casting the whole list.

Use collection helpers instead of manual index logic

Instead of:

waypointList.filter {
    waypointList.indexOf(it) != 0 && waypointList.indexOf(it) != waypointList.lastIndex
}

developers usually write:

waypoints.drop(1).dropLast(1)

This is clearer and avoids repeated searches.

Prefer stronger function signatures

If the function should only accept waypoint lists, declare that directly:

fun getViaPoints(list: List<Waypoint>): List<Waypoint>

This moves type checking to the caller and makes your code easier to reason about.

Error handling choice: null vs empty list vs exception

In production code, teams choose one behavior deliberately:

  • return null when the input is invalid
  • return an empty list when there are no via points
  • throw an exception when invalid input is a programmer error

Your current design uses null for invalid input, which is reasonable if the input type is genuinely unknown.

Common Mistakes

1. Casting the whole list directly

Broken example:

val waypoints = list as List<Waypoint>

Why it is a problem:

  • Kotlin cannot verify the generic element type at runtime
  • this produces an unchecked cast warning
  • it may hide bugs

Better:

if (!list.all { it is Waypoint }) return null
val waypoints = list.map { it as Waypoint }

2. Using filterIsInstance when you need full validation

Broken for this requirement:

val waypoints = list.filterIsInstance<Waypoint>()

Why it is a problem:

  • it removes invalid items instead of rejecting the input
  • you may get a partial result from bad data

Use it only if partial filtering is what you want.

3. Using indexOf inside filter

Broken style:

return waypointList.filter {
    waypointList.indexOf(it) != 0 && waypointList.indexOf(it) != waypointList.lastIndex
}

Problems:

  • inefficient because indexOf searches repeatedly
  • incorrect when duplicate elements exist

Example:

val list = listOf("A", "B", "B", "C")

For the second "B", indexOf("B") still returns the first "B" position.

Better:

return waypoints.drop(1).dropLast(1)

4. Forgetting about empty or short lists

A list with fewer than 3 elements has no middle items.

Good news: this works safely:

listOf(1).drop(1).dropLast(1)

It just returns an empty list.

5. Confusing List<*> with List<Any>

These are not the same.

  • List<*> means a list of unknown element type
  • List<Any> means a list guaranteed to contain non-null Any values

List<*> is more flexible, but it gives you less type information.

Comparisons

ApproachWhat it doesGood for this problem?Notes
list as List<Waypoint>Casts the whole listNoUnsafe and gives unchecked cast warning
list as? List<Waypoint>Safe cast syntaxNoStill unchecked for generic element type
list.all { it is Waypoint } + mapValidates every element, then convertsYesClear and safe
list.filterIsInstance<Waypoint>()Keeps only matching elementsNot for strict validationGood only if partial filtering is acceptable
Function parameter List<Waypoint>Requires typed input from the startBest if possibleNo runtime validation needed

drop/dropLast vs filter with indexes

TechniqueResultRecommended?Reason
drop(1).dropLast(1)Removes first and last itemYesSimple and readable
filterIndexedFilters by indexYesUseful when conditions depend on index
filter { indexOf(it) ... }Looks up indexes by valueNoSlow and wrong with duplicates

Better alternative using filterIndexed

fun getViaPoints(list: List<*>): List<Waypoint>? {
    if (!list.all { it is Waypoint }) return null

    val waypoints = list.map { it as Waypoint }
    return waypoints.filterIndexed { index, _ ->
        index != 0 && index != waypoints.lastIndex
    }
}

This is valid, but drop(1).dropLast(1) is usually shorter for this specific task.

Take Quiz

Cheat Sheet

fun getViaPoints(list: List<*>): List<Waypoint>? {
    if (!list.all { it is Waypoint }) return null
    val waypoints = list.map { it as Waypoint }
    return waypoints.drop(1).dropLast(1)
}

Key rules

  • List<*> means the element type is unknown
  • List<*> cannot be safely cast to List<Waypoint> without an unchecked cast warning
  • Kotlin generic type arguments are erased at runtime
  • Check elements individually with is
  • Build a typed list with map after validation
  • Use drop(1).dropLast(1) to get middle elements

Useful patterns

Validate all elements:

if (!list.all { it is Waypoint }) return null

Convert after validation:

val waypoints = list.map { it as Waypoint }

Get middle items:

waypoints.drop(1).dropLast(1)

Keep only matching items:

list.filterIsInstance<Waypoint>()

Edge cases

  • empty list -> returns empty list after drop operations
  • one or two items -> no via points, so result is empty list
  • mixed types -> return null if strict validation is required
  • duplicate values -> avoid indexOf-based filtering

FAQ

Why does Kotlin warn about casting List<*> to List<Waypoint>?

Because Kotlin cannot fully check generic element types at runtime due to type erasure.

Is as? List<Waypoint> safer than as List<Waypoint>?

It avoids throwing if the outer type is wrong, but it still cannot verify the generic element type, so the cast remains unchecked.

Can I use filterIsInstance<Waypoint>() here?

Yes, but only if you want to keep matching items and ignore the rest. It is not correct if invalid input should make the function return null.

What is the cleanest way to get all middle elements in Kotlin?

Usually:

list.drop(1).dropLast(1)

Should this function accept List<*> or List<Waypoint>?

Use List<Waypoint> if possible. Use List<*> only when the input truly comes from an unknown or generic source.

What happens if the list has fewer than three elements?

There are no middle elements, so drop(1).dropLast(1) returns an empty list.

Why is indexOf a bad choice here?

It repeatedly searches the list and can give wrong results when duplicate elements exist.

Take Quiz

Mini Project

Description

Build a small route utility for a mapping app. The input may come from generic data, so your code must validate that all items are Waypoint objects before processing them. This demonstrates safe handling of List<*>, avoiding unchecked casts, and extracting only the middle route points.

Goal

Create a function that accepts a generic list, returns only the middle Waypoint items, and returns null if any element is not a Waypoint.

Requirements

  1. Define a Waypoint data class.
  2. Write a function that accepts List<*>.
  3. Return null if any item is not a Waypoint.
  4. Return all items except the first and last when the input is valid.
  5. Demonstrate the function with both valid and invalid input.
Take Quiz

Keep learning

Related questions

View more