skip to content

When does Kotlin REQUIRE you to write an explicit type instead of relying on inference?

level: middleimportance: must knowfreq 70%

answer

  1. No initializer → must annotate
  2. lateinit & abstract/interface props need a type
  3. Parameters are never inferred
  4. Recursive function/property → explicit return type
  5. explicitApi() forces public/protected types

basics

~10 s

When there's no value to infer from. If a variable or property has no initializer yet, or the compiler can't work out the type on its own, you must write the type explicitly.

solid answer

~50 s

Inference needs an initializer expression to read; when one isn't available the compiler can't deduce the type, so an annotation is mandatory. The main cases: (1) a property declared without an initializer, including `lateinit var` and abstract/interface properties; (2) a constructor or function parameter — parameters are never inferred; (3) an abstract function or an interface member's signature. There are also cases where inference *would* fail or be unsafe and the compiler forces or strongly prefers an annotation: a recursive function whose body refers to itself before its type is known, and a public/protected API member where Kotlin's *explicit API mode* requires you to spell out the type. By contrast, a simple `lateinit var name: String` needs `: String` purely because there is no initializer to infer from — `lateinit` defers assignment, so nothing exists for the compiler to read.

code

kotlin · 6 lines
kotlin
class Repo {
    lateinit var conn: Connection      // no initializer -> annotate
    fun load(id: Long): User = TODO()  // param + (abstract-style) return annotated
}
fun fib(n: Int): Int =                 // recursive -> explicit return type
    if (n < 2) n else fib(n - 1) + fib(n - 2)

go deeper

for a junior

Knows that a variable with no value needs a written type, e.g. lateinit var x: String.

for a middle

Lists the required-annotation cases: no initializer, parameters, abstract members, recursion.

for a senior

Distinguishes expression-body inference from block bodies and explains recursive-type failure precisely.

for a principal

Connects explicit API mode and public-API stability to inference fragility and team conventions.

## The core rule Type **inference** reads the **initializer**. **No initializer → no inference → explicit annotation required.** Everything below is a variation on that rule plus a couple of special cases. ## 1. Properties/variables without an initializer ```kotlin class Service { lateinit var client: HttpClient // : HttpClient REQUIRED — no initializer val cache: MutableMap<String, Int> // in an interface/abstract — REQUIRED } ``` - **`lateinit var`** defers assignment to later, so there is no right-hand side at declaration — you must annotate. (`lateinit` only works on non-null, non-primitive `var`s.) - An **abstract** property or an **interface** property has no body/initializer in the declaration, so its type must be written. ## 2. Function and constructor parameters Parameters are **never** inferred — each must be annotated: ```kotlin fun area(width: Int, height: Int): Int = width * height // ^ required ^ required ``` A **default value** does not let you drop the type: `fun f(x: Int = 1)` still needs `: Int`. ## 3. Abstract / interface function signatures An abstract function has no body, so its **return type** can't be inferred and must be declared: ```kotlin interface Repo { fun find(id: Long): User } // : User required ``` For a function *with an expression body* the return type **can** be inferred (`fun double(x: Int) = x * 2`), but a block body (`{ ... return ... }`) returning non-Unit requires an explicit return type. ## 4. Recursive declarations If a function's expression body refers to the function itself, the compiler can't infer the return type (it would need the type to compute the type): ```kotlin // fun fib(n: Int) = if (n < 2) n else fib(n-1) + fib(n-2) // ERROR: type checking recursive fun fib(n: Int): Int = if (n < 2) n else fib(n - 1) + fib(n - 2) // OK with : Int ``` The same logic applies to a recursive property initializer that references itself. ## 5. Explicit API mode (libraries) With `explicitApi()` enabled (strict/warning mode in the Gradle/compiler config), every **public and protected** declaration must state its type and visibility. This is a deliberate guard so a library's published API isn't silently changed by a refactor altering an inferred type. ## Why this matters Inferred types on **locals** are great for readability. On **public APIs** an inferred return type is fragile: changing the implementation can change the published type and break callers. That's the reasoning behind explicit API mode and the common style guideline to annotate public members.

  • Why does `lateinit var name` need `: String`?
    Because `lateinit` defers assignment, there is no initializer at declaration, so the compiler has nothing to infer from.
  • Can a function with an expression body skip the return type?
    Usually yes — `fun f() = 42` infers `Int`. But not if it's recursive, abstract, or governed by explicit API mode.
  • Does explicit API mode change runtime behavior?
    No. It's a compile-time lint/strictness setting that forces annotations and visibility on public/protected API members.

saying these in an interview costs you the question

  • Thinking parameters can be inferred from default values
  • Believing inference works without any initializer
  • Not knowing recursive functions need an explicit return type
  • Saying lateinit infers its type
  • Unaware that expression bodies can infer return types but block bodies cannot

context