Variables & Types
val vs var — immutable and mutable
const maxRetries = 3; // const: cannot be reassigned
let attemptCount = 0; // let: can be reassigned
attemptCount = 1;
console.log(maxRetries);
console.log(attemptCount); fun main() {
val maxRetries = 3 // val: immutable binding (like const)
var attemptCount = 0 // var: mutable binding (like let)
attemptCount = 1
println(maxRetries)
println(attemptCount)
} val declares an immutable binding — once assigned, it cannot be reassigned — and var declares a mutable one. The semantics map directly to JavaScript's const and let. Unlike Swift (where let is immutable), Kotlin's naming is intuitive: "val" for value (stable), "var" for variable (changeable). Idiomatic Kotlin strongly prefers val — reach for var only when mutation is genuinely required.Type inference
let count = 42; // inferred as number at runtime
let message = 'hello'; // inferred as string
// count = 'oops'; // valid JS — type can change at any time
console.log(count);
console.log(message); fun main() {
val count = 42 // inferred as Int at compile time
val message = "hello" // inferred as String
// count = "oops" // compile error — type is fixed forever
println(count)
println(message)
} Kotlin infers types from the initial value just as JavaScript does — but Kotlin's inference happens at compile time, not runtime. Once Kotlin infers
count as Int, assigning a String to it is a compile-time error, not a runtime surprise. JavaScript's typeof returns the current type at runtime; Kotlin's type is fixed from the moment the variable is declared.Explicit type annotations
// JavaScript has no built-in type annotation syntax
const productName = "Widget";
const price = 9.99;
const quantity = 100;
const inStock = true;
console.log(productName, price, quantity, inStock); fun main() {
val productName: String = "Widget"
val price: Double = 9.99
val quantity: Int = 100
val inStock: Boolean = true
println(productName)
println(price)
println(quantity)
println(inStock)
} Kotlin type annotations use a colon after the variable name — the same syntax as TypeScript. Unlike JavaScript's single
number type, Kotlin distinguishes Int (32-bit integer), Long (64-bit integer), Double (64-bit float), and Float (32-bit float). Annotations are optional when Kotlin can infer the type from the right-hand side, but they improve readability for complex or non-obvious types.The Any type — the escape hatch
// Every JavaScript variable can hold any value
let value = 42;
value = "now a string";
value = [1, 2, 3];
console.log(value);
console.log(typeof value); fun main() {
var value: Any = 42
value = "now a string" // Any accepts any non-null type
value = listOf(1, 2, 3)
println(value)
println(value::class.simpleName)
} Kotlin's
Any is the root of the type hierarchy — every non-nullable type is a subtype of Any, analogous to TypeScript's any or JavaScript's untyped variables. Using Any deliberately opts out of static type safety for that binding. When null is also a possible value, use Any? instead. Reach for Any sparingly — the Kotlin type system pays off most when types are as specific as possible.Null Safety
Nullable vs non-nullable types
let username = "Alice";
username = null; // any JS variable can be null — no enforcement
console.log(username); // null — no error caught until runtime fun main() {
var name: String = "Alice" // non-nullable: null is a compile error
// name = null // ❌ compile error
var nickname: String? = "Ali" // nullable: null is allowed
nickname = null
println(nickname) // null
} Kotlin's type system draws a hard line between nullable (
String?) and non-nullable (String) types using the ? suffix. A String variable can never be null — attempting to assign null is a compile error. This eliminates an entire class of bugs that JavaScript developers encounter as Cannot read properties of null at runtime. In JavaScript, any variable can be reassigned to null or undefined without any type error.Safe call operator (?.) — same as JS optional chaining
const user = null;
const username = user?.name; // undefined, not an error
const length = user?.name?.length; // undefined, chained safely
console.log(username);
console.log(length); fun main() {
val user: String? = null
val length = user?.length // null, not an error
println(length) // null
val greeting: String? = "Hello"
println(greeting?.length) // 5
println(greeting?.uppercase()) // HELLO
} Kotlin's
?. safe call operator has identical semantics to JavaScript's optional chaining ?. introduced in ES2020: if the receiver is null, the entire expression short-circuits to null rather than throwing. Kotlin has had ?. since version 1.0 (2016). The key advantage over JavaScript is that the Kotlin compiler tracks which expressions can produce null and warns when you call a non-nullable method on a nullable value without the safe call.Elvis operator (?:) — Kotlin's nullish coalescing
const rawInput = null;
const displayName = rawInput ?? "Anonymous";
console.log(displayName); // Anonymous
const count = undefined;
const result = count ?? 0;
console.log(result); // 0 fun main() {
val rawInput: String? = null
val displayName = rawInput ?: "Anonymous"
println(displayName) // Anonymous
val count: Int? = null
val result = count ?: 0
println(result) // 0
// Pairs naturally with safe calls:
val text: String? = null
println(text?.length ?: -1) // -1 because text is null
} Kotlin's Elvis operator
?: is the direct equivalent of JavaScript's nullish coalescing ??: if the left side is null, it evaluates and returns the right side. The name "Elvis" is a joke — ?: resembles an emoticon of Elvis Presley's quiff. Unlike JavaScript's || (which triggers on any falsy value including 0 and ""), ?: triggers only on null, matching the behavior of ??.Non-null assertion (!!) — the danger operator
// JavaScript: accessing a possibly-null value — no compile-time protection
function getLength(text) {
return text.length; // TypeError at runtime if text is null/undefined
}
console.log(getLength("hello")); fun main() {
val possiblyNull: String? = "hello"
// !! asserts non-null — throws NullPointerException if it is actually null
val definitelyString: String = possiblyNull!!
println(definitelyString.length) // 5
// Prefer safe call + Elvis over !!:
val saferLength = possiblyNull?.length ?: 0
println(saferLength) // 5
} The
!! operator converts a nullable String? to a non-nullable String, asserting that the value is not null. If it is null at runtime, Kotlin throws a NullPointerException. Idiomatic Kotlin treats !! as a code smell — it represents the developer overriding the compiler's null tracking. The preferred alternative is to handle null explicitly with ?. and ?:, making the program's behavior under null intentional and visible.Null checks with let and smart casts
const username = "Alice";
if (username !== null && username !== undefined) {
console.log(`Hello, ${username.toUpperCase()}!`);
}
const greeting = username ? `Hello, ${username}!` : "Hello, stranger!";
console.log(greeting); fun main() {
val username: String? = "Alice"
// let runs the block only when username is non-null:
username?.let { name ->
println("Hello, ${name.uppercase()}!")
}
// Smart cast: after the null check, Kotlin knows username is String
if (username != null) {
println("Hello, ${username.uppercase()}!") // no ?. needed here
}
} Kotlin's
?.let { } runs a lambda block only when the receiver is non-null, with the non-null value bound to the parameter (it by default, or a named parameter like name). Inside an if (value != null) block, the Kotlin compiler performs a smart cast — it knows username is String (not String?) and allows calling String methods without the ?. safe call. This is one of Kotlin's most practical null-safety features.Strings
String templates — same ${} syntax as JS
const name = "World";
const count = 42;
const greeting = `Hello, ${name}! Count: ${count}.`;
console.log(greeting);
const sum = `2 + 2 = ${2 + 2}`;
console.log(sum); fun main() {
val name = "World"
val count = 42
val greeting = "Hello, $name! Count: $count." // $ alone for identifiers
println(greeting)
val sum = "2 + 2 = ${2 + 2}" // ${} for expressions
println(sum)
val text = "Length of '$name' is ${name.length}"
println(text)
} Kotlin string templates use
$identifier for simple variable names and ${expression} for expressions — the same ${} delimiter as JavaScript template literals, but without surrounding backticks. Kotlin regular strings (double-quoted) support templates natively; JavaScript template literals require backtick quotes. Simple variable references only need a dollar sign in Kotlin ($name), whereas JavaScript always requires the braces (${name}).Raw multiline strings
const poem = `Roses are red,
Violets are blue,
Kotlin is typed,
And JavaScript too.`;
console.log(poem);
const path = `C:\\Users\\Alice`; // escape needed in template literals
console.log(path); fun main() {
val poem = """
Roses are red,
Violets are blue,
Kotlin is typed,
And JavaScript too.
""".trimIndent()
println(poem)
val path = """C:UsersAlice""" // no escape sequences in raw strings
println(path)
} Kotlin's triple-quoted strings are raw string literals — no escape sequences are processed inside them, so backslashes are literal characters. The
.trimIndent() method strips the common leading whitespace from each line, producing cleanly formatted output. JavaScript template literals always process escape sequences (\n, \t, etc.); Kotlin raw strings do not — a \n inside triple quotes is two characters, not a newline.Common string methods
const text = " Hello, World! ";
console.log(text.trim());
console.log(text.trim().toLowerCase());
console.log(text.trim().toUpperCase());
console.log("hello".includes("ell"));
console.log("hello".startsWith("hel"));
console.log("hello".repeat(3));
console.log("a,b,c".split(",")); fun main() {
val text = " Hello, World! "
println(text.trim())
println(text.trim().lowercase())
println(text.trim().uppercase())
println("hello".contains("ell"))
println("hello".startsWith("hel"))
println("hello".repeat(3))
println("a,b,c".split(","))
} Kotlin's string API closely mirrors JavaScript's. The main naming differences are
lowercase() and uppercase() instead of toLowerCase() and toUpperCase(), and contains() instead of includes(). Splitting in Kotlin returns a List<String> rather than a JavaScript Array. Kotlin strings are immutable — like JavaScript strings, all methods return new strings rather than modifying the original.Converting to and from strings
const number = 42;
const text = String(number);
console.log(text + " is a " + typeof text);
const parsed = parseInt("123");
console.log(parsed + 1);
const price = parseFloat("9.99");
console.log(price * 2); fun main() {
val number = 42
val text = number.toString()
println("$text is a ${text::class.simpleName}")
val parsed = "123".toInt()
println(parsed + 1)
val price = "9.99".toDouble()
println(price * 2)
// Safe conversion — returns null instead of throwing:
val safe = "not-a-number".toIntOrNull() ?: 0
println(safe)
} Kotlin attaches conversion methods directly to values rather than using global functions like
parseInt and parseFloat. The toInt(), toDouble(), and toLong() methods throw a NumberFormatException if the string is not parseable — analogous to JavaScript's parseInt("abc") returning NaN. The safer toIntOrNull() returns null instead of throwing, which pairs naturally with the Elvis operator to provide a default.Accessing characters
const word = "Kotlin";
console.log(word[0]); // K
console.log(word.charAt(0)); // K
console.log(word.charCodeAt(0)); // 75
console.log(word.length); // 6 fun main() {
val word = "Kotlin"
println(word[0]) // K (type: Char, not String)
println(word.first()) // K
println(word[0].code) // 75 (Unicode code point)
println(word.length) // 6
val letter: Char = 'K' // single quotes for Char literals
println(letter.uppercaseChar())
} Kotlin's index operator
[] on a string returns a Char — a distinct primitive type representing a single Unicode character, not a single-character string as in JavaScript. Character literals in Kotlin use single quotes ('K'), while string literals use double quotes. The .code property returns the Unicode code point, equivalent to JavaScript's .charCodeAt(). The .first() and .last() extension functions are idiomatic alternatives to bracket indexing.Collections
Arrays and Lists — immutable vs mutable
const fruits = ["apple", "banana", "cherry"];
fruits.push("date");
console.log(fruits);
console.log(fruits.length);
console.log(fruits[1]); fun main() {
// Immutable list — add/remove are compile errors:
val fruits = listOf("apple", "banana", "cherry")
println(fruits)
println(fruits.size)
println(fruits[1])
// Mutable list — like a JavaScript array:
val mutableFruits = mutableListOf("apple", "banana", "cherry")
mutableFruits.add("date")
println(mutableFruits)
} Kotlin distinguishes between immutable collections (
listOf()) and mutable ones (mutableListOf()) at the type level. Immutable lists have no add() or remove() methods — the compiler enforces read-only access. The size property replaces JavaScript's length. All of Kotlin's collection operations (map, filter, etc.) work on both immutable and mutable lists and always return new immutable lists rather than mutating the original.Maps
const scores = new Map([
["Alice", 95],
["Bob", 87],
["Carol", 92],
]);
scores.set("Dave", 88);
console.log(scores.get("Alice"));
console.log(scores.has("Eve"));
console.log(scores.size); fun main() {
val scores = mutableMapOf(
"Alice" to 95,
"Bob" to 87,
"Carol" to 92,
)
scores["Dave"] = 88
println(scores["Alice"]) // 95
println("Eve" in scores) // false
println(scores.size) // 4
println(scores)
} Kotlin maps use the
to infix function to create key-value pairs — "Alice" to 95 creates a Pair<String, Int>. Accessing a missing key with [] returns null (the map value type becomes nullable), equivalent to JavaScript's Map.get() returning undefined. Kotlin's in operator checks for key existence, replacing Map.has(). Like lists, mapOf() is immutable and mutableMapOf() allows mutation.Sets
const tags = new Set(["typescript", "javascript", "nodejs"]);
tags.add("react");
console.log(tags.has("typescript"));
console.log(tags.size);
tags.delete("nodejs");
console.log([...tags]); fun main() {
val tags = mutableSetOf("typescript", "javascript", "nodejs")
tags.add("react")
println("typescript" in tags) // true
println(tags.size) // 4
tags.remove("nodejs")
println(tags)
} Kotlin's
Set API mirrors JavaScript's Set closely. The in operator replaces has(), and remove() replaces delete(). Adding a duplicate silently does nothing, just like JavaScript's Set. setOf() creates an immutable set; mutableSetOf() allows modification. Kotlin also provides LinkedHashSet (insertion-ordered, the default), TreeSet (sorted), and HashSet (unordered, fastest).Destructuring and combining lists
const numbers = [1, 2, 3];
const [first, second, ...rest] = numbers;
console.log(first); // 1
console.log(second); // 2
console.log(rest); // [3]
const combined = [...numbers, 4, 5];
console.log(combined); fun main() {
val numbers = listOf(1, 2, 3)
val (first, second) = numbers // positional destructuring
val rest = numbers.drop(2)
println(first) // 1
println(second) // 2
println(rest) // [3]
val combined = numbers + listOf(4, 5)
println(combined) // [1, 2, 3, 4, 5]
} Kotlin supports positional list destructuring using
val (a, b) = list, which calls the generated component1() and component2() functions. Unlike JavaScript's rest pattern (...rest), Kotlin has no built-in rest destructuring syntax — use .drop(n) to get remaining elements. The + operator on lists creates a new combined list (originals unchanged), equivalent to JavaScript's spread: [...list1, ...list2].map, filter, fold — same concepts, Kotlin syntax
const numbers = [1, 2, 3, 4, 5];
console.log(numbers.filter(n => n % 2 === 0)); // [2, 4]
console.log(numbers.map(n => n * n)); // [1, 4, 9, 16, 25]
console.log(numbers.reduce((sum, n) => sum + n, 0)); // 15
console.log(numbers.find(n => n > 3)); // 4 fun main() {
val numbers = listOf(1, 2, 3, 4, 5)
println(numbers.filter { it % 2 == 0 }) // [2, 4]
println(numbers.map { it * it }) // [1, 4, 9, 16, 25]
println(numbers.fold(0) { sum, n -> sum + n }) // 15
println(numbers.find { it > 3 }) // 4
} Kotlin's collection operations (
filter, map, find, fold) mirror JavaScript's Array methods closely. The key syntactic difference is that Kotlin lambdas are passed in trailing curly braces rather than parentheses, and the implicit single parameter is named it. JavaScript's reduce() with an initializer corresponds to Kotlin's fold(initialValue) { acc, value -> }. Kotlin also has flatMap, groupBy, sortedBy, partition, zip, and many more.Control Flow
if as an expression — no ternary needed
const score = 75;
const grade = score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : "F";
console.log(grade);
let result;
if (score >= 70) {
result = "pass";
} else {
result = "fail";
}
console.log(result); fun main() {
val score = 75
// if is an expression in Kotlin — it returns a value directly:
val grade = if (score >= 90) "A" else if (score >= 80) "B" else if (score >= 70) "C" else "F"
println(grade)
val result = if (score >= 70) "pass" else "fail"
println(result)
} In Kotlin,
if/else is an expression that returns a value — there is no separate ternary operator ? :. The else branch is required when if is used as an expression (the compiler needs to guarantee a value for every path). This eliminates the need for JavaScript's ? : ternary while keeping multi-branch logic readable without nesting.when vs switch — no fall-through, returns a value
const day = "Monday";
let type;
switch (day) {
case "Saturday":
case "Sunday":
type = "weekend"; break;
case "Monday":
case "Friday":
type = "busy"; break;
default:
type = "regular";
}
console.log(type); fun main() {
val day = "Monday"
val type = when (day) {
"Saturday", "Sunday" -> "weekend"
"Monday", "Friday" -> "busy"
else -> "regular"
}
println(type)
} Kotlin's
when replaces JavaScript's switch and eliminates fall-through: each branch ends at -> with no break needed. Multiple values can match a single branch, separated by commas. when is an expression — it returns the matched branch's value directly. The else branch is required when when is used as an expression; the Kotlin compiler enforces exhaustiveness for sealed classes and enums.when with arbitrary conditions and ranges
const temperature = 25;
let description;
if (temperature < 0) {
description = "freezing";
} else if (temperature < 15) {
description = "cold";
} else if (temperature < 25) {
description = "mild";
} else {
description = "warm";
}
console.log(description); fun main() {
val temperature = 25
// when without a subject — like an if-else chain:
val description = when {
temperature < 0 -> "freezing"
temperature < 15 -> "cold"
temperature < 25 -> "mild"
else -> "warm"
}
println(description)
// when with a subject — range matching with 'in':
val zone = when (temperature) {
in 0..14 -> "cold zone"
in 15..24 -> "temperate zone"
in 25..100 -> "warm zone"
else -> "extreme"
}
println(zone)
} When used without a subject (
when { ... }), Kotlin's when behaves like an if-else if chain where each branch is an arbitrary boolean expression. With a subject, when(value) { in range -> } matches ranges using the in keyword. This makes when more expressive than any form of switch in JavaScript — it subsumes both switch and complex if-else chains.Type checking with when and smart casts
function describe(value) {
if (typeof value === "number") {
return `number: ${value * 2}`;
} else if (typeof value === "string") {
return `string of length ${value.length}`;
} else if (Array.isArray(value)) {
return `array with ${value.length} items`;
}
return "unknown";
}
console.log(describe(21));
console.log(describe("hello"));
console.log(describe([1, 2, 3])); fun describe(value: Any): String = when (value) {
is Int -> "number: ${value * 2}" // smart cast to Int
is String -> "string of length ${value.length}" // smart cast to String
is List<*> -> "list with ${value.size} items"
else -> "unknown"
}
fun main() {
println(describe(21))
println(describe("hello"))
println(describe(listOf(1, 2, 3)))
} Kotlin's
is operator checks the runtime type and automatically smart-casts the value inside the matched branch — no explicit cast needed. After is Int, the compiler knows value is Int and allows Int-specific operations like multiplication. JavaScript's equivalent requires typeof, instanceof, or Array.isArray() with no automatic narrowing (TypeScript provides narrowing, but JavaScript itself does not).Loops & Ranges
Ranges — a first-class language feature
// JavaScript has no built-in range — use Array.from or spread:
const range1 = Array.from({ length: 5 }, (_, i) => i + 1); // [1,2,3,4,5]
const range2 = [...Array(5).keys()]; // [0,1,2,3,4]
console.log(range1);
console.log(range2); fun main() {
val range1 = 1..5 // IntRange, inclusive: 1, 2, 3, 4, 5
val range2 = 1 until 5 // IntRange, exclusive end: 1, 2, 3, 4
val range3 = 5 downTo 1 step 2 // 5, 3, 1
println(range1.toList())
println(range2.toList())
println(range3.toList())
println(3 in range1) // true
println(6 in range1) // false
} Kotlin has first-class range syntax:
.. creates an inclusive range, until creates an exclusive-end range, downTo reverses direction, and step controls the increment. Ranges are lightweight objects — they do not allocate a list in memory, so 1..1_000_000 is efficient to create and use in for loops or in membership checks. JavaScript has no equivalent built-in syntax; the common workaround allocates a full array.for…of vs for (item in collection)
const colors = ["red", "green", "blue"];
for (const color of colors) {
console.log(color);
}
for (let i = 0; i < colors.length; i++) {
console.log(`${i}: ${colors[i]}`);
} fun main() {
val colors = listOf("red", "green", "blue")
for (color in colors) {
println(color)
}
// Indexed iteration with withIndex():
for ((index, color) in colors.withIndex()) {
println("$index: $color")
}
} Kotlin's
for (item in collection) maps directly to JavaScript's for (const item of collection) — both iterate values, not indices. For indexed iteration, Kotlin's .withIndex() destructures to (index, value) pairs, similar to JavaScript's .entries(). Note that Kotlin's in keyword in a for loop is distinct from the in operator used for membership checks — context determines the meaning.Looping over ranges — replaces the C-style for loop
// Classic C-style indexed loop:
for (let i = 1; i <= 5; i++) {
console.log(i);
}
// Count down:
for (let i = 5; i >= 1; i--) {
console.log(i);
} fun main() {
// Inclusive range 1 to 5:
for (i in 1..5) {
println(i)
}
// Count down:
for (i in 5 downTo 1) {
println(i)
}
// Every other number:
for (i in 0..10 step 2) {
println(i)
}
} Kotlin's range-based
for loop replaces the C-style indexed loop (for (let i = 0; i < n; i++)). The 1..5 range includes both endpoints; 1 until 6 is equivalent but excludes the end. downTo creates a descending range and step sets the increment. These are first-class language constructs, not library functions, making the intent clearer than equivalent JavaScript patterns.while and do-while
let count = 0;
while (count < 5) {
console.log(count);
count++;
}
let value = 10;
do {
console.log(value);
value--;
} while (value > 8); fun main() {
var count = 0
while (count < 5) {
println(count)
count++
}
var value = 10
do {
println(value)
value--
} while (value > 8)
} Kotlin's
while and do-while loops have identical semantics to JavaScript's. Both languages support break to exit a loop early and continue to skip to the next iteration. Kotlin adds labeled breaks (break@label) for exiting nested loops from an inner loop — a capability JavaScript lacks without wrapping the code in a function and using return.forEach, forEachIndexed, and repeat
const items = ["a", "b", "c"];
items.forEach((item, index) => {
console.log(`${index}: ${item}`);
});
// Repeat 3 times:
Array.from({ length: 3 }).forEach((_, i) => {
console.log(`Iteration ${i}`);
}); fun main() {
val items = listOf("a", "b", "c")
items.forEachIndexed { index, item ->
println("$index: $item")
}
// repeat(n) is idiomatic for running a block n times:
repeat(3) { iteration ->
println("Iteration $iteration")
}
} Kotlin's
.forEachIndexed { index, value -> } is equivalent to JavaScript's .forEach((value, index) => {}). Note that Kotlin's parameter order puts the index first (the opposite of JavaScript's (value, index) convention). The built-in repeat(n) { i -> } function replaces JavaScript's Array.from({length: n}).forEach(...) pattern for repeating a block a fixed number of times.Functions
Function declaration
function greet(name) {
return `Hello, ${name}!`;
}
console.log(greet("World"));
// Arrow function:
const square = x => x * x;
console.log(square(5)); fun greet(name: String): String {
return "Hello, $name!"
}
// Single-expression shorthand — return type inferred:
fun square(x: Int) = x * x
fun main() {
println(greet("World"))
println(square(5))
} Kotlin functions use the
fun keyword and require type annotations on parameters and return types. The return type follows the parameter list with a colon (): String). Single-expression functions can use the = shorthand — the return type is inferred, so no explicit return or curly braces are needed. Unlike JavaScript, Kotlin cannot infer parameter types from call sites — they must always be annotated in the function signature.Default parameter values
function createGreeting(name, prefix = "Hello", punctuation = "!") {
return `${prefix}, ${name}${punctuation}`;
}
console.log(createGreeting("Alice"));
console.log(createGreeting("Bob", "Hi"));
console.log(createGreeting("Carol", "Hey", ".")); fun createGreeting(name: String, prefix: String = "Hello", punctuation: String = "!"): String =
"$prefix, $name$punctuation"
fun main() {
println(createGreeting("Alice"))
println(createGreeting("Bob", "Hi"))
println(createGreeting("Carol", "Hey", "."))
} Kotlin default parameter values work identically to JavaScript's — trailing parameters with defaults can be omitted at the call site. Unlike JavaScript, Kotlin also supports named arguments (next concept), which makes it possible to skip any default parameter regardless of position. This eliminates the common JavaScript workaround of passing
undefined to skip a middle parameter.Named arguments — no options-object workaround needed
// JavaScript convention: use an options object for clarity
function connect({ host = "localhost", port = 5432, database = "main" } = {}) {
console.log(`Connecting to ${host}:${port}/${database}`);
}
connect({ port: 3306, database: "customers" });
connect({ host: "db.example.com" }); fun connect(host: String = "localhost", port: Int = 5432, database: String = "main") {
println("Connecting to $host:$port/$database")
}
fun main() {
connect(port = 3306, database = "customers") // named args in any order
connect(host = "db.example.com")
connect("db.example.com", database = "orders") // mix positional + named
} Kotlin supports first-class named arguments — any parameter can be specified by name at the call site, in any order. This eliminates the JavaScript convention of wrapping everything in an options object to get readable call sites. Named arguments combined with default values replace method overloading in most cases. Positional arguments must precede named ones; once a named argument appears in a call, all subsequent arguments must also be named.
vararg — variable-length arguments
function sum(...numbers) {
return numbers.reduce((total, n) => total + n, 0);
}
console.log(sum(1, 2, 3));
console.log(sum(10, 20, 30, 40));
const values = [5, 6, 7];
console.log(sum(...values)); // spread into variadic function fun sum(vararg numbers: Int): Int = numbers.fold(0) { total, n -> total + n }
fun main() {
println(sum(1, 2, 3))
println(sum(10, 20, 30, 40))
val values = intArrayOf(5, 6, 7)
println(sum(*values)) // * is the spread operator for arrays
} Kotlin's
vararg keyword marks a parameter that accepts any number of arguments — equivalent to JavaScript's rest parameters (...args). Inside the function, numbers is an IntArray. When calling a vararg function with an existing array, Kotlin uses the spread operator *values — analogous to JavaScript's ...values spread in a function call. Unlike JavaScript's rest parameter (which must be last), Kotlin allows parameters after a vararg, though they must be passed as named arguments.Single-expression functions
const double = x => x * 2;
const isEven = n => n % 2 === 0;
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
console.log(double(5));
console.log(isEven(4));
console.log(clamp(15, 0, 10)); fun double(x: Int) = x * 2
fun isEven(n: Int) = n % 2 == 0
fun clamp(value: Int, minimum: Int, maximum: Int) = minOf(maxOf(value, minimum), maximum)
fun main() {
println(double(5))
println(isEven(4))
println(clamp(15, 0, 10))
} Kotlin's single-expression function syntax (
fun f(x: Int) = x * 2) is equivalent to JavaScript's implicit-return arrow function (const f = x => x * 2), but applies to named top-level functions and methods. The return type is inferred from the expression's type. This is idiomatic in Kotlin for short utility functions and is preferred over a full { return ... } body when the intent fits in one line.Lambdas & Higher-Order Functions
Lambda syntax and the implicit it parameter
const double = x => x * 2;
const add = (a, b) => a + b;
console.log(double(5));
console.log(add(3, 4));
const numbers = [1, 2, 3, 4, 5];
console.log(numbers.map(x => x * 2)); fun main() {
val double = { x: Int -> x * 2 }
val add = { a: Int, b: Int -> a + b }
println(double(5))
println(add(3, 4))
val numbers = listOf(1, 2, 3, 4, 5)
println(numbers.map { it * 2 }) // 'it' = implicit single parameter
println(numbers.map { x -> x * 2 }) // explicit parameter name
} Kotlin lambdas use curly braces with the parameter list before
->, while JavaScript uses arrow functions with the parameter list before =>. When a lambda has exactly one parameter, Kotlin provides the implicit name it — equivalent to naming the parameter x in a trivial JavaScript arrow. Kotlin lambdas passed as the last argument can be moved outside the parentheses (trailing lambda syntax), which is why numbers.map { it * 2 } works without explicit parentheses around the lambda.Function types — (Int) -> Boolean
function applyTwice(fn, value) {
return fn(fn(value));
}
const triple = x => x * 3;
console.log(applyTwice(triple, 2)); // 18
// JavaScript: no type enforcement on fn
try {
applyTwice("not a function", 2);
} catch (error) {
console.log("TypeError: fn is not a function");
} fun applyTwice(operation: (Int) -> Int, value: Int): Int =
operation(operation(value))
fun main() {
val triple = { x: Int -> x * 3 }
println(applyTwice(triple, 2)) // 18
// Kotlin: passing wrong type is a compile error:
// applyTwice("not a function", 2) // ❌ compile error
} Kotlin function types are written as
(ParameterType) -> ReturnType — for example, (Int) -> Int for a function that takes an Int and returns an Int. A (String, Int) -> Boolean function type can only be assigned a compatible lambda, catching mismatches at compile time. JavaScript treats functions as untyped values; TypeScript brings optional typing, but only Kotlin enforces it at every call site without opt-in.Closures — lambdas capture surrounding variables
function makeCounter(start = 0) {
let count = start;
return {
increment: () => ++count,
decrement: () => --count,
value: () => count,
};
}
const counter = makeCounter(10);
console.log(counter.increment()); // 11
console.log(counter.increment()); // 12
console.log(counter.decrement()); // 11 fun makeCounter(start: Int = 0): Map<String, () -> Int> {
var count = start
return mapOf(
"increment" to { ++count },
"decrement" to { --count },
"value" to { count },
)
}
fun main() {
val counter = makeCounter(10)
println(counter["increment"]!!()) // 11
println(counter["increment"]!!()) // 12
println(counter["decrement"]!!()) // 11
} Kotlin closures capture and modify
var variables from the enclosing scope, just like JavaScript closures capture let variables. Kotlin wraps captured mutable variables in a Ref object under the hood to make mutation work across the closure boundary. A captured val is read-only in the closure, just as a captured const cannot be reassigned in a JavaScript arrow function.Method chaining with collection operations
const employees = [
{ name: "Alice", department: "Engineering", salary: 95000 },
{ name: "Bob", department: "Marketing", salary: 75000 },
{ name: "Carol", department: "Engineering", salary: 105000 },
];
const result = employees
.filter(e => e.department === "Engineering")
.map(e => ({ name: e.name, bonus: e.salary * 0.1 }))
.sort((a, b) => b.bonus - a.bonus);
result.forEach(e => console.log(`${e.name}: ${e.bonus}`)); data class Employee(val name: String, val department: String, val salary: Double)
data class Bonus(val name: String, val bonus: Double)
fun main() {
val employees = listOf(
Employee("Alice", "Engineering", 95000.0),
Employee("Bob", "Marketing", 75000.0),
Employee("Carol", "Engineering", 105000.0),
)
employees
.filter { it.department == "Engineering" }
.map { Bonus(it.name, it.salary * 0.1) }
.sortedByDescending { it.bonus }
.forEach { println("${it.name}: ${it.bonus}") }
} Kotlin collection chaining reads almost identically to JavaScript's array method chaining. The
sortedByDescending { } lambda replaces the verbose JavaScript sort((a, b) => b.value - a.value) comparator. The key difference is that Kotlin chains are type-safe throughout — the compiler knows the element type of every intermediate collection, catching mismatches at compile time. Kotlin also provides groupBy, partition, zip, chunked, and windowed, which have no direct JavaScript equivalents.Function references — :: operator
function isPositive(n) { return n > 0; }
// Pass a function reference directly:
const positives = [-1, 2, -3, 4].filter(isPositive);
console.log(positives);
// Method reference style (bound):
const words = ["hello", "world"];
console.log(words.map(w => w.toUpperCase())); fun isPositive(n: Int) = n > 0
fun main() {
// :: creates a function reference — pass the function, don't call it:
val positives = listOf(-1, 2, -3, 4).filter(::isPositive)
println(positives)
// Method references on types:
val words = listOf("hello", "world")
println(words.map(String::uppercase))
// Combining with lambdas:
val numbers = listOf("1", "2", "abc", "4")
println(numbers.mapNotNull(String::toIntOrNull))
} Kotlin's
::functionName creates a function reference that can be passed where a lambda is expected — equivalent to passing the function itself in JavaScript (array.filter(isPositive)) without calling it. String::uppercase creates a method reference, similar to JavaScript's str => str.toUpperCase() but more concise. mapNotNull applies the function and automatically drops any null results, which is common when using converting functions that can fail.Classes & Data Classes
Class declaration — primary constructor in the header
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
distanceTo(other) {
const dx = this.x - other.x;
const dy = this.y - other.y;
return Math.sqrt(dx * dx + dy * dy);
}
toString() { return `Point(${this.x}, ${this.y})`; }
}
const pointA = new Point(0, 0);
const pointB = new Point(3, 4);
console.log(pointA.distanceTo(pointB));
console.log(String(pointB)); import kotlin.math.sqrt
class Point(val x: Double, val y: Double) {
fun distanceTo(other: Point): Double {
val dx = x - other.x
val dy = y - other.y
return sqrt(dx * dx + dy * dy)
}
override fun toString() = "Point($x, $y)"
}
fun main() {
val pointA = Point(0.0, 0.0)
val pointB = Point(3.0, 4.0)
println(pointA.distanceTo(pointB))
println(pointB)
} Kotlin's primary constructor is declared inline in the class header —
class Point(val x: Double, val y: Double) declares the class, its constructor parameters, and its properties all at once. There is no this.x = x boilerplate. Methods that override parent class or interface methods require the override keyword (mandatory, unlike JavaScript where toString is silently redefined). The new keyword is not used — Point(0.0, 0.0) constructs an instance directly.Data classes — equals, toString, copy for free
class User {
constructor(name, email, age) {
this.name = name; this.email = email; this.age = age;
}
toString() { return `User { name: '${this.name}', age: ${this.age} }`; }
withAge(newAge) { return new User(this.name, this.email, newAge); }
}
const alice = new User("Alice", "alice@example.com", 30);
const older = alice.withAge(31);
console.log(alice.toString());
console.log(older.toString()); data class User(val name: String, val email: String, val age: Int)
fun main() {
val alice = User("Alice", "alice@example.com", 30)
val older = alice.copy(age = 31) // named copy — only change what you need
println(alice) // User(name=Alice, email=alice@example.com, age=30)
println(older) // User(name=Alice, email=alice@example.com, age=31)
// Structural equality — generated automatically:
val alice2 = User("Alice", "alice@example.com", 30)
println(alice == alice2) // true (value equality, not reference)
} Kotlin's
data class generates equals(), hashCode(), toString(), copy(), and component destructuring functions automatically from the constructor parameters. The generated toString() includes the class name and all property values. The copy() method creates a new instance with all properties copied, with optional named overrides — replacing manual withAge()-style builder methods. Structural equality (==) compares property values; reference equality is ===.Computed properties with get()
class Circle {
constructor(radius) { this.radius = radius; }
get area() { return Math.PI * this.radius ** 2; }
get circumference() { return 2 * Math.PI * this.radius; }
}
const circle = new Circle(5);
console.log(circle.area.toFixed(2));
console.log(circle.circumference.toFixed(2)); import kotlin.math.PI
class Circle(val radius: Double) {
val area: Double
get() = PI * radius * radius
val circumference: Double
get() = 2 * PI * radius
}
fun main() {
val circle = Circle(5.0)
println("%.2f".format(circle.area))
println("%.2f".format(circle.circumference))
} Kotlin property accessors use a
get() block directly inside the class body, mirroring JavaScript's get accessor() syntax. Computed properties are declared with val (no backing field) and a get() function body — they are recalculated on every access. Kotlin also supports var properties with both get() and set(value) accessors, and field is the identifier for the backing storage inside accessor bodies.Inheritance — classes are final by default
class Animal {
constructor(name) { this.name = name; }
speak() { return `${this.name} makes a sound.`; }
}
class Dog extends Animal {
speak() { return `${this.name} barks.`; }
}
const dog = new Dog("Rex");
console.log(dog.speak());
console.log(dog instanceof Animal); open class Animal(val name: String) {
open fun speak() = "$name makes a sound."
}
class Dog(name: String) : Animal(name) {
override fun speak() = "$name barks."
}
fun main() {
val dog = Dog("Rex")
println(dog.speak())
println(dog is Animal) // true — 'is' replaces instanceof
} Kotlin classes are
final by default — they cannot be subclassed unless explicitly marked open. This is the opposite of JavaScript (and Java), where any class can be extended. Methods must also be open to be overridable, and override is mandatory on the subclass. The : Animal(name) syntax specifies the superclass and calls its constructor simultaneously. This design encourages composition over inheritance and prevents unintended subclassing.Interfaces with default implementations
// JavaScript has no interfaces — this is a TypeScript-only concept:
// interface Drawable { draw(): void; area(): number; }
class Rectangle {
constructor(width, height) { this.width = width; this.height = height; }
draw() { console.log(`Drawing rectangle ${this.width}x${this.height}`); }
area() { return this.width * this.height; }
}
const rectangle = new Rectangle(4, 6);
rectangle.draw();
console.log(rectangle.area()); interface Drawable {
fun draw()
fun area(): Double
fun describe() = "A shape with area ${area()}" // default implementation
}
class Rectangle(val width: Double, val height: Double) : Drawable {
override fun draw() = println("Drawing rectangle ${width}x${height}")
override fun area() = width * height
}
fun main() {
val rectangle = Rectangle(4.0, 6.0)
rectangle.draw()
println(rectangle.area())
println(rectangle.describe()) // uses default implementation
} Kotlin interfaces can contain method bodies (default implementations), making them more powerful than TypeScript's pure-declaration interfaces. A class implements interfaces using the same
: syntax as inheritance, and can implement multiple interfaces simultaneously. Unlike abstract classes, interfaces cannot hold state (no backing fields). When two implemented interfaces have conflicting default methods, the compiler requires an explicit override to disambiguate.Extension Functions
Extension functions — add methods without subclassing
// JavaScript option 1: standalone utility function
function pluralize(word, count) {
return count === 1 ? word : `${word}s`;
}
console.log(pluralize("item", 1)); // item
console.log(pluralize("item", 5)); // items
// Option 2: monkey-patching (strongly discouraged)
String.prototype.shout = function() { return this.toUpperCase() + "!"; };
console.log("hello".shout()); // Extension function — adds .pluralize() to String, scoped to this file:
fun String.pluralize(count: Int) = if (count == 1) this else "${this}s"
// Extension property — adds .wordCount to String:
val String.wordCount: Int
get() = if (isBlank()) 0 else trim().split("""\s+""".toRegex()).size
fun main() {
println("item".pluralize(1)) // item
println("item".pluralize(5)) // items
val sentence = "Hello world from Kotlin"
println(sentence.wordCount) // 4
} Extension functions add methods to a type at the call site without modifying the class, subclassing, or using a wrapper. They look like regular method calls (
"item".pluralize(1)) but are resolved statically at compile time — unlike JavaScript's prototype monkey-patching, they cannot be overridden by subclasses and do not modify the actual class. Extension properties work the same way but look like property access. This is one of Kotlin's most celebrated features, used extensively throughout the standard library.Standard library as extension functions
const numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3];
console.log(Math.max(...numbers)); // 9
console.log(Math.min(...numbers)); // 1
const words = " hello world ".trim().split(" ");
console.log(words.length);
console.log(words.join(", ")); fun main() {
val numbers = listOf(3, 1, 4, 1, 5, 9, 2, 6, 5, 3)
println(numbers.max()) // 9
println(numbers.min()) // 1
println(numbers.average()) // 3.9
println(numbers.sum()) // 39
println(numbers.sorted())
val words = " hello world ".trim().split(" ")
println(words.size)
println(words.joinToString(separator = ", "))
} Kotlin's standard library adds many useful extension functions to built-in types —
max(), min(), average(), sum(), sorted(), chunked(), and many others are extension functions on List rather than standalone functions like JavaScript's Math.max(). The joinToString() extension is a flexible replacement for JavaScript's Array.join(), supporting custom separators, prefixes, suffixes, and per-element transformation.Extensions improve discoverability
// JavaScript: standalone utility functions require knowing the import
// import { parsePositiveInt } from './utils';
// parsePositiveInt("42")
function parsePositiveInt(text) {
const value = parseInt(text, 10);
if (isNaN(value) || value <= 0) throw new Error(`"${text}" is not a positive integer`);
return value;
}
console.log(parsePositiveInt("42")); fun String.toPositiveInt(): Int {
val value = toInt()
require(value > 0) { "\"$this\" is not a positive integer" }
return value
}
fun String.toPositiveIntOrNull(): Int? {
val value = toIntOrNull() ?: return null
return if (value > 0) value else null
}
fun main() {
println("42".toPositiveInt()) // 42
println("-5".toPositiveIntOrNull()) // null
println("abc".toPositiveIntOrNull()) // null
} Extension functions appear in IDE autocomplete alongside the type's built-in methods —
"42".toPositiveInt() is discovered the same way as "42".toInt(). This is a significant discoverability advantage over JavaScript's module-exported utility functions, where callers must know which import to use. Extension functions defined in a file are available everywhere that file is imported; those defined inside a class or object are scoped only to that scope.Generic extension functions
function secondOrNull(array) {
return array.length >= 2 ? array[1] : null;
}
console.log(secondOrNull([10, 20, 30])); // 20
console.log(secondOrNull([42])); // null
console.log(secondOrNull(["a", "b"])); // b fun <T> List<T>.secondOrNull(): T? = if (size >= 2) this[1] else null
fun main() {
println(listOf(10, 20, 30).secondOrNull()) // 20
println(listOf(42).secondOrNull()) // null
println(listOf("a", "b", "c").secondOrNull()) // b
println(emptyList<Int>().secondOrNull()) // null
} Extension functions can be generic —
fun <T> List<T>.secondOrNull(): T? adds secondOrNull() to any List, regardless of element type. The type parameter T is inferred from the list's element type at the call site. This is more powerful than JavaScript's duck-typed utility functions because the compiler verifies type consistency through the entire call chain at compile time.Objects & Companion Objects
object — built-in singleton
// JavaScript singleton pattern using a module-level object:
const Logger = {
level: "info",
log(message) { console.log(`[${this.level.toUpperCase()}] ${message}`); },
setLevel(newLevel) { this.level = newLevel; },
};
Logger.log("Starting up");
Logger.setLevel("debug");
Logger.log("Debug mode active"); object Logger {
var logLevel = "info"
fun log(message: String) {
println("[${logLevel.uppercase()}] $message")
}
fun setLevel(newLevel: String) {
logLevel = newLevel
}
}
fun main() {
Logger.log("Starting up")
Logger.setLevel("debug")
Logger.log("Debug mode active")
} Kotlin's
object declaration creates a singleton — exactly one instance, created lazily on first access and shared throughout the application. It replaces the module-level object pattern common in JavaScript. Unlike JavaScript objects, Kotlin object declarations can implement interfaces and extend classes, and the singleton is thread-safe without any synchronization code.Companion objects — Kotlin's static methods
class User {
constructor(id, name) { this.id = id; this.name = name; }
static create(name) {
return new User(Math.floor(Math.random() * 10000), name);
}
static guest() {
return new User(0, "Guest");
}
}
const user = User.create("Alice");
const guest = User.guest();
console.log(user.name);
console.log(guest.name); class User(val identifier: Int, val name: String) {
companion object {
fun create(name: String) = User((0..9999).random(), name)
fun guest() = User(0, "Guest")
}
}
fun main() {
val user = User.create("Alice")
val guest = User.guest()
println(user.name) // Alice
println(guest.name) // Guest
} A companion object lives inside the class and is accessed using the class name (
User.create()), making it look like Java or JavaScript static methods. Unlike JavaScript's static methods (which are just properties on the constructor function), a companion object is a real singleton object instance that can implement interfaces — enabling the factory method pattern to be formalized. A class can have only one companion object.Object expressions — anonymous interface implementations
// JavaScript: anonymous object literal for one-off behavior
const listener = {
onClick(x, y) { console.log(`Clicked at ${x}, ${y}`); },
onKeyPress(key) { console.log(`Key pressed: ${key}`); },
};
listener.onClick(10, 20);
listener.onKeyPress("Enter"); interface EventListener {
fun onClick(x: Int, y: Int)
fun onKeyPress(key: String)
}
fun registerListener(listener: EventListener) {
listener.onClick(10, 20)
listener.onKeyPress("Enter")
}
fun main() {
registerListener(object : EventListener {
override fun onClick(x: Int, y: Int) =
println("Clicked at $x, $y")
override fun onKeyPress(key: String) =
println("Key pressed: $key")
})
} Kotlin object expressions create anonymous instances of a class or interface on the fly — analogous to JavaScript's object literal, but typed. The
object : Interface { ... } syntax creates a one-off implementation without giving it a name. Object expressions can capture variables from their enclosing scope (closures), just like JavaScript object methods can refer to surrounding variables. This pattern is commonly used for event handlers, callbacks, and strategy objects.Enum classes
// JavaScript enum pattern — frozen object:
const Direction = Object.freeze({
NORTH: "NORTH", SOUTH: "SOUTH", EAST: "EAST", WEST: "WEST",
});
function opposite(direction) {
const map = { NORTH: "SOUTH", SOUTH: "NORTH", EAST: "WEST", WEST: "EAST" };
return map[direction];
}
console.log(opposite(Direction.NORTH)); // SOUTH enum class Direction {
NORTH, SOUTH, EAST, WEST;
fun opposite() = when (this) {
NORTH -> SOUTH
SOUTH -> NORTH
EAST -> WEST
WEST -> EAST
}
}
fun main() {
println(Direction.NORTH.opposite()) // SOUTH
println(Direction.NORTH.name) // NORTH (built-in)
println(Direction.NORTH.ordinal) // 0 (position)
println(Direction.entries.toList()) // [NORTH, SOUTH, EAST, WEST]
} Kotlin's
enum class provides a type-safe alternative to JavaScript's object-freeze pattern. Each enum constant is an instance of the enum class and can carry methods and state. The name and ordinal properties come built-in. When used in a when expression, the Kotlin compiler enforces exhaustiveness — adding a new enum constant causes compile errors everywhere that when forgets to handle it. JavaScript has no built-in enum mechanism; TypeScript provides them, compiling down to JavaScript objects.Sealed Classes & Pattern Matching
Sealed classes — exhaustive type hierarchies
// JavaScript uses tagged unions / discriminated objects:
function area(shape) {
switch (shape.type) {
case "circle": return Math.PI * shape.radius ** 2;
case "rectangle": return shape.width * shape.height;
default: throw new Error("Unknown shape");
}
}
console.log(area({ type: "circle", radius: 5 }).toFixed(2));
console.log(area({ type: "rectangle", width: 4, height: 6 })); import kotlin.math.PI
sealed class Shape {
data class Circle(val radius: Double) : Shape()
data class Rectangle(val width: Double, val height: Double) : Shape()
}
fun area(shape: Shape): Double = when (shape) {
is Shape.Circle -> PI * shape.radius * shape.radius
is Shape.Rectangle -> shape.width * shape.height
} // no else needed — compiler knows all subclasses
fun main() {
println("%.2f".format(area(Shape.Circle(5.0))))
println(area(Shape.Rectangle(4.0, 6.0)))
} A
sealed class defines a closed set of subclasses — all must be declared in the same package. The key benefit is exhaustiveness: when used in a when expression, the Kotlin compiler verifies that every subclass is handled and no else branch is needed. If a new Shape subclass is added, the compiler reports every when that forgot to handle it. This catches entire categories of bugs at compile time that JavaScript's string-discriminated objects miss at runtime.Smart casts inside when branches
function describe(shape) {
if (shape.type === "circle") {
const { radius } = shape; // manual destructuring
return `Circle with radius ${radius}`;
} else if (shape.type === "rectangle") {
const { width, height } = shape;
return `Rectangle ${width}x${height}`;
}
return "Unknown shape";
}
console.log(describe({ type: "circle", radius: 7 }));
console.log(describe({ type: "rectangle", width: 3, height: 5 })); sealed class Shape2 {
data class Circle(val radius: Double) : Shape2()
data class Rectangle(val width: Double, val height: Double) : Shape2()
}
fun describe(shape: Shape2) = when (shape) {
is Shape2.Circle -> "Circle with radius ${shape.radius}"
is Shape2.Rectangle -> "Rectangle ${shape.width}x${shape.height}"
}
fun main() {
println(describe(Shape2.Circle(7.0)))
println(describe(Shape2.Rectangle(3.0, 5.0)))
} Inside a
when branch guarded by is Shape.Circle, Kotlin automatically smart-casts shape to Shape.Circle, giving direct access to shape.radius without any explicit cast or destructuring step. This is equivalent to JavaScript's const { radius } = shape but happens automatically via the type system. Smart casts are a broader Kotlin feature that applies to any if or when type check.Destructuring data classes
const person = { name: "Alice", age: 30, role: "admin" };
const { name, age, role } = person;
console.log(`${name}, age ${age}, role: ${role}`);
const people = [
{ name: "Alice", age: 30 },
{ name: "Bob", age: 25 },
];
people.forEach(({ name, age }) => console.log(`${name}: ${age}`)); data class Person(val name: String, val age: Int, val role: String)
fun main() {
val person = Person("Alice", 30, "admin")
val (name, age, role) = person // positional component destructuring
println("$name, age $age, role: $role")
val people = listOf(
Person("Alice", 30, "admin"),
Person("Bob", 25, "user"),
)
people.forEach { (personName, personAge) ->
println("$personName: $personAge")
}
} Kotlin data classes support positional destructuring using
val (a, b, c) = instance, which calls the generated component1(), component2(), component3() functions in order. This corresponds to JavaScript's object destructuring but is positional (constructor parameter order) rather than name-based. Kotlin also supports destructuring inside lambda parameters — people.forEach { (name, age) -> } works analogously to JavaScript's forEach(({ name, age }) => {}).Result<T> — typed success or failure
function safeParseJson(text) {
try {
return { ok: true, value: JSON.parse(text) };
} catch (error) {
return { ok: false, error: error.message };
}
}
const result = safeParseJson('{"name":"Alice"}');
if (result.ok) {
console.log(result.value.name);
} else {
console.log("Parse error:", result.error);
} fun safeDivide(dividend: Int, divisor: Int): Result<Double> =
if (divisor == 0) Result.failure(ArithmeticException("Division by zero"))
else Result.success(dividend.toDouble() / divisor)
fun main() {
val good = safeDivide(10, 3)
val bad = safeDivide(10, 0)
good.onSuccess { println("Result: $it") }
.onFailure { println("Error: ${it.message}") }
bad.onSuccess { println("Result: $it") }
.onFailure { println("Error: ${it.message}") }
val message = good.fold(
onSuccess = { "Got $it" },
onFailure = { "Failed: ${it.message}" },
)
println(message)
} Kotlin's built-in
Result<T> type wraps either a successful value or an exception, similar to JavaScript's { ok, value } / { ok, error } discriminated union pattern. The .onSuccess { } and .onFailure { } chainable callbacks read similarly to Promise .then() and .catch(). The .fold() function transforms both cases into a single value. For APIs where callers should always handle errors, Result<T> is more explicit than throwing exceptions.Error Handling
try/catch/finally — try is an expression
function parseTemperature(text) {
try {
const value = parseFloat(text);
if (isNaN(value)) throw new Error(`Invalid: "${text}"`);
if (value < -273.15) throw new RangeError("Below absolute zero");
return value;
} catch (error) {
if (error instanceof RangeError) {
console.log("Physical limit:", error.message);
} else {
console.log("Parse error:", error.message);
}
return null;
} finally {
console.log("done");
}
}
console.log(parseTemperature("21.5"));
console.log(parseTemperature("not-a-number")); fun parseTemperature(text: String): Double? {
return try {
val value = text.toDouble()
if (value < -273.15) throw IllegalArgumentException("Below absolute zero")
value
} catch (error: IllegalArgumentException) {
println("Physical limit: ${error.message}")
null
} catch (error: NumberFormatException) {
println("Parse error: ${error.message}")
null
} finally {
println("done")
}
}
fun main() {
println(parseTemperature("21.5"))
println(parseTemperature("not-a-number"))
} Kotlin's
try/catch/finally works the same way as JavaScript's, with two notable differences: catch clauses require a type annotation (catch (error: NumberFormatException)), and try is an expression — its value is the value of the last expression in the try block or the matched catch block. Multiple catch clauses target specific exception types, replacing JavaScript's single catch where you manually inspect error instanceof SomeError.Custom exceptions
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
function validateAge(age) {
if (age < 0 || age > 150) throw new ValidationError("age", "Must be 0–150");
return age;
}
try {
validateAge(200);
} catch (error) {
if (error instanceof ValidationError) {
console.log(`Validation on '${error.field}': ${error.message}`);
}
} class ValidationError(val field: String, message: String) : Exception(message)
fun validateAge(age: Int): Int {
if (age < 0 || age > 150) throw ValidationError("age", "Must be 0–150")
return age
}
fun main() {
try {
validateAge(200)
} catch (error: ValidationError) {
println("Validation on '${error.field}': ${error.message}")
}
} Kotlin custom exceptions extend
Exception using the : Exception(message) inheritance syntax, passing message up to the parent constructor. Unlike JavaScript's Error (where this.name must be set manually), Kotlin exception classes carry a proper class type that catch clauses can match precisely. All Kotlin exceptions are unchecked — unlike Java, there are no checked exceptions requiring throws declarations in method signatures.require and check — built-in precondition functions
function divide(dividend, divisor) {
if (typeof divisor !== "number") throw new TypeError("Must be a number");
if (divisor === 0) throw new RangeError("Divisor cannot be zero");
if (dividend < 0) throw new RangeError("Dividend must be non-negative");
return dividend / divisor;
}
console.log(divide(10, 3)); fun divide(dividend: Double, divisor: Double): Double {
require(divisor != 0.0) { "Divisor cannot be zero" }
require(dividend >= 0.0) { "Dividend must be non-negative" }
return dividend / divisor
}
fun main() {
println(divide(10.0, 3.0))
try {
divide(10.0, 0.0)
} catch (error: IllegalArgumentException) {
println("Error: ${error.message}")
}
} Kotlin provides three built-in precondition functions:
require(condition) { message } throws IllegalArgumentException when false (for validating function arguments); check(condition) { message } throws IllegalStateException (for validating object state); and error(message) unconditionally throws IllegalStateException. These replace the verbose if (!condition) throw ... pattern and are the idiomatic Kotlin approach to defensive programming.throw as an expression — Nothing type
// JavaScript: throw is a statement, not an expression
function getUser(identifier) {
const user = findUser(identifier);
if (!user) throw new Error(`User ${identifier} not found`);
return user;
}
function findUser(id) { return id === 1 ? { name: "Alice" } : null; }
try { getUser(99); } catch (error) { console.log(error.message); } data class UserRecord(val name: String)
fun findUser(identifier: Int): UserRecord? =
if (identifier == 1) UserRecord("Alice") else null
// throw is an expression — works with Elvis operator:
fun getUser(identifier: Int): UserRecord =
findUser(identifier) ?: throw NoSuchElementException("User $identifier not found")
fun main() {
println(getUser(1).name) // Alice
try {
getUser(99)
} catch (error: NoSuchElementException) {
println(error.message)
}
} In Kotlin,
throw is an expression of type Nothing — a special type that is a subtype of every other type, meaning it can appear anywhere a value is expected. This allows throw to be used with the Elvis operator: findUser(id) ?: throw NoSuchElementException("not found") returns the user if found, or throws if null. In JavaScript, throw is a statement and cannot appear in this position.