← Back to blog
Dart Fundamentals · Part 3 of 10
June 23, 202611 min read

Dart's Built-in Types: Numbers, Strings, Booleans & Friends

DartFlutter

Dart's Built-in Types

Every language ships with a handful of types it understands natively — the ones you don't have to import or define yourself. In Dart these get special treatment: you can write them as literals right in your code (42, 'hello', true), and the language has syntax built around them.

The full cast: numbers (int, double), strings, booleans, records, functions, lists, sets, maps, runes, symbols, and null. The collections (lists, sets, maps), records, and functions are big enough to deserve their own posts — here we'll cover the everyday building blocks you'll reach for constantly.


Numbers

Dart has two number types, and a parent that sits above both.

int holds integer values up to 64 bits. double holds 64-bit floating-point numbers (the IEEE 754 standard, same as most languages). Both are subtypes of num, which is where the shared operators (+, -, *, /) and methods (abs(), ceil(), floor(), and friends) actually live.

Integers are just digits — and you can write hex with a 0x prefix:

var x = 1;
var hex = 0xDEADBEEF;

Doubles have a decimal point, or use scientific notation:

var y = 1.1;
var exponents = 1.42e5;

Here's a small thing that trips people up. If a variable is typed num, you can freely move between whole and decimal values:

num x = 1;
x += 2.5;

And if you type something as double but hand it a whole number, Dart quietly converts it for you:

double z = 1; // becomes 1.0

That conversion only goes one way, though. A whole number slots happily into a double, but a double will not auto-assign to an int — you'd silently lose the decimal part, so Dart makes you convert it explicitly:

double pi = 3.14;
pi = 10;         // ✅ fine — 10 becomes 10.0
// int x = 3.14; ❌ nope — that would drop the .14

Dividing numbers

One Dart-specific gotcha: the regular / operator always returns a double, even when both sides are integers. If you want a whole-number result, use ~/ (integer division), which drops the remainder:

22 / 7   // → 3.142857142857143  (double)
22 ~/ 7  // → 3                   (int — decimal dropped)

That ~/ is genuinely handy any time you need a count rather than an exact quotient.

Turning strings into numbers (and back)

This is the conversion you'll do all the time — parsing user input, reading JSON, formatting output:

var one = int.parse('1');
assert(one == 1);

var onePointOne = double.parse('1.1');
assert(onePointOne == 1.1);

String oneAsString = 1.toString();
assert(oneAsString == '1');

String piAsString = 3.14159.toStringAsFixed(2);
assert(piAsString == '3.14');

That toStringAsFixed(2) is a lifesaver for displaying prices and percentages without a wall of decimal places.

A couple of niceties

Integers get the usual bitwise operators if you need them:

assert((3 << 1) == 6); // shift left
assert((3 | 4) == 7);  // bitwise OR
assert((3 & 4) == 0);  // bitwise AND

And here's a genuinely lovely one — digit separators. Long numbers are painful to read, so Dart lets you drop underscores anywhere to group the digits. They're purely cosmetic; the compiler ignores them:

var n1 = 1_000_000;
var n2 = 0.000_000_000_01;
var n3 = 0x00_14_22_01_23_45;
var n4 = 555_123_4567;

1_000_000 reads as "one million" at a glance. 1000000 makes you count zeros. Small feature, big quality-of-life.


Strings

A Dart String is a sequence of UTF-16 code units. You can use single or double quotes — pick whichever saves you from escaping:

var s1 = 'Single quotes work well for string literals.';
var s2 = "Double quotes work just as well.";
var s3 = 'It\'s easy to escape the string delimiter.';
var s4 = "It's even easier to use the other delimiter.";

String interpolation

This is the feature you'll miss the most in other languages. Drop a $variable straight into a string, or ${anExpression} when you need more than a plain variable:

var s = 'string interpolation';

// just a variable
'Dart has $s, which is very handy.';

// an expression needs the curly braces
'That deserves all caps. ${s.toUpperCase()} is very handy!';

Rule of thumb: $name for a bare identifier, ${...} the moment you call a method or do anything more complex.

Multi-line strings

Need a block of text with line breaks? Wrap it in triple quotes (single or double):

var s1 = '''
You can create
multi-line strings like this one.
''';

var s2 = """This is also a
multi-line string.""";

Raw strings

Sometimes you don't want Dart interpreting backslashes and $ — like when you're writing a regex or a file path. Prefix the string with r and everything is taken literally:

var s = r'In a raw string, not even \n gets special treatment.';

Without the r, that \n would become a newline. With it, it stays as the two characters \ and n.

Joining strings

You can concatenate by just putting string literals next to each other, or with the + operator:

var s1 =
    'String '
    'concatenation'
    " works even over line breaks.";

var s2 = 'The + operator ' + 'works, as well.';

Strings and const

One nuance worth knowing: you can build a const string with interpolation, but only if every value you interpolate is itself a compile-time constant. Plain var values won't work, because their values aren't known until the program runs:

const aConstNum = 0;
const aConstBool = true;
const aConstString = 'a constant string';

var aNum = 0;
var aBool = true;
var aString = 'a string';

// ✅ every piece is const, so the whole thing can be const
const validConstString = '$aConstNum $aConstBool $aConstString';

// ❌ these aren't const, so this won't compile as a const
// const invalidConstString = '$aNum $aBool $aString';

Booleans

Booleans are refreshingly simple: there are exactly two values, true and false, both compile-time constants.

But here's the important part — Dart is strict about them. Unlike JavaScript, you can't lean on "truthy" or "falsy" values. An empty string isn't false, and 0 isn't false. If a condition wants a boolean, you give it an actual boolean by checking explicitly:

// check for an empty string
var fullName = '';
assert(fullName.isEmpty);

// check for zero
var hitPoints = 0;
assert(hitPoints == 0);

// check for null
var unicorn = null;
assert(unicorn == null);

// check for NaN
var iMeantToDoThis = 0 / 0;
assert(iMeantToDoThis.isNaN);

This feels stricter at first, but it kills a whole category of "wait, why did that branch run?" bugs. The condition says exactly what it means.


Runes and Grapheme Clusters

Here's where Unicode gets interesting. A Dart string is made of UTF-16 code units, but the character a human sees — like an emoji or a flag — might be built from several of those under the hood. These user-perceived characters are called grapheme clusters.

If you slice a string naively, you can chop one of these characters in half and get garbage. Watch what happens with the Danish flag emoji:

import 'package:characters/characters.dart';

void main() {
  var hi = 'Hi 🇩🇰';
  print(hi);
  print('The end of the string: ${hi.substring(hi.length - 1)}');
  print('The last character: ${hi.characters.last}');
}

The plain substring grabs only half of the flag and prints nonsense. But .characters.last — from the official characters package — correctly grabs the whole flag, because it works in grapheme clusters instead of raw code units.

The takeaway: whenever you're doing real work with user-facing text (counting characters, reversing strings, truncating), reach for the characters package rather than the raw String length and indexes.


Symbols

Symbols are the most niche type here, so don't sweat them early on. A Symbol represents an operator or identifier in a Dart program — a name, captured as a value. You write one with a # in front:

#radix
#bar

Why would you want this? Because identifier names normally get mangled when code is minified, but a symbol preserves the name. So they show up in APIs that need to refer to identifiers by name even after minification — reflection-style code being the classic example. You'll rarely write one yourself, but now you'll recognize that # when you see it.


Practice Challenges

Reading about types is one thing — using them is what makes them stick. Try each of these yourself first, then pop open the solution. There's almost always more than one right answer, so don't worry if yours looks a little different from mine.

Challenge 1 — Average of three numbers. Read three integers, compute their average as a double, and print it rounded to one decimal place (e.g. 3.7).

Show solution
void main() {
  int a = 4, b = 5, c = 2;
  double average = (a + b + c) / 3; // `/` always gives a double
  print(average.toStringAsFixed(1)); // → 3.7
}

The key insight: (a + b + c) is an int, but dividing with / returns a double, so average keeps the fractional part. toStringAsFixed(1) handles the rounding and formatting in one go.

Challenge 2 — Split a bill. You have a bill of 2500 and 7 people. Print how much each person pays as a whole number, and how much is left over.

Show solution
void main() {
  int total = 2500;
  int people = 7;

  int each = total ~/ people; // integer division → 357
  int leftover = total % people; // remainder → 1

  print('Each pays $each, with $leftover left over.');
  // → Each pays 357, with 1 left over.
}

This is exactly what ~/ (integer division) and % (remainder) are for — no decimals, just clean whole numbers.

Challenge 3 — Build a greeting. Given a name and an age, use string interpolation to print: Hi ALICE, next year you'll be 31! (name in uppercase, age plus one).

Show solution
void main() {
  var name = 'Alice';
  var age = 30;

  print('Hi ${name.toUpperCase()}, next year you\'ll be ${age + 1}!');
  // → Hi ALICE, next year you'll be 31!
}

Anything more than a bare variable — calling .toUpperCase(), doing age + 1 — needs the ${ ... } form. A plain $name would only get you the raw value.

Challenge 4 — A file path without the pain. Print this Windows-style path exactly as written, backslashes and all: C:\Users\dev\notes.txt. No double-escaping allowed.

Show solution
void main() {
  print(r'C:\Users\dev\notes.txt');
  // → C:\Users\dev\notes.txt
}

The r makes it a raw string, so \U, \d, and \n are left completely alone. Without it you'd have to write every backslash twice ('C:\\Users\\dev\\notes.txt').

Challenge 5 — Is it really empty? Given a String username, print "Please enter a name" only when it's empty — without relying on truthy/falsy tricks.

Show solution
void main() {
  String username = '';

  if (username.isEmpty) {
    print('Please enter a name');
  }
}

Dart booleans are strict — there's no "empty string is falsy" shortcut like in JavaScript. You ask the string directly with isEmpty, which reads clearly and says exactly what you mean.

Challenge 6 — Count emoji correctly. Given var text = 'Hello 👋🏽';, print the real last character (the waving hand), not a broken half of it.

Show solution
import 'package:characters/characters.dart';

void main() {
  var text = 'Hello 👋🏽';

  // ❌ naive — slices into the middle of the emoji
  // print(text.substring(text.length - 1));

  // ✅ grapheme-aware — grabs the whole character
  print(text.characters.last); // → 👋🏽
}

That emoji is several code units (a hand plus a skin-tone modifier), so substring would tear it apart. The characters package works in grapheme clusters — the characters humans actually see.


Wrapping Up

That's the everyday toolkit:

  • Numbersint and double, both under num; remember toStringAsFixed for display and the underscore digit separators for readability.
  • Strings — single or double quotes, $interpolation everywhere, triple quotes for multi-line, and r'...' for raw.
  • Booleans — only true/false, and no truthy/falsy shortcuts. Check explicitly.
  • Runes & grapheme clusters — use the characters package for anything user-facing, or emoji will betray you.
  • Symbols — a #name value, mostly for minification-safe identifier references.

Next up I'll cover the collection types — lists, sets, and maps — which is where Dart starts to get really fun.