A teammate writes a `fun List<T>.secondOrNull()` extension and is surprised that calling it through a `Collection<T>`-typed reference doesn't compile, while a member would. Explain the underlying resolution rule and why static typing causes these surprises with supertypes.
answer
- Extension candidacy = declared type
- Supertype reference hides subtype extensions
- No runtime narrowing for extensions
- Smart-cast/as to expose List extension
- Members are inherited; extensions aren't
basics
~10 sExtensions are picked by the declared type. A List extension isn't visible on a Collection-typed variable, because the compiler only sees Collection. Members would be inherited and resolved on the object instead.
solid answer
~50 sExtension resolution is purely static: the compiler searches for an applicable extension using the **declared type** of the receiver expression and its supertypes' extensions in scope. An extension defined on `List<T>` is applicable only when the compile-time type is `List` (or a subtype). If the reference is declared as `Collection<T>`, the `List` extension is simply not a candidate, so the call fails to compile even though the runtime object is a `List`. A real member declared on `Collection` (or inherited) would be available and, if open, dispatched virtually on the runtime object. The fix is to either declare the receiver more specifically, smart-cast/`as` to `List`, or define the extension on the broader type you actually call it on. This is the same static-resolution principle behind the `Animal`/`Dog` surprise — extensions follow the type you wrote, not the object you hold.
code
kotlin · 9 linesfun <T> List<T>.secondOrNull(): T? = getOrNull(1)
fun demo(items: Collection<Int>) {
// items.secondOrNull() // compile error: receiver is Collection
if (items is List<Int>) {
items.secondOrNull() // OK: smart-cast makes declared type List
}
(items as List<Int>).secondOrNull() // OK: explicit cast
}go deeper
Recognizes that the call fails and that the variable's type matters, even if fuzzy on the fix.
Explains candidacy is by declared type and proposes smart-cast or explicit cast correctly.
Connects it to the broader static-resolution rule, contrasts member inheritance, and reasons about API receiver-type design.
Guides API design so receiver types match intended call sites, avoiding hidden subtype-only extension cliffs across a codebase.
## Why this compiles differently than a member Extensions are resolved at **compile time** against the **declared (static) type** of the receiver expression. The compiler asks: "Given this expression's declared type, is there an in-scope extension whose receiver type this expression is assignable to?" If the declared type is a **supertype** that the extension's receiver isn't compatible with, the extension is **not a candidate** — and you get a compile error, not a fallback. ```kotlin fun <T> List<T>.secondOrNull(): T? = getOrNull(1) fun demo(items: Collection<Int>) { // items.secondOrNull() // does NOT compile: receiver type is Collection, not List } ``` The runtime object might genuinely be an `ArrayList`, but the compiler only sees `Collection<Int>`, and there is no `Collection` extension named `secondOrNull`. ## Contrast with a member If `secondOrNull` were a **member** of `Collection` (or inherited), it would resolve through the object: available on any `Collection` reference, and dispatched virtually if `open`. Members are part of the type and its subtypes; extensions are external and bound to the **exact declared receiver type** in scope. ## The general principle - Extension applicability is decided by the **static type**, including its supertypes' extensions, but **not** by narrowing to the runtime type. - A supertype-typed receiver hides subtype-only extensions. A subtype-typed receiver can see both its own and inherited supertype extensions. ## Fixes ```kotlin fun demo(items: Collection<Int>) { if (items is List<Int>) { items.secondOrNull() // smart-cast: declared type is now List in this block } (items as List<Int>).secondOrNull() // explicit cast } ``` Or widen the extension's receiver to `Collection<T>` if the operation is meaningful there (note: `Collection` has no positional `get`, so you'd iterate). Smart-cast works because after `is List`, the compile-time type **inside the block** becomes `List`, making the extension a candidate. ## Takeaway Extensions never "discover" subtype capabilities through a supertype reference. Choose the receiver type deliberately, or cast to expose subtype-only extensions.
- Why does `if (items is List) { items.secondOrNull() }` compile while the bare call doesn't?Smart-cast changes the compile-time type inside the block to `List`, making the `List` extension a valid candidate.
- If you had declared `secondOrNull` as a member of `Collection`, would the `Collection`-typed call compile?Yes — members are part of the type and inherited by all subtypes, so the call resolves on any `Collection` reference.
saying these in an interview costs you the question
- Expecting the compiler to narrow to the runtime List automatically
- Saying extensions are inherited like members
- Thinking a supertype reference can still see subtype-only extensions
- Not knowing smart-cast changes the static receiver type
- Confusing compile error here with a runtime ClassCastException