Kotlin for JS Devs

~12 min read · Slice 1 of 10

Introduction

This is the first article in a series where I'll be picking up native Android development, with the goal of building a small boxercise drill timer called 1-1-2 in Kotlin and Jetpack Compose, then doing the iOS counterpart in Swift.

Before scaffolding the actual app in the next slice, this article is the Kotlin language pieces a JavaScript developer needs to know to read and write Android code. We're not touching Android itself yet, that comes in the next slice of the series where we'll set up Android Studio.

Throughout the article, each snippet has a small language tag in the top-right corner: red for JavaScript and green for Kotlin, so you can tell which is which at a glance.

Variables, val and var

Kotlin gives you two keywords for declaring variables, val for things you don't reassign, and var for things you do. If you've used const and let in JavaScript, you've already got the model, since val maps to const and var lines up with let.

1const fighter = "Ali"; // can't be reassigned
2let rounds = 0; // can be reassigned
3rounds = 1;
1val fighter = "Ali" // can't be reassigned
2var rounds = 0 // can be reassigned
3rounds = 1
Keyword pairing
JavaScriptKotlinBehaviour
constvallocked, no reassign
letvarreassignable

There's no Kotlin equivalent of JavaScript's var, since block scope just works the way you'd expect with no scope leaking, which keeps things simpler when reading someone else's code.

Type inference

You don't have to write types most of the time, since Kotlin will infer them based on the assignment, in the same way TypeScript infers them for const foo = "bar".

1// JavaScript is dynamically typed, no static types in the source to infer
2const fighter = "Ali";
3const rounds = 12;
4const pace = 2.5;
1val fighter = "Ali" // Kotlin knows this is a String
2val rounds = 12 // Kotlin knows this is an Int
3val pace = 2.5 // Kotlin knows this is a Double

When you do want to be explicit, the type goes after the name with a colon, the same shape you'd use in TypeScript but without the let keyword in front.

1// TypeScript:
2const fighter: string = "Ali";
3const rounds: number = 12;
1val fighter: String = "Ali"
2val rounds: Int = 12

Null safety, the big one

Definition

A type with no question mark cannot be null. A type with a question mark is nullable, and the compiler will refuse to let you call methods on it without first acknowledging that it might be null.

This is the bit that looks like it'll genuinely change how you write code, especially coming from TypeScript projects that mix strict and non-strict modes. In TypeScript, a value can be null or undefined and the compiler will only catch it if strict mode is on and the codebase has actually been configured to use it. Kotlin tracks nullability for every single value as part of the type system, and it won't compile if you try to use a nullable value as if it isn't.

1// JavaScript has no compile-time nullable distinction
2const gym = "Eastside Boxing";
3const gymMaybe = null;
1val gym: String = "Eastside Boxing" // can never be null
2val gymMaybe: String? = null // can be null

If you try to call a method on a nullable value without handling it, the compiler stops you before the code can run.

1const gym = null;
2gym.length; // runtime TypeError, not a compile error
1val gym: String? = null
2gym.length // won't compile

There are three ways to handle a nullable value, and you'll see all three in real Android code.

1gym?.length; // returns undefined if null/undefined
2gym.length; // throws if null. Don't.
3gym?.length ?? 0; // nullish coalescing fallback
1gym?.length // returns null if gym is null, otherwise the length
2gym!!.length // throws NullPointerException if null. Don't do this in real code.
3gym?.length ?: 0 // Elvis operator: if null, fall back to 0
Nullable handlers
OperatorWhat it does
?.Safe call, returns null if the value is null
?:Elvis, falls back to the right side when null
!!Asserts not-null, throws NullPointerException if it is

If you ever wrote user && user.profile && user.profile.name in JavaScript before optional chaining showed up in 2020, the ?. operator will feel familiar. The ?: is called the Elvis operator because it has a quiff, and it does the same job as ?? in JavaScript.

The reason this matters in practice is that NullPointerException is one of the most common crashes in older Android code written in Java. Kotlin's type system makes it almost impossible to write one accidentally, which from a JavaScript background should mean fewer "undefined is not an object" reports in production once the app is live.

when expressions

Switch statements in JavaScript have a few footguns that have caught me out over the years, the main ones being fallthrough by default, no expression form (so you can't easily use a switch to assign a value), and that break keyword you have to remember every single time. Kotlin replaces switch with when, and it doesn't have any of those problems.

1let score;
2switch (round) {
3 case 1: score = 10; break;
4 case 2: score = 9; break;
5 case 3:
6 case 4: score = 8; break;
7 default:
8 score = (round >= 5 && round <= 10) ? 7 : 0;
9}
1val score = when (round) {
2 1 -> 10
3 2 -> 9
4 3, 4 -> 8 // multiple values per branch
5 in 5..10 -> 7 // ranges
6 else -> 0
7}

It's an expression that returns a value, with no break statements and no fallthrough behaviour. You can match multiple values per branch, use ranges with the in keyword, and pattern-match without an argument by writing it as a chain of conditions.

1const verdict =
2 score >= 9 ? "won the round" :
3 score >= 7 ? "competitive round" :
4 "rough one";
1val verdict = when {
2 score >= 9 -> "won the round"
3 score >= 7 -> "competitive round"
4 else -> "rough one"
5}

This looks like the pattern you'd naturally reach for instead of nested ternaries in any function that needs to return a value based on conditions, since it reads much more clearly than the equivalent if/else chain.

While we're on expressions, Kotlin's if also returns a value the same way a JavaScript ternary does, so val score = if (won) 10 else 0 is idiomatic and you don't need to wrap it in a function or pre-declare the variable.

Data classes

A data class is closer to a TypeScript or JavaScript class than to a TypeScript interface, since it's a real runtime class rather than a type that compiles away to nothing. The thing Kotlin adds on top is generating the standard boilerplate methods for you, so you get equals, hashCode, toString, and a copy method without having to write any of them yourself.

1class Drill {
2 constructor(name, durationSeconds, isRest) {
3 this.name = name;
4 this.durationSeconds = durationSeconds;
5 this.isRest = isRest;
6 }
7 // toString, equals, copy: all manual in JS
8}
1data class Drill(
2 val name: String,
3 val durationSeconds: Int,
4 val isRest: Boolean,
5)

The copy method looks like the bit that'll save the most time day to day, since it gives you the immutable update pattern you'd otherwise write by hand in JavaScript with the spread operator.

1const jab = { name: "Jab cross", durationSeconds: 60, isRest: false };
2const jabExtended = { ...jab, durationSeconds: 90 };
3// jab is unchanged. jabExtended is a new object.
1val jab = Drill("Jab cross", 60, isRest = false)
2val jabExtended = jab.copy(durationSeconds = 90)
3// jab is unchanged. jabExtended is a new instance.

It's basically { ...drill, durationSeconds: 90 } with type safety, so if the source type changes you'll get a compile error rather than a silently dropped field.

Functions

Function syntax is straightforward, with fun followed by the name, parameters, return type, and body.

1function callOut(fighter) {
2 return `Step in, ${fighter}`;
3}
1fun callOut(fighter: String): String {
2 return "Step in, $fighter"
3}

For one-line functions you can drop the braces and the return statement and use a single-expression form, which looks like a much nicer way to write helper functions than the equivalent JavaScript arrow expression.

1const callOut = (fighter) => `Step in, ${fighter}`;
1fun callOut(fighter: String): String = "Step in, $fighter"

Kotlin has two function features that combine to make APIs nicer to use. The first is named arguments, where you can label each argument at the call site and pass them in any order, which is the bit JavaScript doesn't have natively (the closest you can get is a destructured options object).

1// JavaScript fakes named args with a destructured options object
2function startRound({ durationSeconds, restSeconds, withVoice }) {}
3
4startRound({ durationSeconds: 180, restSeconds: 60, withVoice: true });
5startRound({ withVoice: true, durationSeconds: 180, restSeconds: 60 });
1fun startRound(durationSeconds: Int, restSeconds: Int, withVoice: Boolean) { }
2
3startRound(durationSeconds = 180, restSeconds = 60, withVoice = true)
4startRound(withVoice = true, durationSeconds = 180, restSeconds = 60)

The second is default parameters, which work the same way as the ES2015 default parameters JavaScript has had for years, where each parameter can have a fallback value used if the caller doesn't provide one.

1function startRound({
2 durationSeconds = 180,
3 restSeconds = 60,
4 withVoice = true,
5} = {}) {}
6
7startRound();
8startRound({ durationSeconds: 120 });
1fun startRound(
2 durationSeconds: Int = 180,
3 restSeconds: Int = 60,
4 withVoice: Boolean = true,
5) { }
6
7startRound() // uses every default
8startRound(durationSeconds = 120)

The combination is what JavaScript can't do cleanly, since named arguments plus defaults means you can write a Kotlin function with five optional parameters and the call sites stay readable, naming only the arguments you actually want to pass.

Strings

Template literals exist in Kotlin, just with a different syntax than JavaScript. JavaScript uses backticks and ${...} for interpolation, while Kotlin uses double quotes and a $ prefix for variable interpolation, with ${...} reserved for embedding an expression rather than a single variable name.

1const greeting = `Hey ${fighter}, you've boxed ${rounds} rounds`
1val greeting = "Hey $fighter, you've boxed $rounds rounds"

The expression form looks the same in both languages, since the body inside ${...} is just regular code.

1const greeting = `Hey ${fighter}, ${rounds > 0 ? "welcome back" : "let's begin"}`
1val greeting = "Hey $fighter, ${if (rounds > 0) "welcome back" else "let's begin"}"

Multi-line strings use triple quotes, and the .trimIndent() method removes the common leading whitespace, which is handy if you want to indent the string in source for readability without that indent showing up in the output. JavaScript doesn't have a built-in equivalent, so you end up reaching for a regex.

1// No built-in trimIndent, you do it yourself
2const callout = `
3 Round 1: jab cross
4 Round 2: jab cross hook
5 Round 3: anything goes
6`.replace(/^\s+/gm, '').trim();
1val callout = """
2 Round 1: jab cross
3 Round 2: jab cross hook
4 Round 3: anything goes
5""".trimIndent()

Collections

Coming from JavaScript, the bit that's new here is that Kotlin makes the read-only and mutable split explicit at the type level. listOf gives you a List you can't add to, and mutableListOf gives you a MutableList you can. Same shape for sets and maps with setOf, mutableSetOf, mapOf, and mutableMapOf.

1// JavaScript arrays are always mutable, no read-only/mutable distinction
2const drills = ["jab", "cross", "hook"];
3drills.push("uppercut");
4
5const scores = { ali: 10, frazier: 9 };
6scores.foreman = 8;
1val drills = listOf("jab", "cross", "hook") // read-only List
2val moreDrills = mutableListOf("jab", "cross", "hook") // can add to
3moreDrills.add("uppercut")
4
5val scores = mapOf("ali" to 10, "frazier" to 9) // read-only Map
6val mutableScores = mutableMapOf("ali" to 10)
7mutableScores["frazier"] = 9

Once you spend more time with .map, .filter, and .reduce returning new collections, you'll catch yourself reaching for mutableListOf less often than you'd think, since most of the time you don't need a mutable container at all.

Lambdas

Definition

A lambda is a function written as a value. You can assign it to a variable, pass it straight to another function, or return it from one. Kotlin's { x: Int -> x * x } is the same idea as JavaScript's (x) => x * x, just with braces around the whole thing and an arrow separating the argument list from the body.

The Kotlin syntax sits the argument list and the body both inside the braces, separated by an arrow. The example below assigns the lambda to a variable, the same way you'd assign an arrow function in JavaScript.

1const square = (x) => x * x;
2square(3); // 9
1val square = { x: Int -> x * x }
2square(3) // 9

The next example passes a lambda directly to .map without naming it, the same way you'd pass an inline arrow function in JavaScript. Two extra Kotlin niceties are doing the work here. First, when a lambda is the last argument to a function, you can pull it outside the parentheses, which is what makes the standard library read so cleanly. Second, when the lambda takes a single argument, Kotlin gives you that argument for free under the name it, so you don't have to declare a parameter name at all. JavaScript still needs you to name the parameter, even if it's just s or x.

1["jab", "cross", "hook"].map((s) => s.toUpperCase());
2// ["JAB", "CROSS", "HOOK"]
1listOf("jab", "cross", "hook").map { it.uppercase() } // [JAB, CROSS, HOOK]

Extension functions

This is one Kotlin feature with no direct JavaScript equivalent. You can add a method to a class you don't own by writing a function that has a receiver type, which gets you method-call syntax without having to wrap or subclass anything. The closest JS gets is monkey-patching a prototype, which is why most teams ban it.

1// Closest JS equivalent is monkey-patching the prototype, not type-safe
2String.prototype.shout = function () {
3 return this.toUpperCase() + "!";
4};
5
6"jab".shout(); // "JAB!"
1fun String.shout(): String = this.uppercase() + "!"
2
3"jab".shout() // "JAB!"

Under the hood Kotlin's version is a static function that takes the receiver as its first argument, but at the call site it reads like a method on the type. This is the bit that shows up all over the Android docs, across Compose, Coroutines, and the standard library, and the shape is similar to lodash chaining, except first-party and type-safe.

JS to Kotlin at a glance

This is the table I'll be glancing at while I'm in the editor, so it lives here at the bottom of the article rather than buried in the middle.

Quick translation
JavaScriptKotlin
constval
letvar
???:
?.?.
switchwhen
`Hey ${name}`"Hey $name"
{ ...drill, durationSeconds: 90 }drill.copy(durationSeconds = 90)
arr.map(x => x * 2)list.map { it * 2 }

Final thoughts

There's plenty more I haven't covered here, including sealed classes, coroutines, scope functions like let and apply, generics, type aliases, and delegation. I'll get to those in later articles when we actually need them in the app, since reading about language features without using them sticks for about ten minutes in my experience.

Everything above is enough to start reading the Android code we'll begin writing in the next slice of the series.

Further reading

There's plenty more to Kotlin than fits in one article, and the official docs are well-organised. These are the pages worth bookmarking for the bits I've skipped and the topics that go beyond what we've covered here.

  • Basic syntax: a single-page overview of the language, handy as a quick refresher.
  • Keyword reference: every hard, soft, and modifier keyword in one place, with what each one does.
  • Basic types: how Int, Long, Float, Double, Boolean, Char, String, and arrays behave.
  • Control flow: if and when as expressions, for, while, and ranges.
  • Collections: the full picture on List, Set, Map, sequences, and the standard library functions you'll be reaching for daily.
  • Classes and inheritance: class definitions, constructors, inheritance, abstract classes, and the open modifier.
  • Visibility modifiers: public, private, internal, protected, and where each applies.
  • Packages and imports: package declaration rules and the default imports Kotlin gives you for free.
  • Annotations: used heavily in Compose and Room, which we'll hit later in the series.
  • Scope functions: let, run, with, apply, and also, plus a guide on which one to pick. These show up everywhere in Android Kotlin and confuse JS devs more than anything, since they don't map onto a JavaScript pattern at all.
  • Coroutines: Kotlin's async story. We'll dig into these when we get to ViewModels and StateFlow.

Try this for yourself

If you want to feel the difference between TypeScript and Kotlin in practice, here's a TypeScript snippet I'd recommend translating to Kotlin. It's short, but it touches optional fields, nullish coalescing, conditional logic, template literals, and a small bit of arithmetic, so you'll bump into the Elvis operator, when expressions, data classes, and string interpolation when you do the translation.

1type Drill = {
2 name: string;
3 durationSeconds: number;
4 callout?: string;
5 intensity?: 'light' | 'medium' | 'hard';
6};
7
8const formatDrill = (drill: Drill): string => {
9 const intensity = drill.intensity ?? 'medium';
10 const callout = drill.callout ?? drill.name;
11
12 if (drill.durationSeconds < 30) {
13 return `Quick ${callout} (${intensity})`;
14 } else if (drill.durationSeconds < 90) {
15 return `${callout} for ${drill.durationSeconds}s (${intensity})`;
16 } else {
17 const mins = Math.floor(drill.durationSeconds / 60);
18 return `${callout} for ${mins} min (${intensity})`;
19 }
20};

Open the Kotlin Playground and have a go at translating it. When you're done, expand the solution below to see how I'd write it.

Show solution
1data class Drill(
2 val name: String,
3 val durationSeconds: Int,
4 val callout: String? = null,
5 val intensity: String? = null,
6)
7
8fun formatDrill(drill: Drill): String {
9 val intensity = drill.intensity ?: "medium"
10 val callout = drill.callout ?: drill.name
11
12 return when {
13 drill.durationSeconds < 30 -> "Quick $callout ($intensity)"
14 drill.durationSeconds < 90 -> "$callout for ${drill.durationSeconds}s ($intensity)"
15 else -> {
16 val mins = drill.durationSeconds / 60
17 "$callout for $mins min ($intensity)"
18 }
19 }
20}
21
22fun main() {
23 val drills = listOf(
24 Drill(name = "Jab cross", durationSeconds = 20),
25 Drill(name = "Mountain climbers", durationSeconds = 60, intensity = "hard"),
26 Drill(name = "Heavy bag", durationSeconds = 180, callout = "1-2 hooks"),
27 )
28 drills.forEach { println(formatDrill(it)) }
29}

I find these things stick better when you've actually typed them out and had to figure out the Kotlin equivalent yourself, rather than just read along. The next article in the series picks up with Android Studio, Gradle, and the first screen of 1-1-2.