Skip to content
Commitwise
Menu
Programming

Variables, types and type inference

Declare constants and variables, let the compiler infer types (and know when to write them), and avoid the classic number and string traps, from integer division and overflow to parsing and locale formatting, in Swift, Kotlin, Java and TypeScript.

beginner

The core idea with one simple example.Step by step, with more examples and exercises.A cheat sheet: key APIs, gotchas and trade-offs.Internals, trade-offs and production pitfalls.

let average = total / count compiles, runs, and says the average of 7 and 8 is 7. Nothing crashed and nothing warned you. Most beginner bugs aren’t in clever code; they’re in the first line of it, where a variable got a type you didn’t intend. This lesson is about choosing those types on purpose, and knowing what the compiler chose for you.

New to some of these? Open what you need, or skip ahead.

Basics:What is a program, a compiler and a type?

A program is a list of instructions the computer runs in order. You write it as text (source code), and a compiler turns that text into something the computer can run, checking it for mistakes on the way.

A type says what kind of value something is: a whole number, a decimal, text, true/false. The compiler uses types to catch mistakes before the program runs, like adding a number to a list of names.

Basics:What are functions and printing?

A function is a named piece of code you can call: print("Hi") in Swift, println("Hi") in Kotlin, System.out.println("Hi") in Java, console.log("Hi") in TypeScript. All four show text in the console, which is how small examples show their result.

Text in quotes, like "Hi", is a string. A line starting with // is a comment: the compiler ignores it, it’s there for people.

Part Not started

Constants, variables and inferred types

Think of labeled jars in a kitchen. A jar has a name on the label (“sugar”), it holds one kind of thing (you don’t pour soup into the sugar jar), and some jars are sealed once filled while others get refilled every day.

In code, that jar is a variable: a name, a type (what kind of value it holds), and a value. A sealed jar is a constant: once it has a value, it keeps it.

All four languages are statically typed (TypeScript at compile time only) with local type inference: the type of a declaration is inferred from its initializer, not from later uses. Declarations split on two axes: rebindable or not (var vs let/val/final/const) and the type, explicit or inferred. Neither axis says anything about whether the object is mutable; that’s covered in Value types vs reference types.

let name = "Ana"          // constant: can't be reassigned
var score = 0             // variable: can change
score += 10

let price: Double = 3     // the type written explicitly
val name = "Ana"          // constant: can't be reassigned
var score = 0             // variable: can change
score += 10

val price: Double = 3.0   // the type written explicitly
final String name = "Ana";   // constant: can't be reassigned
int score = 0;               // variable: can change
score += 10;

var city = "Novi Sad";       // local type inference (Java 10+)
const name = "Ana";       // constant: can't be reassigned
let score = 0;            // variable: can change
score += 10;

const price: number = 3;  // the type written explicitly
  • Use a constant by default (let, val, final, const). If you later try to change it, the compiler tells you, and you decide whether that change is really what you want.
  • Use a variable (var, or let in TypeScript) only for values that really change: a counter, a running total.
  • Type inference: when you write let name = "Ana", the compiler sees a string and gives name the type String. You don’t have to write it. Java needs the type written, except with var for local variables.

TypeScript note

TypeScript’s names are confusing at first: const is the constant, and let is the variable. The old var still exists but behaves differently (it ignores blocks); don’t use it.

When to write the type yourself

Inference works from the value on the right. When there’s no value, or the value could mean several types, write the type:

var names: [String] = []                // empty: nothing to infer from
let ratio: Double = 1                   // 1 alone would be an Int
let id: Int64 = 9_000_000_000           // a specific size
val names = mutableListOf<String>()     // empty: nothing to infer from
val ratio: Double = 1.0                 // Kotlin never turns 1 into a Double
val id: Long = 9_000_000_000            // a specific size
List<String> names = new ArrayList<>(); // empty: the type says what goes in
double ratio = 1;                       // Java widens int 1 to double
long id = 9_000_000_000L;               // L: the literal itself is a long
const names: string[] = [];             // empty: [] alone would be any[] or never[]
const ratio = 1;                        // all numbers are one type: number
const id = 9_000_000_000n;              // n: a bigint, for whole numbers above 2^53

Predict the output

Which type does the compiler give each of these? let a = 7, let b = 7.0, let c = 7 / 2

let a = 7
let b = 7.0
let c = 7 / 2
val a = 7
val b = 7.0
val c = 7 / 2
var a = 7;
var b = 7.0;
var c = 7 / 2;
const a = 7;
const b = 7.0;
const c = 7 / 2;

In your editor you can check what was inferred: Option-click a name in Xcode, hover in Android Studio or IntelliJ, hover in VS Code.

Scope: where a name exists

A variable exists from where it’s declared to the end of the block ({ … }) it’s in. A variable declared inside an if or a loop is gone after it, which is useful: it can’t be changed by accident later.

var total = 0
for price in [3, 5, 8] {
    let withTax = price * 12 / 10       // exists only inside the loop
    total += withTax
}
print(total)                            // 18
var total = 0
for (price in listOf(3, 5, 8)) {
    val withTax = price * 12 / 10       // exists only inside the loop
    total += withTax
}
println(total)                          // 18
int total = 0;
for (int price : List.of(3, 5, 8)) {
    int withTax = price * 12 / 10;      // exists only inside the loop
    total += withTax;
}
System.out.println(total);              // 18
let total = 0;
for (const price of [3, 5, 8]) {
  const withTax = Math.floor((price * 12) / 10);   // exists only inside the loop
  total += withTax;
}
console.log(total);                     // 18

(3 × 12 / 10 is 3, 5 × 12 / 10 is 6, 8 × 12 / 10 is 9: integer division drops the decimals, which is the topic of the next part.)

What inference does and doesn’t do

  • Local, forward-only. The type comes from the initializer. var x = 0 is an integer forever, even if you later assign a decimal (a compile error in Swift, Kotlin and Java; in TypeScript, let x = 0 is number, so a decimal fits).
  • Literals get defaults. Swift integer literals default to Int, float literals to Double, but a literal adapts to an expected type (let d: Double = 1 is fine). Kotlin and Java literals have fixed types (1 is Int/int; Java widens it on assignment, Kotlin doesn’t).
  • Widening the declared type. TypeScript infers const x = "a" as the literal type "a" and let x = "a" as string. Object literal properties widen ({ kind: "a" } gives kind: string) unless you use as const or satisfies.
  • Public APIs should state types. Inferred return types of public functions turn implementation changes into API changes. Kotlin’s explicit API mode and Swift library evolution both push you to write them.
Deep diveWhy Java's var stops at local variablesInterview favorite

Java 10 added var only for local variables (and lambda parameters in 11): never for fields, parameters or return types. The reasoning was that local inference can’t leak into a class’s contract, while field and signature inference would let a change in an initializer silently change an API and break callers in other files. Kotlin and Swift infer property and return types too, which is why both nudge you to annotate public declarations.

  • Constants by default (let, val, final, const); they prevent rebinding, not mutation of the object.
  • Inference is local and from the initializer. Annotate empty collections, literals that need a non-default type, and public API surfaces.
SwiftKotlinJavaTypeScript
Constant / variablelet / varval / varfinal / plainconst / let
Inference scopelocals, properties, closureslocals, properties, expression-bodied returnslocals only (var)everywhere; literal types for const
Integer literaladapts to context (Int default)Int (or Long if too big)int (L for long)number (n for bigint)

Check this part

  1. Which declares a value that can't be reassigned?

  2. When must you write the type yourself?

  3. let/val/final on a variable holding a list means…

  4. Why annotate the return types of public functions even when inference works?

Use a constant (let, val, final, const) by default and a variable (var, let in TypeScript) only when the value must change. The compiler infers types from values; write them for empty collections, ambiguous literals and public APIs.

Part Not started

Numbers: whole, decimal and too big

Computers store whole numbers and decimal numbers differently. Whole numbers (integers) are exact but have a maximum size. Decimal numbers (floating point, Double) can be huge or tiny, but they’re stored in binary and are usually a tiny bit off. Most number bugs come from forgetting one of those two facts.

Swift, Kotlin and Java have fixed-width two’s-complement integers and IEEE 754 binary floating point; TypeScript has only IEEE 754 doubles (plus bigint). The differences that matter: integer division truncates toward zero; overflow traps in Swift, wraps silently on the JVM, and loses precision past 2⁵³ in TypeScript; implicit numeric conversions exist only in Java (widening) and TypeScript (everything is one type), while Swift and Kotlin require explicit conversion.

Integer division

Spot the bugThe average rating that's always a whole number

Tap the line with the bug.

func averageRating(_ ratings: [Int]) -> Double {
    let total = ratings.reduce(0, +)
    let average = total / ratings.count
    return Double(average)
}
fun averageRating(ratings: List<Int>): Double {
    val total = ratings.sum()
    val average = total / ratings.size
    return average.toDouble()
}
double averageRating(List<Integer> ratings) {
    int total = ratings.stream().mapToInt(Integer::intValue).sum();
    double average = total / ratings.size();
    return average;
}
function averageRating(ratings: number[]): number {
  const total = ratings.reduce((a, b) => a + b, 0);
  const average = Math.trunc(total / ratings.length);
  return average;
}

Limits and rounding

print(0.1 + 0.2)              // 0.30000000000000004
print(0.1 + 0.2 == 0.3)       // false
print(Int.max)                // 9223372036854775807
println(0.1 + 0.2)            // 0.30000000000000004
println(0.1 + 0.2 == 0.3)     // false
println(Int.MAX_VALUE)        // 2147483647
System.out.println(0.1 + 0.2);          // 0.30000000000000004
System.out.println(0.1 + 0.2 == 0.3);   // false
System.out.println(Integer.MAX_VALUE);  // 2147483647
console.log(0.1 + 0.2);                  // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3);          // false
console.log(Number.MAX_SAFE_INTEGER);    // 9007199254740991
  • Decimals are slightly off. 0.1 can’t be stored exactly in binary, just as 1/3 can’t be written exactly in decimal. Never compare decimals with ==; check whether they’re close enough. And never store money as a Double: use whole cents in an integer, or a decimal type (Decimal in Swift, BigDecimal in Kotlin and Java).
  • Integers have a maximum. Kotlin’s Int and Java’s int stop at about 2.1 billion; Swift’s Int and Kotlin/Java Long at about 9.2 quintillion. Going past the maximum crashes in Swift and silently wraps around to a negative number in Kotlin and Java. TypeScript’s number is exact for whole numbers only up to about 9 quadrillion.

Predict the output

A shop computes change from a 1.10 EUR coin payment for a 1.00 EUR item. What does it print?

let paid = 1.10
let price = 1.00
print(paid - price)
val paid = 1.10
val price = 1.00
println(paid - price)
double paid = 1.10;
double price = 1.00;
System.out.println(paid - price);
const paid = 1.10;
const price = 1.00;
console.log(paid - price);

Converting between number types

let count = 3
let average = 10.0 / Double(count)    // Swift never converts by itself
let rounded = Int(average.rounded())  // 3
let truncated = Int(3.9)              // 3: Int() cuts off the decimals
val count = 3
val average = 10.0 / count            // Int is converted for this division
val rounded = average.roundToInt()    // 3 (import kotlin.math.roundToInt)
val truncated = 3.9.toInt()           // 3: toInt() cuts off the decimals
int count = 3;
double average = 10.0 / count;        // int is widened to double
long rounded = Math.round(average);   // 3
int truncated = (int) 3.9;            // 3: the cast cuts off the decimals
const count = 3;
const average = 10 / count;            // 3.3333333333333335
const rounded = Math.round(average);   // 3
const truncated = Math.trunc(3.9);     // 3

Converting a decimal to a whole number cuts off the decimals (3.9 becomes 3). If you want normal rounding, round first.

Overflow before widening

Predict the output

Microseconds in a day. What does this print?

let micros = 24 * 60 * 60 * 1000 * 1000
print(micros)
val micros = 24 * 60 * 60 * 1000 * 1000
println(micros)
long micros = 24 * 60 * 60 * 1000 * 1000;
System.out.println(micros);
const micros = 24 * 60 * 60 * 1000 * 1000;
console.log(micros);

Production rules for numbers

  • Money: integer minor units or Decimal/BigDecimal with an explicit RoundingMode; on the JVM, BigDecimal("1.10"), never BigDecimal(1.10) (which captures the binary error). Across APIs, send amounts as strings or integer minor units: JSON numbers are doubles in most clients.
  • IDs and timestamps: 64-bit IDs don’t fit in a JavaScript number (Twitter’s snowflake IDs famously broke JS clients). Send them as strings.
  • Detect overflow where it matters: Swift’s &+ makes wrapping explicit and addingReportingOverflow reports it; Java/Kotlin Math.addExact/multiplyExact throw instead of wrapping.
  • Float comparison: compare with a tolerance relative to the magnitude, and remember NaN != NaN (so a NaN in a set or as a map key behaves strangely).
  • Integer division truncates toward zero; convert an operand before dividing. Java’s double x = a / b divides in int first.
  • Overflow: Swift traps; Kotlin/Java wrap silently (the whole expression is computed in the operands’ type before widening: 24L * …); TypeScript loses precision past 2⁵³.
  • Money in minor units or Decimal/BigDecimal (string constructor, explicit rounding). 64-bit IDs as strings across JSON.
  • No implicit numeric conversion in Swift or Kotlin; Java widens implicitly; TypeScript has one number.
let avg = Double(total) / Double(count)
let (sum, overflow) = a.addingReportingOverflow(b)
let price = Decimal(string: "19.99")!
val avg = total.toDouble() / count
val micros = 24L * 60 * 60 * 1000 * 1000
val price = BigDecimal("19.99")
double avg = (double) total / count;
long micros = Math.multiplyExact(24L * 60 * 60, 1_000_000L);
BigDecimal price = new BigDecimal("19.99");
const avg = total / count;
const id = BigInt("9007199254740993");       // beyond 2^53: use bigint or a string
const priceCents = 1999;

Check this part

  1. Both total and count are Int. What is total / count when total is 9 and count is 2?Both total and count are int. What is total / count when total is 9 and count is 2?What is 9 / 2 in TypeScript?

  2. How should a shopping app store prices?

  3. What happens when a Swift Int addition overflows?val micros = 24 * 60 * 60 * 1000 * 1000 prints…long micros = 24 * 60 * 60 * 1000 * 1000; prints…What happens to an integer above Number.MAX_SAFE_INTEGER?

  4. Why send 64-bit IDs as strings in JSON?

Dividing two integers drops the decimals. Integers have limits: Swift crashes past them, Kotlin and Java wrap around silently. Decimals are approximate, so 0.1 + 0.2 isn't 0.3; store money as whole cents or a decimal type.

Part Not started

Text and conversions

Putting values into text

let name = "Ana"
let items = 3
print("Hi \(name), you have \(items) items")
val name = "Ana"
val items = 3
println("Hi $name, you have $items items")
String name = "Ana";
int items = 3;
System.out.println("Hi " + name + ", you have " + items + " items");
System.out.println("Hi %s, you have %d items".formatted(name, items));
const name = "Ana";
const items = 3;
console.log(`Hi ${name}, you have ${items} items`);

String interpolation puts values right inside the text: \(…) in Swift, $name or ${expression} in Kotlin, ${…} inside backticks in TypeScript. Java has no interpolation, so you join pieces with + or use formatted. Interpolation is easier to read and avoids a classic trap with +:

Predict the output

What does this print?

print("Total: " + 1 + 2)
println("Total: " + 1 + 2)
System.out.println("Total: " + 1 + 2);
console.log("Total: " + 1 + 2);

Turning text into numbers

Text from a form, a file or a URL is a string, even when it looks like a number. Converting it can fail, and each language tells you differently:

let age = Int("42")          // Optional(42)
let bad = Int("42 years")    // nil: you must handle "not a number"
if let age = Int(input) {
    print("Next year: \(age + 1)")
}
val age = "42".toIntOrNull()        // 42
val bad = "42 years".toIntOrNull()  // null: you must handle "not a number"
input.toIntOrNull()?.let { println("Next year: ${it + 1}") }
// "42 years".toInt() would throw NumberFormatException
int age = Integer.parseInt("42");   // 42
try {
    int bad = Integer.parseInt("42 years");
} catch (NumberFormatException e) {   // Java throws: catch it
    System.out.println("Not a number");
}
const age = Number("42");          // 42
const bad = Number("42 years");    // NaN: "not a number", still of type number
if (!Number.isNaN(Number(input))) { /* ... */ }
console.log(parseInt("42 years")); // 42: parseInt stops at the first non-digit

Swift and Kotlin (toIntOrNull) return “nothing” when the text isn’t a number, and the compiler makes you handle it; see Null safety. Java throws an exception you should catch. TypeScript is the sneakiest: Number returns NaN, a special number that means “not a number”, and parseInt happily reads the digits at the start and ignores the rest.

Exerciseeasy

Format a duration

A video player stores durations in seconds. Write formatDuration(seconds) that returns h:mm:ss, or m:ss when it’s shorter than an hour.

65     → "1:05"
3600   → "1:00:00"
3725   → "1:02:05"
9      → "0:09"
›

Integer division and the remainder (%) split the total: hours = seconds / 3600, minutes = (seconds % 3600) / 60, seconds = seconds % 60. Here integer division is exactly what you want.

›

Minutes and seconds need a leading zero when they’re below 10. Swift: String(format: "%02d", n); Kotlin: n.toString().padStart(2, '0'); Java: String.format("%02d", n); TypeScript: String(n).padStart(2, "0").

SolutionTry it yourself first
func formatDuration(_ seconds: Int) -> String {
    let h = seconds / 3600
    let m = seconds % 3600 / 60
    let s = seconds % 60
    let ss = String(format: "%02d", s)
    return h > 0 ? "\(h):\(String(format: "%02d", m)):\(ss)" : "\(m):\(ss)"
}
fun formatDuration(seconds: Int): String {
    val h = seconds / 3600
    val m = seconds % 3600 / 60
    val s = seconds % 60
    fun two(n: Int) = n.toString().padStart(2, '0')
    return if (h > 0) "$h:${two(m)}:${two(s)}" else "$m:${two(s)}"
}
String formatDuration(int seconds) {
    int h = seconds / 3600;
    int m = seconds % 3600 / 60;
    int s = seconds % 60;
    return h > 0 ? String.format("%d:%02d:%02d", h, m, s) : String.format("%d:%02d", m, s);
}
function formatDuration(seconds: number): string {
  const h = Math.floor(seconds / 3600);
  const m = Math.floor((seconds % 3600) / 60);
  const s = seconds % 60;
  const two = (n: number) => String(n).padStart(2, "0");
  return h > 0 ? `${h}:${two(m)}:${two(s)}` : `${m}:${two(s)}`;
}

3725 / 3600 is 1 hour; 3725 % 3600 is 125 seconds left, which is 2 minutes (125 / 60) and 5 seconds (3725 % 60). In TypeScript, Math.floor does the job integer division does in the other languages. Try a negative input or a very large one, and decide what the function should do with them.

Locale-dependent formatting

Predict the output

The device language is German. The app sends this string in a JSON request as the price. What does the server receive?

let price = 3.5
let body = String(format: "%.2f", price)
val price = 3.5
val body = String.format("%.2f", price)
double price = 3.5;
String body = String.format("%.2f", price);
const price = 3.5;
const body = price.toFixed(2);

The same class of bug hits toUpperCase() / lowercased() (Turkish dotless i: "TITLE".lowercase() with a Turkish default locale isn’t "title"), date formatting, and sorting. Rule: machine formats use a fixed locale (Locale.ROOT, en_US_POSIX, ISO 8601), human formats use the user’s.

Exercisemedium

Strict parsing for a CSV import

An admin uploads a CSV of products with a quantity column. Today the import uses the language’s most forgiving parser, and support has seen: "12 pcs" imported as 12 (TypeScript parseInt), an empty cell imported as 0 (Number("")), "1,000" as 1, and one import that crashed halfway on "N/A" (Java parseInt without a catch). Write parseQuantity that accepts only a plain non-negative whole number up to 1,000,000 (surrounding spaces allowed) and reports what was wrong otherwise, so the import can show every bad row at once.

›

Return a result type, not an exception or a magic number: a success with the value, or a failure with a reason. The import then collects failures per row.

›

Validate the text first (trim, then only digits, sensible length), then convert. Every language’s strict converter is fine once the text is known to be digits.

SolutionTry it yourself first
enum QuantityError: Error, Equatable { case empty, notANumber(String), outOfRange(Int) }

func parseQuantity(_ raw: String) -> Result<Int, QuantityError> {
    let text = raw.trimmingCharacters(in: .whitespaces)
    guard !text.isEmpty else { return .failure(.empty) }
    guard text.count <= 7, text.allSatisfy({ $0.isASCII && $0.isNumber }), let value = Int(text) else {
        return .failure(.notANumber(text))
    }
    return value <= 1_000_000 ? .success(value) : .failure(.outOfRange(value))
}
sealed interface QuantityResult {
    data class Ok(val value: Int) : QuantityResult
    data class Invalid(val reason: String) : QuantityResult
}

private val DIGITS = Regex("""\d{1,7}""")

fun parseQuantity(raw: String): QuantityResult {
    val text = raw.trim()
    if (text.isEmpty()) return QuantityResult.Invalid("empty")
    if (!DIGITS.matches(text)) return QuantityResult.Invalid("not a whole number: \"$text\"")
    val value = text.toInt()
    return if (value <= 1_000_000) QuantityResult.Ok(value) else QuantityResult.Invalid("over 1,000,000: $value")
}
sealed interface QuantityResult {
    record Ok(int value) implements QuantityResult {}
    record Invalid(String reason) implements QuantityResult {}
}

private static final Pattern DIGITS = Pattern.compile("[0-9]{1,7}");

static QuantityResult parseQuantity(String raw) {
    String text = raw.strip();
    if (text.isEmpty()) return new QuantityResult.Invalid("empty");
    if (!DIGITS.matcher(text).matches()) return new QuantityResult.Invalid("not a whole number: \"" + text + "\"");
    int value = Integer.parseInt(text);
    return value <= 1_000_000 ? new QuantityResult.Ok(value) : new QuantityResult.Invalid("over 1,000,000: " + value);
}
type QuantityResult = { ok: true; value: number } | { ok: false; reason: string };

function parseQuantity(raw: string): QuantityResult {
  const text = raw.trim();
  if (text === "") return { ok: false, reason: "empty" };
  if (!/^[0-9]{1,7}$/.test(text)) return { ok: false, reason: `not a whole number: "${text}"` };
  const value = Number(text);
  return value <= 1_000_000 ? { ok: true, value } : { ok: false, reason: `over 1,000,000: ${value}` };
}
  • Validate the text, then convert: [0-9] instead of \d or isNumber on purpose; Unicode-aware digit classes accept Arabic-Indic or full-width digits that your database and downstream code may not expect. (Kotlin’s \d in a JVM regex is ASCII-only by default.)
  • A result per row, not an exception: the import can report “row 18: not a whole number: “12 pcs"" for every bad row instead of failing on the first.
  • No silent defaults: empty isn’t 0, and “1,000” isn’t 1. If thousands separators must be accepted, accept them deliberately with a locale-aware parser, documented in the import format.
  • Interpolation over +: "a" + 1 + 2 is "a12" on the JVM and in TypeScript; Swift refuses String + Int.
  • Parsing: Swift Int(_:) → nil, Kotlin toIntOrNull() → null (toInt() throws), Java parseInt throws, TypeScript Number("") is 0, Number("x") is NaN, parseInt("12 pcs") is 12. Validate the text (ASCII digits, length) before converting.
  • Machine formats use a fixed locale (Locale.ROOT, en_US_POSIX, ISO 8601); String.format on the JVM uses the default locale. Watch case conversion (Turkish i) too.
let n = Int(text.trimmingCharacters(in: .whitespaces))     // nil on anything odd
let wire = String(format: "%.2f", price)                    // POSIX by default
val n = text.trim().toIntOrNull()
val wire = String.format(Locale.ROOT, "%.2f", price)
OptionalInt n = text.strip().matches("[0-9]{1,9}") ? OptionalInt.of(Integer.parseInt(text.strip())) : OptionalInt.empty();
String wire = String.format(Locale.ROOT, "%.2f", price);
const n = /^[0-9]{1,15}$/.test(text.trim()) ? Number(text.trim()) : undefined;
const wire = price.toFixed(2);                            // locale-independent

Check this part

  1. What does Int("42 years") return?What does "42 years".toIntOrNull() return?What does Integer.parseInt("42 years") do?What does Number("42 years") return?

  2. How do you put a value into a Swift string?How do you put a value into a Kotlin string?What does "Total: " + 1 + 2 produce in Java?What does "Total: " + 1 + 2 produce?

  3. A JVM app formats a price for a JSON request with String.format("%.2f", p). What breaks on a German device?

  4. What does Number("") return in TypeScript?

Build text with string interpolation instead of +. Turning text into a number can fail: Swift and Kotlin return nil/null, Java throws, TypeScript returns NaN or even 0. Formatting with a locale can change the decimal separator.

Key takeaways

  • Use constants (let, val, final, const) by default and variables only for values that change.
  • The compiler infers types from values; write the type for empty collections and when the value could mean several types.
  • Dividing two whole numbers drops the decimals; convert first. Never store money as a Double.
  • Text that looks like a number still has to be converted, and the conversion can fail.
  • Rebinding (let/val/final) and mutability are separate; annotate public types even when inference works.
  • Know your overflow model: Swift traps, the JVM wraps (the expression’s type decides, not the target’s), TypeScript loses precision past 2⁵³.
  • Money in minor units or decimal types; 64-bit IDs as strings in JSON.
  • Parse strictly, return results instead of magic values, and use a fixed locale for anything a machine reads.

Finished this page?Nice work, this page is complete!

Mark it as done to track your progress. It's saved in this browser, no account needed.

Its cards are now in your Bites review ⚡
Esc

Type to search the whole site.