skip to content

Nullable Type Unification

T is a subtype of T?, so mixing nullable and non-null branches produces a nullable least-upper-bound, and Nothing? sits at the bottom as the type of null. This is the machinery behind inference results people find surprising.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

questions

5

In Kotlin's type system, what is the relationship between a type T and its nullable counterpart T?, and why does assigning a String to a String? compile without any conversion?

level: juniorimportance: must knowfreq 70%

answer

  1. T is a subtype of T?
  2. Values of T? = values of T plus null
  3. Widening to nullable is implicit, no conversion
  4. Narrowing T? -> T is rejected (may be null)
  5. Same JVM reference, nullability is compile-time only

basics

~20 s

T is a subtype of T?. Every non-null String is also a valid String?, so a String fits anywhere a String? is expected, just like a child fits where a parent is asked for. No conversion happens.

solid answer

~40 s

Each non-nullable type T is a strict subtype of its nullable form T?. The set of values of T? is exactly the values of T plus the single value null, so T's values are a subset of T?'s values. Because subtype-to-supertype assignment is always allowed (Liskov substitution), `val a: String? = "hi"` compiles with no boxing, wrapping, or conversion — the runtime representation is identical; nullability is a compile-time-only distinction the compiler tracks. The reverse (`val s: String = a`) is rejected because a String? may hold null, which String forbids. This T <: T? edge is the foundational link of the whole nullable lattice and is why widening to nullable is implicit while narrowing requires a smart cast or operator.

code

kotlin · 5 lines
kotlin
val s: String = "hi"
val n: String? = s      // OK, T <: T? widening, no conversion
// val back: String = n  // compile error: String? not a subtype of String
fun len(x: String?) = x?.length
len(s)                   // passing String where String? is expected: fine

go deeper

for a junior

Knows T is a subtype of T? and that a non-null value can be used where nullable is expected without conversion.

for a middle

Explains it via value sets (T's values are a subset of T?'s) and notes the runtime representation is identical for reference types.

for a senior

Frames it as the foundational lattice edge enabling implicit widening and explains why narrowing is rejected via Liskov substitution.

for a principal

Connects the edge to overall lattice design, primitive boxing nuance for Int?, and how the compiler models nullability as a compile-time-only distinction.

## The core relationship: T <: T? For any type `T`, Kotlin defines a **nullable** type `T?`. The key fact is a **subtype relationship**: `T` is a subtype of `T?` (written `T <: T?`). 'Subtype' means: anywhere a value of the supertype is expected, a value of the subtype is accepted. Why is `T <: T?`? Think in terms of the **set of values** each type can hold: - `String` can hold any string: `""`, `"hi"`, etc. - `String?` can hold all of those **plus** the special value `null`. So the values of `String` are a *subset* of the values of `String?`. A type whose values are a subset is a subtype. Hence every non-null `String` already *is* a legal `String?`. ## Why assignment needs no conversion ```kotlin val s: String = "hi" val n: String? = s // OK: widening T -> T?, implicit ``` There is **no boxing, no wrapper object, no runtime conversion**. Nullability is tracked by the compiler at compile time; at the JVM level both `s` and `n` are the same reference. The compiler simply *permits* `n` to also be `null` later. The reverse is rejected: ```kotlin val back: String = n // ERROR: String? is not a subtype of String ``` Because `n` could be `null`, and `String` excludes `null`, you cannot widen-then-narrow implicitly. ## How this scales the lattice This single edge `T <: T?` is repeated for every type, producing pairs like `Int <: Int?`, `List<T> <: List<T>?`, etc. Combined with normal subtyping it forms the **nullable lattice**: e.g. `String <: String?` and `String <: Any` both hold, and `String? <: Any?`. ## Related keywords - The `?` suffix marks a type as nullable. - A *non-null assertion* or smart cast is needed to go the other way (those operators belong to null-safety, not covered here). - `Nothing` sits below everything as the bottom type, including below every `T`. In short: `T? ` is `T` *widened* to also admit `null`, and widening (subtype to supertype) is always implicit.

  • Is there any runtime representation difference between String and String? on the JVM?
    No. For reference types the bytecode is identical; nullability is compile-time metadata. (Primitives like Int may differ because Int? must box to java.lang.Integer, but the subtype relation still holds.)
  • If T <: T?, why can't I assign a String? to a String directly?
    Subtyping only permits widening (subtype -> supertype). String? is the supertype here, so going String? -> String is narrowing and unsafe, since the value might be null.

T? is a box that holds everything T holds plus an 'empty' marker — every full box already qualifies as 'box-or-empty'.

saying these in an interview costs you the question

  • Claiming T? is a subtype of T (the direction is reversed)
  • Saying assigning T to T? performs a wrapping/boxing conversion for reference types
  • Believing String and String? are unrelated, separate types
  • Thinking the reverse assignment T? -> T is allowed without a check

context

open as a page

When a Kotlin `if/else` (or `when`) expression has one branch returning String and another returning null, what is the inferred type of the whole expression, and what general rule produces it?

level: middleimportance: must knowfreq 65%

basics

~10 s

The expression's type is String?. Kotlin takes the common supertype of all branches. One branch is String, the other is null (type Nothing?), and the smallest type covering both is String?.

open as a page

Given `listOf("a", null, "b")` and `listOf(1, "x")`, what element types does Kotlin infer, and how does nullable unification combine with generic variance to produce them?

level: middleimportance: should knowfreq 45%

basics

~20 s

listOf("a", null, "b") is List<String?> because the elements unify to String?. listOf(1, "x") is List<Any> because Int and String share only the supertype Any (or its intersection). The common type of all elements becomes the element type.

open as a page

Explain where Nothing and Nothing? sit in Kotlin's type lattice, and how that bottom position interacts with nullable types during type unification.

level: seniorimportance: should knowfreq 50%

basics

~20 s

Nothing is the bottom type: it is a subtype of every type and has no values. Nothing? is its nullable form, sitting just above Nothing, and is a subtype of every nullable type T?. Together they anchor the bottom of the lattice.

open as a page

How does the nullable lattice handle generic type parameters whose bound is non-null (e.g. `<T : Any>` vs `<T>`), and what happens to nullability when a type parameter T is itself nullable and you write T? — is there a 'double nullable' T??

level: seniorimportance: nice to knowfreq 30%

basics

~10 s

An unbounded T defaults to bound Any?, so T can already be nullable. Writing T? just guarantees nullability. There is no T?? — nullable is idempotent: making an already-nullable type nullable again changes nothing.

open as a page