- Hiểu Dart type system — static + sound (sau Dart 2.12).
- Phân biệt
var,final,const,dynamic,Object,Object?— khi nào dùng cái nào. - Sử dụng đúng built-in types:
int,double,num,String,bool,List,Set,Map. - Hiểu type inference vs type annotation — khi nào để Dart tự suy luận, khi nào ghi explicit.
- Sử dụng String interpolation, raw string, multi-line string thành thạo.
- Hiểu
nullkhông phải value bình thường — phải explicit qua nullable typeT?.
1. Static type system là gì?
"Type system" mô tả cách ngôn ngữ quản lý kiểu dữ liệu của biến/biểu thức. Có 4 trục chính mà mọi ngôn ngữ phân loại theo:
| Ngôn ngữ | Static / Dynamic | Strong / Weak | Sound / Unsound | Null safety |
|---|---|---|---|---|
| JavaScript | Dynamic | Weak (auto coerce) | N/A | Không |
| Python | Dynamic | Strong | N/A | Không |
| TypeScript | Static (gradual) | Strong | Unsound (do any) | Strict mode optional |
| Java | Static | Strong | Sound (mostly) | Không (có null bất kỳ đâu) |
| Dart 3 | Static | Strong | Sound | Sound, mặc định |
| Kotlin | Static | Strong | Sound | Sound, mặc định |
| Rust | Static | Strong | Sound | Không có null — dùng Option |
Định nghĩa nhanh:
- Static — type biết tại compile-time. Analyzer kiểm tra trước khi chạy. Ngược lại: dynamic (type biết runtime).
- Strong — không auto-convert.
1 + "1"ở Dart lỗi compile. Ở JavaScript trả"11"(auto coerce string). - Sound — type analyzer chứng minh được không lỗi runtime. TS có
anynên không sound. Dart không cóany(chỉ códynamicnhưng phải opt-in explicit) — sound. - Sound null safety — analyzer chứng minh được biến nullable hoặc non-nullable. Compile fail nếu vi phạm.
Khi type system sound, bạn có một guarantee mạnh: nếu code compile được, một class lỗi runtime (ví dụ NullPointerException) không thể xảy ra. Mọi lỗi đã bị bắt ở compile-time.
TypeScript "gradual typing" cho phép any escape — type check tại edge này, không tại edge kia.
Code TS compile vẫn có thể throw Cannot read property of undefined. Dart 3 không có lỗ hổng đó.
2. Khai báo biến — 4 cách
Dart cho 4 cách khai báo biến. Hiểu rõ khác biệt = code clean ngay từ đầu.
// 1. var — type inferred từ giá trị khởi tạo
var name = 'Việt'; // type: String
var age = 25; // type: int
// 2. final — gán 1 lần, immutable variable
final birthday = DateTime.now(); // OK — runtime value
// 3. const — compile-time constant
const pi = 3.14159; // OK — biết tại compile time
// const now = DateTime.now(); // Lỗi: runtime value
// 4. Type annotation explicit
int count = 0;
String message = 'hello';
List<int> numbers = [1, 2, 3];
Bảng quyết định:
| Cách | Reassign? | Compile-time? | Type cố định? | Khi nào dùng |
|---|---|---|---|---|
var x = 5 | Có | Không | Có (inferred) | Default cho biến local |
final x = 5 | Không | Không | Có (inferred) | Khi không cần reassign (đa số case) |
const x = 5 | Không | Có | Có (inferred) | Hằng số biết trước compile-time |
int x = 5 | Có | Không | Có (explicit) | Khi inference fail hoặc muốn ép rõ |
final
Lint prefer_final_locals khuyến khích dùng final mặc định cho biến local.
Lý do: immutable variable an toàn hơn, dễ đọc — khi reassign mới chuyển var.
Áp dụng cùng nguyên tắc với JS const > let.
final vs const — khác biệt quan trọng
Cả 2 đều immutable nhưng có 1 điểm khác chí mạng:
final now1 = DateTime.now(); // OK — runtime value
// const now2 = DateTime.now(); // ❌ Lỗi compile
final list1 = [1, 2, 3];
const list2 = [1, 2, 3];
list1.add(4); // OK — final chỉ khoá biến, không khoá nội dung
// list2.add(4); // ❌ Runtime error — const list là unmodifiable
final: biến gán 1 lần, giá trị có thể là runtime. Nội dung object (vd List) vẫn mutate được.
const: biến và toàn bộ giá trị phải biết tại compile-time. Nội dung immutable hoàn toàn.
const tạo canonical instance
Đây là feature đặc biệt của const:
const a = [1, 2, 3];
const b = [1, 2, 3];
print(identical(a, b)); // true — cùng instance trong memory!
var c = [1, 2, 3];
var d = [1, 2, 3];
print(identical(c, d)); // false — 2 instance khác nhau
Dart "canonicalize" const value — 2 const giống hệt nội dung dùng cùng một instance trong memory. Tiết kiệm bộ nhớ + cho phép compare bằng identical. Đặc biệt quan trọng cho Flutter widget — sẽ thấy ở sub-pillar Flutter.
3. Type inference — khi nào fail?
Dart analyzer infer type từ giá trị khởi tạo. Đa số trường hợp work tốt — nhưng có case fail:
var a = 5; // infer int — OK
var b = 3.14; // infer double — OK
var c = [1, 2, 3]; // infer List<int> — OK
var d = [1, 'a']; // infer List<Object> — vẫn OK nhưng có thể không phải ý bạn
var e; // ❌ infer dynamic — analyzer warn
Khi infer ra dynamic (mất type safety), nên annotate rõ:
int? value; // nullable int, default null
value = 5;
// value = 'a'; // ❌ analyzer bắt
var ≠ dynamicĐừng nhầm:
var x = 5; // infer int — TYPE CỐ ĐỊNH
x = 'a'; // ❌ Lỗi: String không gán được vào int
dynamic y = 5; // type dynamic — TẮT TYPE CHECK
y = 'a'; // ✅ OK — vì dynamic chấp nhận tất cả
var chỉ là shorthand cho "type rõ ràng, đừng bắt tôi viết". Không phải "dynamic".
4. Numeric types — int, double, num
Dart có 2 type số: int (số nguyên) và double (số thực — IEEE 754 64-bit).
Cả 2 cùng kế thừa num.
int count = 42;
double price = 19.99;
num any = 10; // num chấp nhận int hoặc double
any = 10.5; // OK
// int và double KHÔNG auto convert
int a = 3.0; // ❌ Lỗi compile
double b = 3; // ❌ Lỗi compile (Dart 2.1+ cho phép, Dart 3 strict)
// Convert thủ công
double c = 3.toDouble(); // 3.0
int d = 3.7.toInt(); // 3 (truncate)
int e = 3.7.round(); // 4 (round)
Parse string thành số
int n = int.parse('42'); // throws FormatException nếu fail
int? m = int.tryParse('42a'); // trả null thay throw, safer
double p = double.parse('3.14');
double? q = double.tryParse('abc'); // null
BigInt — số cực lớn
BigInt huge = BigInt.parse('12345678901234567890');
print(huge * huge); // không overflow như int 64-bit
int Dart là 64-bit signed (-9.2 × 10¹⁸ đến +9.2 × 10¹⁸). Vượt giới hạn → wrap-around hoặc dùng BigInt.
5. String — interpolation + multiline + raw
String literal
String a = 'single quote'; // idiomatic Dart
String b = "double quote"; // cũng OK
String c = "Cô ấy nói: 'Xin chào'"; // dùng double quote khi content có '
String d = 'It\'s OK'; // escape với \
String interpolation
final name = 'Việt';
final age = 25;
final hobbies = ['code', 'đọc'];
// $identifier — chỉ với identifier đơn
print('Tôi tên $name, $age tuổi');
// ${expression} — bất kỳ expression
print('Sang năm tôi ${age + 1} tuổi');
print('Tổng hobbies: ${hobbies.length}');
// Toán cũ Java-style — không cần
// 'Tôi tên ' + name + ', ' + age.toString() + ' tuổi' ← đừng viết thế
// JavaScript
const msg = `Tôi tên ${name}, ${age} tuổi`;
// Dart — gần giống, khác ở dấu mở
final msg = 'Tôi tên $name, $age tuổi';
JS dùng backtick + ${...}. Dart dùng quote thường + $name shorthand cho identifier.
Multi-line string
String html = '''
<div>
<h1>$title</h1>
</div>
''';
// Hoặc double quote triple
String json = """
{
"name": "$name",
"age": $age
}
""";
Raw string — không xử lý escape
// Normal — \n thành newline thực sự
String path1 = 'C:\\Users\\Việt'; // phải escape \
// Raw — \ giữ nguyên
String path2 = r'C:\Users\Việt'; // chữ r trước string
// Tiện cho regex
final emailRegex = RegExp(r'^[\w-]+@[\w-]+\.[\w]{2,}$');
String là immutable
final s = 'hello';
// s[0] = 'H'; // ❌ Không tồn tại operator []= cho String
final upper = s.toUpperCase(); // Trả string mới 'HELLO'
// Common method:
print(s.length); // 5
print(s.contains('ll')); // true
print(s.replaceAll('l', 'L')); // 'heLLo'
print(s.split('')); // ['h','e','l','l','o']
bool — strict, không có truthy
bool isOk = true;
bool done = false;
// Khác JS: không có truthy/falsy
if (1) { } // ❌ Lỗi compile — 1 không phải bool
if ('hello') { } // ❌ Lỗi compile
if (someList) { } // ❌ Lỗi compile
// Phải check explicit
if (someList.isNotEmpty) { } // ✅
if (count > 0) { } // ✅
if (name != null) { } // ✅
Nếu bạn viết JS lâu, thói quen if (someValue) để check truthy sẽ bị reject ngay ở Dart.
Lúc đầu khó chịu, sau quen → code rõ ràng hơn (không có magic coerce).
6. null và nullable type
Đây là điểm khác biệt lớn nhất Dart so với JS/Java/Python.
int a = null; // ❌ Lỗi compile — int là non-nullable
int? b = null; // ✅ — int? nullable
String? name; // ✅ — mặc định null vì nullable
String message; // ❓ — phải gán trước khi đọc
print(message); // ❌ Lỗi compile — non-nullable chưa gán
Quy tắc vàng:
T(không có?) — type non-nullable. Không bao giờ lànull.T?(có?) — type nullable. Có thể làThoặcnull.- Mọi biến non-nullable phải được gán (initializer hoặc trong constructor) trước khi đọc lần đầu.
Demo:
void greet(String? name) {
// Không gọi trực tiếp name.toUpperCase() — name có thể null
// name.toUpperCase(); // ❌ Lỗi compile
// 3 cách handle:
// (a) Check null trước → flow promotion
if (name != null) {
print(name.toUpperCase()); // OK — analyzer biết name không null trong block
}
// (b) Null-aware operator
print(name?.toUpperCase()); // Trả null nếu name null
// (c) Provide default
print((name ?? 'guest').toUpperCase());
}
Đây mới là basic. Chương 5 (Null Safety & Error Handling) sẽ deep dive 5 null-aware operator,
! non-null assertion, late keyword, definite assignment analysis, custom exception.
7. dynamic vs Object?
Khi cần "type bất kỳ", có 2 lựa chọn:
// dynamic — tắt type check (giống TS any)
dynamic a = 5;
a = 'hello';
a = [1, 2];
a.whateverMethod(); // Compile OK, RUNTIME crash nếu sai
// Object? — type cha của mọi type, type check vẫn hoạt động
Object? b = 5;
b = 'hello';
b = [1, 2];
// b.toUpperCase(); // ❌ Lỗi compile — Object? không có toUpperCase
// Phải cast hoặc check type:
if (b is String) {
print(b.toUpperCase()); // OK — b promoted thành String trong block
}
| Type | Chấp nhận giá trị? | Type check? | Use case |
|---|---|---|---|
dynamic | Bất kỳ | Tắt — runtime crash nếu sai | JSON parse, FFI, interop JS — escape hatch |
Object? | Bất kỳ kể cả null | Bật — phải cast hoặc is check | Default cho "tôi không biết type" |
Object | Bất kỳ trừ null | Bật | Khi chắc chắn non-null nhưng type bất kỳ |
dynamic
dynamic mất hết safety của Dart. Chỉ dùng khi không có cách khác (vd parse JSON
Map<String, dynamic>). Mặc định dùng Object? + is check.
8. Type promotion / Flow analysis
Dart analyzer "thông minh" — sau khi kiểm tra type, nó thu hẹp type trong block tiếp theo:
void describe(Object? value) {
if (value is String) {
// Trong block này, value được promoted thành String
print('String length: ${value.length}');
} else if (value is int) {
print('Int double: ${value * 2}');
} else if (value != null) {
// value promoted thành Object (loại null)
print('Other: ${value.toString()}');
}
}
Flow analysis cũng hoạt động với null check:
String? name = getName();
if (name != null) {
print(name.length); // OK — name promoted thành String
}
// Pattern early return cũng work:
if (name == null) return;
print(name.length); // OK — sau early return, name chắc chắn không null
Flow analysis chỉ work với local variable. Field của class bị invalidate khi gọi method bất kỳ (vì method có thể đổi field).
class User {
String? name;
void show() {
if (name != null) {
// name.length; // ❌ — analyzer không promote field
// Pattern: copy ra local
final n = name;
if (n != null) print(n.length); // OK
}
}
}
9. Collection types — sơ lược
3 collection cơ bản: List, Set, Map. Chỉ điểm qua — chương 6 sẽ deep dive.
// List — sequence ordered
List<int> numbers = [1, 2, 3];
var names = ['Việt', 'Anh', 'Phương']; // inferred List<String>
// Set — unique unordered
Set<String> tags = {'a', 'b', 'a'}; // {a, b}
// Map — key-value
Map<String, int> ages = {
'Việt': 25,
'Anh': 30,
};
print(numbers[0]); // 1
print(ages['Việt']); // 25
10. Type hierarchy Dart
Mọi type Dart thừa kế từ Object?. Sơ đồ chính:
11. Bài tập
5 cách khai báo biến
Khai báo 5 biến trong một file vars.dart: 1 dùng var, 1 final,
1 const, 1 explicit type, 1 nullable. Mỗi cái 1 dòng kèm comment giải thích.
Chạy dart analyze — không có warning.
💡 Gợi ý đáp án
void main() {
var name = 'Việt'; // var — inferred String
final age = DateTime.now().year - 2000; // final — runtime value OK
const pi = 3.14159; // const — compile-time
double price = 19.99; // explicit type
String? optionalLabel; // nullable, default null
print('$name, $age tuổi, pi=$pi, price=$price, label=$optionalLabel');
}
formatPrice với interpolation
Viết function String formatPrice(int amount, String currency) trả về string format
"1,500,000 VND" (có dấu phẩy ngăn cách hàng nghìn). Dùng interpolation + helper.
💡 Gợi ý đáp án
String formatPrice(int amount, String currency) {
final str = amount.toString();
final buf = StringBuffer();
for (var i = 0; i < str.length; i++) {
if (i > 0 && (str.length - i) % 3 == 0) buf.write(',');
buf.write(str[i]);
}
return '$buf $currency';
}
void main() {
print(formatPrice(1500000, 'VND')); // "1,500,000 VND"
print(formatPrice(99, 'USD')); // "99 USD"
}
Chuyên nghiệp hơn: dùng package intl với NumberFormat. Nhưng tự implement giúp hiểu loop + interpolation.
Compile error spotting
Cho biết dòng nào sau lỗi compile, vì sao:
(a) int x = 3.0;
(b) num y = 3;
(c) double z = 3;
(d) var w; w = 5;
(e) const t = DateTime.now();
(f) final list = [1, 2]; list.add(3);
(g) int? n = null; print(n.toString());
💡 Gợi ý đáp án
- (a) ❌ Lỗi — 3.0 là double, không gán được vào int (Dart strict).
- (b) ✅ OK — num chấp nhận cả int và double.
- (c) ❌ Lỗi — 3 là int literal, không gán được vào double (cần
3.0hoặc3.toDouble()). - (d) ❌ Warning —
var w;không có initializer → infer dynamic. Analyzer warn. - (e) ❌ Lỗi —
DateTime.now()là runtime value, không thể const. - (f) ✅ OK —
finalchỉ khoá biến (list reference), không khoá nội dung list. - (g) ❌ Lỗi —
n.toString()không safe trênint?. Phảin?.toString()hoặc check null.
Const canonical instance
Viết file demo: tạo 2 biến const list1 = [1, 2, 3] và const list2 = [1, 2, 3].
Gọi identical(list1, list2) — kết quả? Lặp lại với var thay const — kết quả?
Giải thích vì sao.
💡 Gợi ý đáp án
void main() {
const a = [1, 2, 3];
const b = [1, 2, 3];
print(identical(a, b)); // true
var c = [1, 2, 3];
var d = [1, 2, 3];
print(identical(c, d)); // false
}
const Dart "canonicalize" — 2 const cùng nội dung dùng cùng instance memory.
Tiết kiệm bộ nhớ + cho phép identity check.
var tạo instance riêng mỗi lần — 2 list khác địa chỉ memory dù cùng nội dung.
Đây là lý do Flutter khuyến khích const cho widget — tránh rebuild thừa khi 2 widget cùng config.
Refactor dynamic → explicit type
Cho đoạn code:
dynamic parseUser(dynamic json) {
return {
'name': json['name'],
'age': json['age'],
};
}
void main() {
dynamic user = parseUser({'name': 'Việt', 'age': 25});
print(user['name']);
}
Refactor để loại bỏ dynamic. Hint: dùng Map<String, dynamic> cho JSON (compromise), và type rõ ràng cho kết quả.
💡 Gợi ý đáp án
Map<String, Object?> parseUser(Map<String, dynamic> json) {
return {
'name': json['name'] as String?,
'age': json['age'] as int?,
};
}
void main() {
final user = parseUser({'name': 'Việt', 'age': 25});
print(user['name']); // type Object? — phải cast nếu dùng
}
Map<String, dynamic> ở input vẫn cần — đây là quy ước cho JSON. Nhưng output có type cụ thể hơn.
Chương 8 sẽ học cách dùng records hoặc class để type-safe hoàn toàn.
12. Quiz
var x = 5; rồi x = '5'; — chuyện gì xảy ra?
Xem đáp án
Đáp án: Lỗi compile. x đã được inferred type int tại dòng khai báo. '5' là String — không gán được. var ≠ dynamic.
final và const — cái nào tạo canonical instance?
Xem đáp án
Đáp án: const. Hai const cùng nội dung dùng cùng instance memory (canonicalize). final chỉ khoá biến gán 1 lần, nội dung vẫn là instance riêng.
1 == 1.0 ở Dart trả về gì?
Xem đáp án
Đáp án: true. int và double cùng kế thừa num, so sánh giá trị qua num equality. Khác Java/C# strict (so sánh type).
if (1) { ... } ở Dart hợp lệ không?
Xem đáp án
Đáp án: Không. Lỗi compile. Dart không có truthy/falsy — if chỉ chấp nhận bool thuần. Khác JS hoàn toàn.
Type nào là parent chung của int và double?
Xem đáp án
Đáp án: num. int và double đều extends num. num extends Object.
int? x; chưa gán giá trị — đọc x trả gì?
Xem đáp án
Đáp án: null. Nullable type mặc định là null. Khác với non-nullable int x; — đọc x trước khi gán → lỗi compile (definite assignment).
int x; (non-nullable) chưa gán giá trị, sau đó đọc x — chuyện gì?
Xem đáp án
Đáp án: Lỗi compile — "Non-nullable variable 'x' must be assigned before it can be used." Đây gọi là definite assignment analysis. Phải gán trước khi đọc.
Khác biệt 'a$b' và 'a${b}' là gì?
Xem đáp án
Đáp án:
Không khác — cả 2 đều interpolation cho biến b.
${...} với braces cần thiết khi expression không phải identifier đơn (vd ${user.name} hoặc ${a + b}).
$identifier shorthand cho biến đơn.
13. Tổng kết
- ✅ Hiểu Dart static + sound type system (vs JS dynamic, TS gradual).
- ✅ 4 cách khai báo biến:
var(inferred),final(runtime immutable),const(compile-time + canonical), explicit type. - ✅ Built-in types:
int,double,num,String,bool,List,Set,Map. - ✅ String interpolation:
$namevà${expr}. Multi-line'''. Rawr'...'. - ✅
boolstrict — không có truthy/falsy. - ✅ Nullable
T?vs non-nullableT— sound null safety. - ✅
dynamic≠var,Object?vsdynamic. - ✅ Flow analysis — analyzer thu hẹp type sau
is/null check.
Dart không "hack" được type system. Mọi escape (dynamic, !, as cast) phải explicit.
Kết quả: code compile được hầu như không bao giờ throw TypeError hay NullPointer runtime.
Đổi lại: bạn phải viết type nhiều hơn JS, ít hơn Java/TS — Dart cân giữa concise và explicit.
14. Kết nối tới các chương khác
- Chương 3 — Functions & Control Flow: function params có type,
requirednamed param. - Chương 4 — Classes: class field có type,
finalfield,constconstructor. - Chương 5 — Null Safety: deep dive 5 null-aware operator,
late,!. - Chương 6 — Collections: List/Set/Map đầy đủ + higher-order method.
- Flutter chương 2 — Widget Tree: widget property hầu hết non-nullable,
constconstructor optimize.