Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Kotlin vs. Dart: Type Systems, Null Safety, and Type Inference

Hello! This course starts with Kotlin, then uses it to explore algorithms, backend development, and larger software systems. In this first lesson, you’ll use Dart as a reference point: both languages make types non-nullable by default, but their syntax and some important type-system rules differ.

By the end, you should be able to read a Kotlin declaration, decide where null can flow, choose an appropriate null-handling operator, and predict the types Kotlin infers for ordinary local values.


One familiar rule, two type systems

Kotlin and null-safe Dart share a starting rule: String cannot hold null; String? can. The question mark changes the type, not the variable’s mutability.

val title: String = "Ready"
val subtitle: String? = null
// val invalid: String = null   // Compile error
final String title = 'Ready';
final String? subtitle = null;
// final String invalid = null; // Compile error

Kotlin’s val is a read-only reference; var permits reassignment. Dart’s final and var play roughly the same roles here. In either language, a nullable value cannot simply be passed where a non-nullable value is required.

Read the Kotlin documentation’s basic declarations and its example of a null check. Pay particular attention to the compiler’s reason for rejecting direct access to b.length.

Null safety | Kotlin Documentation

Read “Null safety” from the Kotlin documentation to establish what String? means and how a check can make a nullable value usable.

In “Nullable types and non-nullable types,” begin with the type distinction and stop after the explanation following the rejected direct length access. Then, in “Check for null with the if conditional,” read the null-check examples, through the note about the value remaining unchanged between check and use. Focus on when the compiler can treat String? as String.

The Dart rule is similar enough to transfer, but it helps to picture what the types mean. In the diagram below, a type lower in the hierarchy can be used where an appropriate type above it is expected. Object? admits any Dart value, including null; Object does not. Never has no possible values and is at the bottom.

Dart’s null-safe type hierarchy places nullable `Object?` above both non-null `Object` and `Null`, and places the uninhabited `Never` type below the other types shown. Only selected branches are drawn.

Kotlin has useful counterparts: Any? can hold any value including null, Any excludes null, and Nothing has no values. These are not reasons to use the broadest type everywhere. A String? tells a caller much more than an Any?.

Understanding null safety

Read the relevant parts of Dart’s “Understanding null safety” to refresh the Dart side of the comparison.

In “Nullability in the type system” → “Non-nullable and nullable types,” read the hierarchy explanation, stopping after the makeCoffee example and the paragraph introducing the ? suffix. Then jump to “Flow analysis” → “Type promotion on null checks.” Read the promotion example. Look for the same two ideas you just saw in Kotlin: nullable values need protection, and a proven null check can provide it.

One distinction matters when you choose a broad type. Dart’s dynamic opts out of many static checks. Kotlin’s Any? does not: it is a broad type, but the compiler still controls which operations are valid on it. Also, Kotlin does not automatically convert one numeric type to another merely because a conversion seems safe: an Int is not automatically a Long; use toLong() when that conversion is needed.


What a null check proves

A null check changes what the compiler knows at that point in the program. Kotlin calls the resulting refinement a smart cast; Dart calls the comparable behavior type promotion.

fun labelLength(label: String?): Int {
    if (label == null) return 0
    return label.length
}
int labelLength(String? label) {
  if (label == null) return 0;
  return label.length;
}

In both functions, execution reaches label.length only if label was non-null. Notice that the declared parameter type has not changed: callers may still pass null. The compiler has established a narrower type for a particular use inside the function.

That proof must remain valid. If a value can change between checking it and using it—for example, through mutable state the compiler cannot reliably track—promotion or smart casting may not apply. A practical response is to read the value into a stable local variable, check that local, and use it. Avoid replacing an uncertain proof with a forceful assertion unless you genuinely need one.


Null-handling operators: translate the behavior, not just the symbol

Watch these portions of the Kotlin video for the three operators you will encounter most often. The examples print results, but focus on what each expression evaluates to.

Kotlin Null Safe Operators. Safe Call, with Let, Elvis & Non-null operator. Kotlin Tutorials #12.1

Watch “Kotlin Null Safe Operators” from Smartherd to see safe calls, the Elvis operator, and non-null assertions used in code.

Watch safe calls for how ?. skips an access and produces null. Then watch Elvis defaults for the fallback value supplied by ?:. Finish with non-null assertions to see why !! can compile yet fail at runtime.

Suppose label has type String?. Here is the direct translation of a common Dart expression:

final int length = label?.length ?? 0;
val length: Int = label?.length ?: 0

The safe call label?.length has type Int? in Kotlin: it produces the length when there is a string and null otherwise. The Elvis operator ?: supplies 0 only when its left-hand expression is null. Thus length is an Int, not an Int?. Dart’s corresponding fallback operator is ??, not ?:.

Kotlin can also use an early exit as the Elvis fallback:

fun requireLength(label: String?): Int {
    val present = label ?: return 0
    return present.length
}

After that assignment, present is non-nullable. This is useful when absence has a clear outcome, rather than an arbitrary default value.

If you want to reject null with an exception instead of handling it, Kotlin uses !! where Dart uses !:

val length = label!!.length
final length = label!.length;

Neither assertion makes the incoming value safe. Each performs a runtime check and fails if the value is null. Prefer a check, safe call, or meaningful fallback when those express the intended behavior.

There is one subtler difference in chains. Kotlin requires a safe call at each access whose receiver could be null:

val text: String? = null
val representation: String? = text?.length?.toString()

In current null-safe Dart, a null-aware access short-circuits the remainder of that access chain when its receiver is null:

String? text;
String? representation = text?.length.toString();

Do not copy a Dart chain character for character into Kotlin. Check the type produced by each access and use ?. again where needed. The Dart reading below explains its chain rule and the runtime cost of an assertion.

Understanding null safety

Return to Dart’s “Understanding null safety” for the details most likely to cause mistranslations.

In “Working with nullable types” → “Smarter null-aware methods,” skip the historical examples above the revised chain rule. Start at the chain rule and stop after the explanation of what a second ?. signifies. Then, in “Not-null assertion operator,” read the assertion explanation, from the shorthand for a cast through its runtime risk. Stop before “Late variables.”

A related Kotlin operator is as?, a safe cast: value as? String yields a String?, returning null if the value is not a string. Dart’s as cast can fail with an exception; a Dart is check is often the better comparison when you want to test a type before using it.


Type inference keeps the types, not just the brevity

Neither language requires you to annotate every local variable. The initializer often gives the compiler enough information:

val count = 3          // Int
var status = "queued"  // String
status = "running"     // OK
// status = null       // Compile error
final count = 3;        // int
var status = 'queued';  // String
status = 'running';     // OK
// status = null;       // Compile error

Inference did not make status untyped. Both compilers inferred a non-nullable string type, so changing the value is allowed but assigning null is not. If absence is part of the intended contract, say so:

var status: String? = null
String? status;

Annotations are especially useful at boundaries. Kotlin function parameters require declared types, and you should make the return type explicit when it communicates an important promise:

fun displayName(input: String?): String =
    input ?: "Unknown"

The signature promises callers a non-null String even though the input may be absent. Dart can express the same contract:

String displayName(String? input) => input ?? 'Unknown';

As you read more Kotlin, distinguish an inferred type from a nullable type. val name = "A" infers String; val name: String? = "A" is declared nullable despite currently holding a string. Subsequent code must respect the declared possibility of null unless a check establishes otherwise.


Key takeaways

Kotlin and Dart both distinguish T from T?, infer types for many local declarations, and let null checks establish when a nullable value is safe to use. The translation points to remember are Kotlin ?: versus Dart ??, Kotlin !! versus Dart !, and Kotlin’s per-receiver safe calls versus Dart’s null-aware chain behavior. Kotlin’s Any? remains statically checked; it is not Dart’s dynamic.

Next, you’ll use Kotlin’s control-flow expressions, ranges, and destructuring to write concise program logic. The null checks and early returns from this lesson will be useful immediately.

Can't find a good explanation? Sign up and we'll make it for you

Sign up