Data Types #

Dart is a sound type system language — every expression has a type known to the compiler, and the compiler guarantees that type is always correct at runtime. No surprise ClassCastException like in Java or TypeError like in JavaScript. This advantage works because Dart has a cohesive type hierarchy: all types are objects, all objects inherit from Object, and its generics system enables reusable code without sacrificing type safety. This article covers each type from a practical angle — not just a list of definitions, but when to use them, the traps to avoid, and the methods you’ll need most often.

The Dart Type Hierarchy #

Before diving into each type, it’s important to understand the big picture. In Dart, all types — including int, bool, and String — are classes that inherit from Object. There’s no separate primitive type like in Java.

flowchart TD
    O["Object\n(all non-null types)"] --> N["num"]
    O --> S["String"]
    O --> B["bool"]
    O --> L["List<T>"]
    O --> St["Set<T>"]
    O --> M["Map<K,V>"]
    O --> Fn["Function"]
    O --> Other["...and more"]
    N --> I["int"]
    N --> D["double"]

    Null["Null\n(only null)"]

    style O fill:#4f86c6,color:#fff
    style Null fill:#888,color:#fff

One type that stands outside this hierarchy is Null — the type that has only one value: null. Because Dart uses sound null safety, Null can’t be assigned to any non-nullable type without the ? marker.

Two special types need to be understood differently from regular types:

TypePositionDescription
dynamicOutside the type systemDisables type checking — use only when truly forced
NeverSubtype of all typesMarks code that never completes (always throws, infinite loops)

int — Whole Numbers #

int in Dart represents whole numbers with arbitrary precision on the Dart VM (running on server/CLI), and 64-bit integers on platforms compiled to JavaScript. There’s no size limit like int32 or int64 on the Dart VM — numbers can be as big as available memory.

int umur = 25;
int populasiDunia = 8_100_000_000;  // underscore as a thousands separator
int suhu = -15;
int heksadesimal = 0xFF;            // 255 — hex literal
int biner = 0b1010;                 // 10 — binary literal
int oktal = 0o17;                   // 15 — octal literal

Important Methods and Properties #

int n = -42;

// Conversion
print(n.abs());           // 42 — absolute value
print(n.toDouble());      // -42.0
print(n.toString());      // '-42'
print(n.toRadixString(16)); // '-2a' — hex representation

// Checks
print(n.isNegative);      // true
print(n.isEven);          // true (42 is even)
print(n.isOdd);           // false

// Bounds
print(n.sign);            // -1 (negative), 0 (zero), 1 (positive)
print(n.clamp(-100, 0));  // -42 — clamp within the range [-100, 0]

// Bit operations
int a = 0b1010;  // 10
int b = 0b1100;  // 12
print(a & b);    // 8  (0b1000) — bitwise AND
print(a | b);    // 14 (0b1110) — bitwise OR
print(a ^ b);    // 6  (0b0110) — bitwise XOR
print(a << 1);   // 20 (0b10100) — left shift
print(a >> 1);   // 5  (0b0101)  — right shift

Parsing Strings to int #

// ANTI-PATTERN: assuming parsing always succeeds
int nilai = int.parse('abc'); // ✗ throws FormatException

// CORRECT: use tryParse for untrusted input
int? nilai = int.tryParse('42');    // 42
int? gagal = int.tryParse('abc');   // null — doesn't throw
int? hex = int.tryParse('FF', radix: 16); // 255

// Handle null with a fallback
int tampil = int.tryParse(inputPengguna) ?? 0;

double — Decimal Numbers #

double uses the IEEE 754 64-bit floating-point standard — the same standard used by almost all modern languages. This means double has about 15–17 significant decimal digits of precision, but it also has representational limitations you need to understand.

double pi = 3.14159265358979;
double suhu = -273.15;           // absolute zero
double ilmiah = 1.5e10;          // 15,000,000,000 — scientific notation
double kecil = 2.5e-4;           // 0.00025

// Special values
print(double.infinity);          // Infinity
print(double.negativeInfinity);  // -Infinity
print(double.nan);               // NaN (Not a Number)
print(double.maxFinite);         // ~1.8e308
print(double.minPositive);       // ~5e-324

The Floating-Point Trap #

This is one of the most common sources of bugs in every language that uses IEEE 754:

// ANTI-PATTERN: comparing doubles with == directly
double a = 0.1 + 0.2;
print(a == 0.3);           // false — floating-point isn't precise
print(a);                  // 0.30000000000000004

// CORRECT: use a tolerance (epsilon) for comparison
const double epsilon = 1e-10;
bool hampirSama = (a - 0.3).abs() < epsilon;
print(hampirSama);         // true
// ANTI-PATTERN: storing money as a double
double harga = 19999.99;
double qty = 3;
double total = harga * qty;
print(total);  // 59999.970000000005 — precision error

// CORRECT: use int (in cents/smallest unit) or the 'decimal' package
int hargaSen = 1999999;   // Rp 19,999.99 in cents
int qtySatuan = 3;
int totalSen = hargaSen * qtySatuan;
print('Rp ${(totalSen / 100).toStringAsFixed(2)}'); // Rp 59999.97

Important Methods and Properties #

double x = -3.7;

print(x.abs());              // 3.7
print(x.ceil());             // -3 — round up
print(x.floor());            // -4 — round down
print(x.round());            // -4 — round to nearest
print(x.truncate());         // -3 — drop the decimal part
print(x.toInt());            // -3 — convert to int (truncate)

// Display formatting
print(x.toStringAsFixed(2));      // '-3.70' — 2 decimal places
print(x.toStringAsPrecision(4));  // '-3.700' — 4 significant digits
print(x.toStringAsExponential(2)); // '-3.70e+0'

// Special checks
double y = 0.0 / 0.0;  // NaN
print(y.isNaN);         // true
print(y.isFinite);      // false
print(y.isInfinite);    // false

num — Numeric Supertype #

num is the abstract parent class of int and double. Use num when a function or variable needs to accept both without restricting to either one:

// A function that accepts both int and double
double hitungAkar(num nilai) {
  if (nilai < 0) throw ArgumentError('Value cannot be negative');
  return nilai.toDouble().sqrt(); // no sqrt() on num, convert first
}

num a = 10;    // int behind the scenes
num b = 10.5;  // double behind the scenes

print(a.runtimeType); // int
print(b.runtimeType); // double
// ANTI-PATTERN: using num when the specific type is already certain
num umur = 25;         // ✗ umur is definitely int, declare it as int
num pi = 3.14159;      // ✗ pi is definitely double, declare it as double

// CORRECT: num only for genuinely generic cases
num hitungNilai(bool gunakanDesimal) {
  return gunakanDesimal ? 3.14 : 3; // returns a different type depending on the condition
}

bool — Boolean Values #

bool in Dart can only be true or false — there’s no truthy/falsy like in JavaScript or Python. Every conditional expression must explicitly produce a bool.

bool aktif = true;
bool selesai = false;

// Logical operators
bool a = true;
bool b = false;

print(a && b);   // false — AND
print(a || b);   // true  — OR
print(!a);       // false — NOT
print(a ^ b);    // true  — XOR (exclusive or)
// ANTI-PATTERN: using a non-bool value as a condition (not possible in Dart)
int stok = 5;
if (stok) { ... }           // ✗ compile error — int isn't bool
if ('teks') { ... }         // ✗ compile error — String isn't bool

// CORRECT: conditional expressions must explicitly produce a bool
if (stok > 0) { ... }       // ✓
if (nama.isNotEmpty) { ... } // ✓

Short-Circuit Evaluation #

Dart evaluates && and || in short-circuit fashion — the right operand is only evaluated if truly needed:

// Safe because obj?.method() is only called if the left condition is true
String? nama;
if (nama != null && nama.isNotEmpty) {
  // If nama is null, the right expression isn't evaluated
  print(nama.toUpperCase());
}

// Useful for default values with side effects
bool? izinGPS;
bool gunakanGPS = izinGPS ?? false; // false if null

String — Text #

String in Dart is an immutable sequence of Unicode characters. Every “modification” operation on a String (like + or replaceAll) always produces a new String object — the original object never changes.

String salam = 'Hello, Dart!';
String multiline = '''
  First line
  Second line
  Third line
''';

// Single and double quotes are equivalent
String a = 'Dart';
String b = "Dart";
print(a == b); // true

String Interpolation #

Interpolation is Dart’s idiomatic way to insert values into a String:

String nama = 'Budi';
int umur = 25;
double gaji = 8_500_000;

// Simple interpolation — $variable
print('Name: $nama');

// Expression interpolation — ${expression}
print('Age next year: ${umur + 1}');
print('Formatted salary: Rp ${gaji.toStringAsFixed(0)}');
print('Name length: ${nama.length} characters');

// Property access — no {} needed
print('Uppercase: ${nama.toUpperCase()}');
// ANTI-PATTERN: concatenating strings with + in a loop
String hasil = '';
List<String> kata = ['one', 'two', 'three', 'four', 'five'];
for (final k in kata) {
  hasil += k + ' ';  // ✗ creates a new String object every iteration — O(n²)
}

// CORRECT: use StringBuffer for concatenation in loops
final buffer = StringBuffer();
for (final k in kata) {
  buffer.write(k);
  buffer.write(' ');
}
String hasil = buffer.toString(); // ✓ O(n)

// Or use join for simple cases
String hasil = kata.join(' '); // ✓ the most concise

Most Commonly Used String Methods #

String teks = '  Hello, Dart!  ';

// Checks
print(teks.isEmpty);               // false
print(teks.isNotEmpty);            // true
print(teks.contains('Dart'));      // true
print(teks.startsWith('  Hello')); // true
print(teks.endsWith('!  '));       // true

// Transformations
print(teks.trim());                // 'Hello, Dart!'
print(teks.trimLeft());            // 'Hello, Dart!  '
print(teks.toUpperCase());         // '  HELLO, DART!  '
print(teks.toLowerCase());         // '  hello, dart!  '
print(teks.replaceAll(',', ';'));   // '  Hello; Dart!  '

// Extraction
print(teks.trim().substring(5));   // 'Dart!'
print(teks.trim().split(', '));     // ['Hello', 'Dart!']
print(teks.trim()[0]);             // 'H' — access a character by index

// Search
print(teks.indexOf('Dart'));        // 7
print(teks.trim().length);         // 11

// Padding
print('42'.padLeft(5));            // '   42'
print('42'.padLeft(5, '0'));       // '00042'
print('hi'.padRight(6, '.'));      // 'hi....'

Raw Strings #

Raw strings ignore escape sequences — useful for regex, Windows paths, and string templates:

// Regular string — backslash is interpreted as an escape
String path = 'C:\\Users\\Budi\\Documents'; // needs \\ escapes

// Raw string — backslash is read literally
String rawPath = r'C:\Users\Budi\Documents'; // no escaping needed

// Very useful for regex
RegExp emailRegex = RegExp(r'^[\w\.-]+@[\w\.-]+\.\w{2,}$');

List<T> — Ordered Collections #

List is a collection of elements accessed by zero-based integer index. The generic parameter T ensures all elements are the same type — the compiler will reject elements of the wrong type.

// Declaration
List<int> angka = [1, 2, 3, 4, 5];
List<String> nama = ['Budi', 'Siti', 'Andi'];
List<Map<String, dynamic>> produk = [];   // empty list

// Access
print(angka[0]);          // 1 — first index
print(angka.last);        // 5
print(angka.first);       // 1
print(angka.length);      // 5

// Modification
angka.add(6);             // add at the end
angka.insert(0, 0);       // insert at index 0
angka.remove(3);          // remove the value 3
angka.removeAt(0);        // remove at index 0
angka.removeLast();       // remove the last element

Functional List Methods #

List<int> angka = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// map — transform every element
List<int> kuadrat = angka.map((n) => n * n).toList();
// [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

// where — filter elements
List<int> genap = angka.where((n) => n % 2 == 0).toList();
// [2, 4, 6, 8, 10]

// reduce — aggregate into one value
int jumlah = angka.reduce((acc, n) => acc + n);   // 55
int maks = angka.reduce((a, b) => a > b ? a : b); // 10

// fold — like reduce but with an initial value
int jumlahFold = angka.fold(0, (acc, n) => acc + n); // 55

// any and every — condition checks
bool adaYangBesar = angka.any((n) => n > 8);         // true
bool semuaPositif = angka.every((n) => n > 0);       // true

// sort — in-place sorting
List<int> acak = [3, 1, 4, 1, 5, 9, 2, 6];
acak.sort();                                  // [1, 1, 2, 3, 4, 5, 6, 9]
acak.sort((a, b) => b.compareTo(a));          // descending: [9, 6, 5, 4, 3, 2, 1, 1]

// sublist — slicing
print(angka.sublist(2, 5));   // [3, 4, 5] — indices 2 through 4

// spread operator — combining lists
List<int> a = [1, 2, 3];
List<int> b = [4, 5, 6];
List<int> gabung = [...a, ...b]; // [1, 2, 3, 4, 5, 6]
// ANTI-PATTERN: accessing an index without checking the length
List<String> hasil = ambilData();
print(hasil[0]);  // ✗ crashes if the list is empty — RangeError

// CORRECT: check the length or use firstOrNull
if (hasil.isNotEmpty) {
  print(hasil.first); // ✓
}
// Or use firstOrNull from the collection package
print(hasil.firstOrNull); // null if empty, no crash

Set<T> — Unique Collections #

Set guarantees every element appears only once. Adding an element that already exists is silently ignored. Sets also support mathematical set operations directly.

Set<String> tag = {'dart', 'flutter', 'mobile'};

// Modification
tag.add('web');         // added
tag.add('dart');        // ignored — already exists
print(tag.length);      // 4

// Checks
print(tag.contains('flutter')); // true

// Set operations
Set<String> a = {'dart', 'flutter', 'mobile'};
Set<String> b = {'dart', 'web', 'desktop'};

print(a.intersection(b)); // {'dart'} — intersection
print(a.union(b));        // {'dart', 'flutter', 'mobile', 'web', 'desktop'} — union
print(a.difference(b));   // {'flutter', 'mobile'} — A minus B
print(b.difference(a));   // {'web', 'desktop'} — B minus A
// ANTI-PATTERN: using a List to check uniqueness — O(n) per operation
List<String> dilihat = [];
for (String item in data) {
  if (!dilihat.contains(item)) {  // ✗ O(n) every iteration — total O(n²)
    dilihat.add(item);
    prosesItem(item);
  }
}

// CORRECT: use a Set for uniqueness lookups — O(1) per operation
Set<String> dilihat = {};
for (String item in data) {
  if (dilihat.add(item)) {   // ✓ add() returns false if already present
    prosesItem(item);
  }
}

Map<K, V> — Key-Value Pairs #

Map stores data as key-value pairs. Keys must be unique; values may be duplicated. Accessing a value by key runs in O(1) on average.

Map<String, int> skor = {
  'Budi': 90,
  'Siti': 85,
  'Andi': 92,
};

// Access
print(skor['Budi']);          // 90
print(skor['Tono']);          // null — key doesn't exist, no crash
print(skor['Tono'] ?? 0);    // 0 — with a fallback

// Modification
skor['Rini'] = 88;            // add or update
skor.remove('Andi');          // remove an entry

// Checks
print(skor.containsKey('Budi'));    // true
print(skor.containsValue(85));      // true
print(skor.length);                 // 3

// Iteration
skor.forEach((nama, nilai) {
  print('$nama: $nilai');
});

// Transformation
Map<String, String> grade = skor.map(
  (nama, nilai) => MapEntry(nama, nilai >= 90 ? 'A' : 'B'),
);
// {'Budi': 'A', 'Siti': 'B', 'Rini': 'B'}

// Accessing keys, values, entries
print(skor.keys.toList());    // ['Budi', 'Siti', 'Rini']
print(skor.values.toList());  // [90, 85, 88]

Maps for Dynamic JSON Data #

// Map<String, dynamic> is the standard type for JSON data
Map<String, dynamic> pengguna = {
  'id': 'U001',
  'nama': 'Budi Santoso',
  'umur': 25,
  'aktif': true,
  'alamat': {
    'kota': 'Jakarta',
    'kodePos': '10110',
  },
};

// Accessing a nested map
String kota = (pengguna['alamat'] as Map<String, dynamic>)['kota'];
print(kota); // Jakarta

// ANTI-PATTERN: using Map<String, dynamic> as a permanent data model
// Maps have no type safety — every access can be the wrong type
String nama = pengguna['Nama'] as String; // ✗ typo 'Nama' instead of 'nama', crashes at runtime

// CORRECT: create a model class with fromJson() for frequently used data
class Pengguna {
  final String id;
  final String nama;
  final int umur;

  const Pengguna({required this.id, required this.nama, required this.umur});

  factory Pengguna.fromJson(Map<String, dynamic> json) {
    return Pengguna(
      id: json['id'] as String,
      nama: json['nama'] as String,
      umur: json['umur'] as int,
    );
  }
}

Special Types: dynamic, Object, and Never #

These three types are often confusing because they all seem to “hold anything”. The differences are fundamental:

// dynamic — type checking completely disabled
dynamic apa = 'teks';
apa = 42;               // ✓ change type — not checked by the compiler
apa.tidakAda();         // ✓ at compile time — ✗ crashes at runtime

// Object — all types are descendants of Object, but type-safe
Object sesuatu = 'teks';
sesuatu = 42;           // ✓ change type — checked by the compiler
// sesuatu.length;      // ✗ compile error — Object has no .length
(sesuatu as String).length; // ✓ explicit cast needed

// Object? — same as Object but nullable
Object? mungkinNull = null;  // ✓
// ANTI-PATTERN: using dynamic for "convenience"
dynamic data = ambilDariAPI();
print(data.nama);    // ✗ no guarantees — crashes if the API changes structure

// CORRECT: use the right type or cast with a check
Map<String, dynamic> data = ambilDariAPI();
if (data['nama'] is String) {
  print(data['nama'] as String); // ✓ type-safe
}

// Or better, create a model
final pengguna = Pengguna.fromJson(data);
print(pengguna.nama); // ✓ compile-time safe

Never is the type indicating a function never returns a normal value — it always throws or runs forever:

// Never as a return type — a function that always throws
Never lemparError(String pesan) {
  throw ArgumentError(pesan);
}

// Useful in exhaustive switches
String deskripsikan(Object value) {
  if (value is int) return 'number: $value';
  if (value is String) return 'text: $value';
  // The compiler knows this code can't be reached if all types are handled
  throw UnimplementedError('Unknown type: ${value.runtimeType}');
}

Generics and Type Parameters #

Generics allow classes and functions to work with many types without sacrificing type safety. List<T>, Set<T>, and Map<K,V> are generic examples already in Dart’s core.

// Generic function — works with any type
T pertama<T>(List<T> list) {
  if (list.isEmpty) throw StateError('List is empty');
  return list.first;
}

int angka = pertama([1, 2, 3]);      // T inferred as int
String kata = pertama(['a', 'b']);   // T inferred as String

// Generic class
class Pasangan<A, B> {
  final A pertama;
  final B kedua;
  const Pasangan(this.pertama, this.kedua);

  @override
  String toString() => '($pertama, $kedua)';
}

final p = Pasangan('Budi', 25);       // Pasangan<String, int>
final q = Pasangan(true, [1, 2, 3]);  // Pasangan<bool, List<int>>

// Generics with type bounds (bounded generics)
T nilaiMaks<T extends Comparable<T>>(T a, T b) {
  return a.compareTo(b) >= 0 ? a : b;
}

print(nilaiMaks(10, 20));        // 20
print(nilaiMaks('apple', 'zen')); // zen

Variance and Wildcards #

// Covariance — List<int> is not a subtype of List<num> in Dart
List<int> angka = [1, 2, 3];
// List<num> campur = angka; // ✗ error — type-safe, prevents hidden bugs

// But it can be explicitly converted
List<num> campur = angka.cast<num>();  // ✓ explicit cast

// Example of why this matters:
// If List<int> could be assigned to List<num>, you could add a double to an int list
// campur.add(3.14); // this would corrupt the int-typed angka list!

Converting Between Types #

Dart doesn’t convert types implicitly — every conversion must be done explicitly. This prevents subtle bugs common in JavaScript:

// int ↔ double
int i = 42;
double d = i.toDouble();   // ✓ 42.0
int kembali = d.toInt();   // ✓ 42 (truncate, not round)
int dibulatkan = d.round(); // ✓ for rounding

// num → int or double
num n = 3.7;
int bawah = n.floor().toInt();   // 3
int atas = n.ceil().toInt();     // 4

// String → int / double
int dariString = int.parse('42');
double dariStringD = double.parse('3.14');
int? amanParse = int.tryParse('not a number'); // null, doesn't throw

// int / double → String
String str = 42.toString();
String desimal = 3.14159.toStringAsFixed(2); // '3.14'

// bool → no implicit conversion from/to other types
// bool must always be an explicit boolean expression
bool aktif = (stok > 0);   // ✓ not: bool aktif = stok;
// ANTI-PATTERN: explicit cast without a type check
Object nilai = ambilData();
String teks = nilai as String;  // ✗ crashes with CastError if it's not a String

// CORRECT: check the type before casting, or use is for type promotion
if (nilai is String) {
  // Here Dart automatically treats 'nilai' as a String
  print(nilai.toUpperCase()); // ✓ no cast needed
}

// Or use as knowingly that it can throw
try {
  String teks = nilai as String;
  print(teks);
} on TypeError {
  print('Value is not a String: ${nilai.runtimeType}');
}

Summary #

  • All types are objects in Dart — there’s no separate primitive type. int, double, bool, and String are all classes inheriting from Object.
  • int for unbounded whole numbers on the Dart VM; use tryParse() instead of parse() for untrusted input.
  • double uses IEEE 754 — don’t use == to compare doubles, and avoid storing money as a double.
  • num is the supertype of int and double — use it only when a function genuinely needs to accept both.
  • bool is strict — there’s no truthy/falsy. Every condition must explicitly produce a bool.
  • String is immutable — every “modification” creates a new object. Use StringBuffer for concatenation in loops, and join() for joining lists.
  • List<T> for ordered collections with fast index access; Set<T> for unique collections with O(1) lookups; Map<K,V> for key-value pairs with O(1) access.
  • Avoid Map<String, dynamic> as a permanent data model — create a class with fromJson() for real type safety.
  • dynamic disables the type checker — all errors move from compile time to runtime. Use it only as a last resort.
  • Generics enable reusable code that stays type-safe — List<T>, Set<T>, Map<K,V> are everyday examples, but you can also write your own generic functions and classes.
  • Type conversion must be explicit — Dart doesn’t convert int to double automatically. Use toDouble(), toInt(), toString(), and parse() deliberately.

← Previous: Constants   Next: Operators →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact