Hello World & Basics
Hello, World
console.log("Hello, World!"); void main() {
print("Hello, World!");
} Dart requires an explicit
void main() entry point; there is no top-level statement mode as in a TypeScript module. Output goes through print rather than console.log.Comments
// a line comment
/* a block comment */
/** A JSDoc comment. */
const answer = 42;
console.log(answer); void main() {
// a line comment
/* a block comment */
/// A doc comment for the next declaration.
const answer = 42;
print(answer);
} Line and block comments are identical. For documentation, Dart uses the triple-slash
/// (fed to dart doc) where TypeScript uses the JSDoc /** */ form.String interpolation
const name = "Ada";
const count = 3;
console.log(`${name} has ${count} items`); void main() {
final name = "Ada";
final count = 3;
print("$name has $count items");
} Dart interpolates inside ordinary string literals — no backtick template needed. A bare identifier is
$name; any expression takes braces, ${count + 1}, matching TypeScript’s ${…}.Variables & Constants
let/const → var/final
let counter = 0; // mutable
const fixed = 10; // immutable binding
counter += 1;
console.log(`${fixed} ${counter}`); void main() {
var counter = 0; // mutable
final fixed = 10; // immutable binding
counter += 1;
print("$fixed $counter");
} TypeScript’s
let maps to Dart’s var, and its const (an immutable binding) maps to Dart’s final. Dart’s own const means something stronger — see the next row.Compile-time const
// TS 'const' is just an immutable binding; the value
// can still be computed at run time.
const maxUsers = 50 * 2;
const list = [1, 2, 3]; // the array is still mutable
list.push(4);
console.log(list); void main() {
const maxUsers = 50 * 2; // must be compile-time known
const list = [1, 2, 3]; // deeply immutable + canonicalized
// list.add(4); // would throw: Unsupported
print(list);
} Dart’s
const is a compile-time constant: it must be computable at compile time, is deeply immutable, and is canonicalized so identical values share one instance. TypeScript’s const only forbids reassignment — the referenced object stays mutable.Type annotations
const inferred = 3.14; // number
const annotated: number = 42;
const items: string[] = ["a", "b"];
console.log(`${inferred} ${annotated} ${items}`); void main() {
final inferred = 3.14; // double
final double annotated = 42; // int literal promoted to double
final List<String> items = ["a", "b"];
print("$inferred $annotated $items");
} The annotation goes before the name in Dart (
Type name), the reverse of TypeScript’s name: Type. And Dart is stricter about numbers: double annotated = 42 is fine (an int literal in a double context is promoted), but int x = 3.14 is a compile error — there is no implicit narrowing.Nullability
null vs null + undefined
let a: string | undefined; // absent → undefined
let b: string | null = null; // explicit null
console.log(a, b);
console.log(a ?? "default"); void main() {
String? a; // absent → null (no 'undefined')
String? b = null;
print("$a $b");
print(a ?? "default");
} Dart has only
null — there is no separate undefined. An unassigned nullable variable is null, and a single T? covers every "might be absent" case that TypeScript splits across undefined and null.Nullable types & narrowing
function greet(name: string | null): string {
if (name === null) return "hi, stranger";
return `hi, ${name}`; // narrowed to string
}
console.log(greet(null));
console.log(greet("Ada")); String greet(String? name) {
if (name == null) return "hi, stranger";
return "hi, $name"; // promoted to String here
}
void main() {
print(greet(null));
print(greet("Ada"));
} Both narrow after a null check. Dart calls it type promotion and it works on local variables and parameters but not on non-final instance fields — a limitation TypeScript’s control-flow narrowing does not share.
Chaining, ?? and !
const words: string[] | null = ["hi", "there"];
console.log(words?.length ?? 0); // 2
const forced = words!; // assert non-null
console.log(forced[0]); void main() {
List<String>? words = ["hi", "there"];
print(words?.length ?? 0); // 2
final forced = words!; // throws if null
print(forced[0]);
} The trio is nearly identical:
?. for safe access, ?? for a fallback, and postfix ! to assert non-null. The one difference is at runtime — Dart’s ! actually throws if the value is null, whereas TypeScript’s ! is erased and does nothing.unknown → Object?
function describe(value: unknown): string {
if (typeof value === "string") return value.toUpperCase();
if (typeof value === "number") return value.toFixed(1);
return "other";
}
console.log(describe("hi"));
console.log(describe(3)); String describe(Object? value) {
if (value is String) return value.toUpperCase();
if (value is num) return value.toStringAsFixed(1);
return "other";
}
void main() {
print(describe("hi"));
print(describe(3));
} Dart’s
Object? is the safe top type — the counterpart to TypeScript’s unknown — accepting anything but requiring a check before use. The check is is (which also promotes the variable) rather than typeof, and because Dart types are reified, is works on class and generic types too.Numbers
number → int/double
const whole = 7; // number (a float)
const fraction = 3.5; // also number
console.log(whole / 2); // 3.5
console.log(Math.trunc(whole / 2)); // 3 void main() {
int whole = 7; // a distinct integer type
double fraction = 3.5;
print(whole / 2); // 3.5 — / always yields double
print(whole ~/ 2); // 3 — truncating integer division
} TypeScript has one numeric type,
number, which is always a 64-bit float. Dart splits it into int and double (both under num). Watch the operators: / always produces a double, and truncating division has its own symbol, ~/.Arbitrary-precision integers
// TS needs a separate BigInt type and the 'n' suffix.
const big: bigint = 2n ** 64n;
console.log(big.toString()); void main() {
// Native int is 64-bit; BigInt is built in for more.
final big = BigInt.from(2).pow(64);
print(big);
} Dart’s
int is a true 64-bit integer (not a float), so ordinary integer math is exact up to 2^63. Beyond that, BigInt is a core type used with normal method calls — no special literal suffix like TypeScript’s 2n.Number methods
console.log(Math.abs(-4));
console.log((10 % 2) === 0);
console.log((3.7).toFixed(0));
console.log(Math.max(3, 8)); import 'dart:math' as math;
void main() {
print((-4).abs());
print(10.isEven);
print(3.7.round());
print(math.max(3, 8));
} Many helpers live on the number itself as methods and getters (
(-4).abs(), 10.isEven), rather than on a global Math object. But max/min/sqrt are free functions in dart:math, the closest thing to TypeScript’s Math.Strings
Common operations
const phrase = "hello world";
console.log(phrase.length);
console.log(phrase.toUpperCase());
console.log(phrase.includes("world"));
console.log(phrase.split(" ")); void main() {
final phrase = "hello world";
print(phrase.length);
print(phrase.toUpperCase());
print(phrase.contains("world"));
print(phrase.split(" "));
} The methods line up closely; the main rename is
includes → contains. Both length values count UTF-16 code units, so they behave the same on astral characters — a familiar quirk from JavaScript.Multiline & raw strings
const poem = `line one
line two`;
const path = String.raw`C:\\temp`;
console.log(poem);
console.log(path); void main() {
final poem = '''
line one
line two''';
final path = r"C:\temp";
print(poem);
print(path);
} A template literal spans lines in TypeScript; in Dart a multiline string needs triple quotes (
''' or """). A raw string is a lowercase r prefix rather than String.raw, and inside it backslashes are literal.Collections
Array → List
const numbers: number[] = [1, 2, 3];
numbers.push(4);
console.log(numbers);
console.log(numbers.map((n) => n * 2)); void main() {
final numbers = <int>[1, 2, 3];
numbers.add(4);
print(numbers);
print(numbers.map((n) => n * 2).toList());
} A Dart
List is TypeScript’s Array with different method names (push → add). The gotcha: map returns a lazy Iterable, so you finish it with .toList() — TypeScript’s map returns an array directly.Object/Map → Map
const ages = new Map<string, number>([["Ada", 36]]);
ages.set("Alan", 41);
console.log(ages.get("Ada")); // 36
console.log(ages.size); void main() {
final ages = {"Ada": 36}; // a Map literal
ages["Alan"] = 41;
print(ages["Ada"]); // 36
print(ages.length);
} Dart’s
{} literal is a Map — there is no separate plain-object type as in TypeScript, and no new Map() ceremony. Indexing returns a nullable value (V?), so ages["Ada"] has type int?, forcing you to handle a missing key.Set
const unique = new Set([1, 2, 2, 3]);
console.log(unique.has(2));
console.log(unique.size); void main() {
final unique = {1, 2, 2, 3}; // a Set literal
print(unique.contains(2));
print(unique.length);
} A brace literal with no colons is a
Set; with colons it is a Map. Note has → contains and size → length. The empty case is ambiguous: {} is a Map, so an empty set needs <int>{}.Spread & collection-if/for
const base = [1, 2, 3];
const includeZero = true;
const built = [
...(includeZero ? [0] : []),
...base,
...base.map((n) => n * 10),
];
console.log(built); void main() {
final base = [1, 2, 3];
const includeZero = true;
final built = [
if (includeZero) 0,
...base,
for (final n in base) n * 10,
];
print(built);
} Both have the spread operator
..., but Dart adds if and for elements right inside a collection literal, so conditional and repeated entries need no ? [] : [] trick or pre-built map. This is everywhere in Flutter widget trees.Structural vs Nominal Typing
Structural → nominal typing
interface Named { name: string; }
function announce(thing: Named): void {
console.log(thing.name);
}
// Any object with the right shape works — no 'implements'.
announce({ name: "Ada", extra: 99 }); abstract interface class Named {
String get name;
}
class Person implements Named {
@override
final String name;
Person(this.name);
}
void announce(Named thing) => print(thing.name);
void main() {
announce(Person("Ada")); // must implement Named by name
} This is the deepest difference. TypeScript typing is structural: any value with the right shape satisfies an interface, no declaration required. Dart is nominal: a type conforms only if it explicitly
implements the interface, so an anonymous object literal will not do.No anonymous object types
// A bare object literal has an inferred structural type.
function distance(point: { x: number; y: number }): number {
return Math.sqrt(point.x ** 2 + point.y ** 2);
}
console.log(distance({ x: 3, y: 4 })); import 'dart:math' as math;
// The closest is a record: structural, but positional/named.
double distance(({double x, double y}) point) =>
math.sqrt(point.x * point.x + point.y * point.y);
void main() {
print(distance((x: 3, y: 4)));
} Dart has no inline structural object type like
{ x: number; y: number }. The nearest equivalent is a record with named fields, ({double x, double y}) — which, unlike everything else in Dart, is compared structurally. For anything with behavior you declare a class.Union Types → Sealed Classes
No union types
// A value that is one of several types.
function format(value: string | number): string {
return typeof value === "number" ? value.toFixed(1) : value;
}
console.log(format(3));
console.log(format("hi")); // Dart has no union types. Use the common supertype
// plus an 'is' check, or an Object? with narrowing.
String format(Object value) => switch (value) {
num n => n.toStringAsFixed(1),
String s => s,
_ => "?",
};
void main() {
print(format(3));
print(format("hi"));
} Dart has no union types: there is no way to write
String | int as a parameter type. You fall back to the nearest common supertype (Object) and narrow with is or a switch — losing the compile-time guarantee that the value is exactly one of the listed types.Discriminated union → sealed class
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rect"; width: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle": return Math.PI * shape.radius ** 2;
case "rect": return shape.width * shape.height;
}
}
console.log(area({ kind: "circle", radius: 2 })); sealed class Shape {}
class Circle extends Shape {
final double radius;
Circle(this.radius);
}
class Rect extends Shape {
final double width, height;
Rect(this.width, this.height);
}
double area(Shape shape) => switch (shape) {
Circle(:final radius) => 3.14159 * radius * radius,
Rect(:final width, :final height) => width * height,
};
void main() {
print(area(Circle(2)));
} The idiomatic replacement for a TypeScript discriminated union is a
sealed class hierarchy. The compiler knows the complete set of subtypes, so a switch over a sealed type is exhaustive — the same safety the kind discriminant gives you, but checked without a manually maintained tag field.Literal union → enum
type Direction = "north" | "south" | "east" | "west";
function opposite(d: Direction): Direction {
return d === "north" ? "south" : "north";
}
console.log(opposite("north")); enum Direction { north, south, east, west }
Direction opposite(Direction d) =>
d == Direction.north ? Direction.south : Direction.north;
void main() {
print(opposite(Direction.north).name);
} A union of string literals — a common TypeScript enum substitute — becomes an actual Dart
enum. You gain exhaustiveness and a real type, and lose the ability to pass a bare "north" string; the enum value Direction.north is required, with .name to recover the string.Functions & Closures
Function definition
function add(x: number, y: number): number {
return x + y;
}
const square = (x: number): number => x * x;
console.log(add(3, 4));
console.log(square(5)); int add(int x, int y) {
return x + y;
}
int square(int x) => x * x; // arrow body
void main() {
print(add(3, 4));
print(square(5));
} Dart puts the return type first and drops the
function keyword. The => arrow body is for single-expression functions — like a TypeScript arrow function, but usable for named top-level and member functions too, not just values.Named & optional parameters
// TS simulates named args with an options object.
function makeUser(opts: { name: string; admin?: boolean }): string {
return `${opts.name} admin=${opts.admin ?? false}`;
}
console.log(makeUser({ name: "Ada" }));
console.log(makeUser({ name: "Alan", admin: true })); String makeUser({required String name, bool admin = false}) {
return "$name admin=$admin";
}
void main() {
print(makeUser(name: "Ada"));
print(makeUser(name: "Alan", admin: true));
} Dart has real named parameters in
{ }, so there is no options-object pattern. They are optional by default; mark one required to force it, or give it a default. Optional positional parameters use [ ] instead.Closures & higher-order
const numbers = [1, 2, 3, 4];
const evens = numbers.filter((n) => n % 2 === 0);
const total = numbers.reduce((sum, n) => sum + n, 0);
console.log(evens);
console.log(total); void main() {
final numbers = [1, 2, 3, 4];
final evens = numbers.where((n) => n % 2 == 0).toList();
final total = numbers.fold(0, (sum, n) => sum + n);
print(evens);
print(total);
} Closure syntax is almost identical. The renames to learn:
filter → where (lazy, so .toList()), and reduce(fn, initial) → fold(initial, fn) — Dart’s own reduce exists but takes no seed and throws on an empty list.Function types
const greet = (name: string): string => `Hi, ${name}`;
const fn: (name: string) => string = greet;
console.log(fn("Ada")); void main() {
String greet(String name) => "Hi, $name";
final String Function(String) fn = greet;
print(fn("Ada"));
} A function type is written
ReturnType Function(ArgTypes) in Dart, versus TypeScript’s (args) => ReturnType. Functions are first-class in both, and Dart allows nested named functions inside main.Classes & Constructors
Constructor parameter fields
class Animal {
constructor(
public readonly name: string,
public legs: number = 4,
) {}
}
const dog = new Animal("Rex");
console.log(`${dog.name} ${dog.legs}`); class Animal {
final String name;
int legs;
Animal(this.name, {this.legs = 4});
}
void main() {
final dog = Animal("Rex"); // no 'new' keyword needed
print("${dog.name} ${dog.legs}");
} Both shorthand constructor parameters into fields — TypeScript with
public/readonly modifiers, Dart with this.name. Note Dart makes new optional (and idiomatically omitted), and there is no constructor keyword: the constructor is named after the class.Privacy: # vs _
class Counter {
#count = 0;
increment(): void { this.#count += 1; }
get value(): number { return this.#count; }
}
const c = new Counter();
c.increment();
console.log(c.value); class Counter {
int _count = 0; // library-private by convention + enforcement
void increment() => _count += 1;
int get value => _count;
}
void main() {
final c = Counter();
c.increment();
print(c.value);
} Dart privacy is at the library level, not the class: a leading underscore (
_count) makes a member private to its file, and it is genuinely inaccessible from other libraries. There is no per-class private or the # hard-private of modern JS/TS.Named constructors
class Point {
constructor(public x: number, public y: number) {}
// TS uses static factory methods for alternatives.
static origin(): Point { return new Point(0, 0); }
}
const p = Point.origin();
console.log(`${p.x} ${p.y}`); class Point {
final double x, y;
Point(this.x, this.y);
Point.origin() : x = 0, y = 0; // a named constructor
}
void main() {
final p = Point.origin();
print("${p.x} ${p.y}");
} Dart supports multiple named constructors (
Point.origin()) as a language feature, with an initializer list after the colon to set final fields. TypeScript has a single constructor, so alternatives are static factory methods.Cascades
// TS repeats the receiver for each call.
const parts: string[] = [];
parts.push("a");
parts.push("b");
parts.push("c");
console.log(parts); void main() {
final parts = <String>[]
..add("a")
..add("b")
..add("c");
print(parts);
} The cascade operator
.. calls a sequence of methods on the same receiver and evaluates to that receiver, so you build and configure an object without naming it repeatedly. TypeScript has no equivalent; you either repeat the variable or design a fluent (return-this) API.Interfaces & Mixins
Interfaces & abstract classes
interface Greeter { greet(): string; }
abstract class Base implements Greeter {
abstract greet(): string;
loud(): string { return this.greet().toUpperCase(); }
}
class English extends Base {
greet(): string { return "hello"; }
}
console.log(new English().loud()); abstract interface class Greeter {
String greet();
}
abstract class Base implements Greeter {
String loud() => greet().toUpperCase();
}
class English extends Base {
@override
String greet() => "hello";
}
void main() {
print(English().loud());
} Every Dart class defines an implicit interface, so
implements works on any class; abstract interface class declares a pure contract. Unlike TypeScript, @override is an advisory annotation, not required, and a class can implements many interfaces but extends only one.Mixins
// TS mixins are a function returning an extended class.
type Ctor = new (...args: any[]) => {};
function Logging<T extends Ctor>(Base: T) {
return class extends Base {
log(message: string) { console.log(`[log] ${message}`); }
};
}
class Service {}
const LoggingService = Logging(Service);
new LoggingService().log("started"); mixin Logging {
void log(String message) => print("[log] $message");
}
class Service with Logging {}
void main() {
Service().log("started");
} Dart has first-class
mixins applied with with — no class-factory pattern. A mixin can hold state and be constrained to a superclass with on, giving true multiple-implementation composition that TypeScript can only approximate with the mixin-function idiom shown here.Extension methods
// TS cannot add methods to an existing type without
// module augmentation of its declaration; here is a plain
// helper function instead.
function squared(n: number): number { return n * n; }
console.log(squared(5)); extension IntExtras on int {
int get squared => this * this;
}
void main() {
print(5.squared); // reads like a built-in member
} Dart extension methods add members to an existing type — even
int or String — that are called with dot syntax, scoped to wherever the extension is imported. TypeScript has no clean equivalent; you use free functions or fragile declaration-merging.Enums
Enums
enum Status { Active, Inactive, Pending }
const s: Status = Status.Active;
console.log(Status[s]); // "Active"
console.log(s === Status.Active); enum Status { active, inactive, pending }
void main() {
const s = Status.active;
print(s.name); // "active"
print(s == Status.active);
} Dart enums are real, type-safe values (never plain numbers), and
.name gives the string form — no reverse-index lookup like Status[s]. Every enum also has .index and a generated values list.Enums with fields & methods
// A TS enum can only map to a number or string.
enum Planet { Earth = 9.8, Mars = 3.7 }
console.log(Planet.Mars); // 3.7 — the raw value enum Planet {
earth(9.8),
mars(3.7);
final double gravity;
const Planet(this.gravity);
bool get isHeavy => gravity > 5;
}
void main() {
print(Planet.mars.gravity);
print(Planet.earth.isHeavy);
} Dart’s enhanced enums (2.17+) carry typed fields, a const constructor, methods, and getters — far beyond TypeScript enums, whose members map only to a number or a string. Each case invokes the constructor with its payload.
Pattern Matching
switch expressions
// TS switch is a statement; return from each case.
function describe(n: number): string {
switch (true) {
case n === 0: return "zero";
case n >= 1 && n <= 9: return "small";
default: return "big";
}
}
console.log(describe(5)); String describe(int n) => switch (n) {
0 => "zero",
>= 1 && <= 9 => "small",
_ => "big",
};
void main() {
print(describe(5));
} Dart 3’s
switch is an expression that returns a value through => arms, with relational (>= 1) and logical (&&) patterns and _ as the wildcard. TypeScript has only the statement form and the switch (true) trick for ranges.Record & object patterns
const point: [number, number] = [2, 0];
const [x, y] = point; // array destructuring
console.log(`${x}, ${y}`); void main() {
final point = (2, 0);
final (x, y) = point; // record destructuring
print("$x, $y");
// and destructuring inside a switch:
final label = switch (point) {
(0, 0) => "origin",
(final a, 0) => "on x-axis at $a",
_ => "elsewhere",
};
print(label);
} Dart destructures records and objects not just in a binding but inside
switch arms and if-case, binding names and constraining constants at once. TypeScript destructuring is limited to bindings — it has no structural pattern matching in control flow.if-case binding
const response: [number, string] = [200, "OK"];
const [status, message] = response;
if (status === 200) {
console.log(`success: ${message}`);
} void main() {
final response = (200, "OK");
if (response case (200, final message)) {
print("success: $message");
}
} The
if (value case Pattern) form matches and binds in one step, only entering the branch when the pattern (including the literal 200) fits. TypeScript needs a separate destructure plus an equality check to express the same intent.Generics
Generic functions
function firstOrNull<T>(items: T[]): T | null {
return items.length > 0 ? items[0] : null;
}
console.log(firstOrNull([10, 20, 30]) ?? -1); T? firstOrNull<T>(List<T> items) {
return items.isEmpty ? null : items.first;
}
void main() {
print(firstOrNull([10, 20, 30]) ?? -1);
} Generic syntax is close: a type parameter in angle brackets. Dart writes the return type first (
T? firstOrNull<T>), and the nullable result is T? rather than the union T | null.Reified vs erased types
function isStringList<T>(items: T[]): boolean {
// T is ERASED at runtime — this cannot check T.
return items.every((x) => typeof x === "string");
}
console.log(isStringList(["a", "b"])); bool isStringList(List<Object?> items) {
// Type arguments SURVIVE to runtime in Dart.
return items is List<String>;
}
void main() {
print(isStringList(<String>["a", "b"]));
print(isStringList(<int>[1, 2]));
} This is a core divergence: TypeScript types are erased — gone at runtime, so you cannot test a type parameter. Dart generics are reified:
items is List<String> genuinely inspects the runtime type argument, a check impossible in TypeScript or on the JVM.Bounded type parameters
function largest<T extends { compareTo(other: T): number }>(
items: T[],
): T {
return items.reduce((a, b) => (a.compareTo(b) >= 0 ? a : b));
}
// (illustrative — TS has no built-in Comparable)
console.log("see Dart column"); T largest<T extends Comparable<T>>(List<T> items) {
var result = items.first;
for (final item in items.skip(1)) {
if (item.compareTo(result) > 0) result = item;
}
return result;
}
void main() {
print(largest([3, 9, 2]));
print(largest(["apple", "pear", "fig"]));
} Both bound a type parameter with
extends. Dart ships a built-in Comparable<T> that int, double, String, and DateTime already implement, so the bound is meaningful out of the box — TypeScript has no standard comparable protocol.Async & Concurrency
Promise → Future
function doubled(x: number): Promise<number> {
return Promise.resolve(x * 2);
}
(async () => {
const result = await doubled(21);
console.log(result);
})(); Future<int> doubled(int x) async {
return x * 2;
}
Future<void> main() async {
final result = await doubled(21);
print(result);
} A Dart
Future<T> is TypeScript’s Promise<T>, and async/await work the same way. The difference is that main itself can be async and await at the top level — no (async () => { … })() wrapper needed.Awaiting many
function value(n: number): Promise<number> {
return Promise.resolve(n);
}
(async () => {
const results = await Promise.all([value(1), value(2), value(3)]);
console.log(results.reduce((a, b) => a + b, 0));
})(); Future<int> value(int n) async => n;
Future<void> main() async {
final results = await Future.wait([value(1), value(2), value(3)]);
print(results.reduce((a, b) => a + b));
} Dart’s
Future.wait is the direct analog of Promise.all: it runs the futures concurrently and resolves to a list of their results. Both let independent async work overlap instead of awaiting each in turn.Async iterator → Stream
async function* countUp(): AsyncGenerator<number> {
for (let index = 1; index <= 3; index++) {
yield index;
}
}
(async () => {
for await (const value of countUp()) {
console.log(value);
}
})(); Stream<int> countUp() async* {
for (var index = 1; index <= 3; index++) {
yield index;
}
}
Future<void> main() async {
await for (final value in countUp()) {
print(value);
}
} Dart’s
Stream is TypeScript’s async iterable, and an async* generator with yield maps directly to async function*. You consume it with await for, the counterpart of for await…of. Streams are also the backbone of Flutter’s reactive APIs.Workers → isolates
// True parallelism in TS/JS means a Web Worker or
// worker_thread — a separate file and message passing.
// Everything on the main thread shares one heap.
const heavy = (n: number): number => {
let total = 0;
for (let i = 1; i <= n; i++) total += i;
return total;
};
console.log(heavy(1000)); import 'dart:isolate';
int heavySum(int n) {
var total = 0;
for (var i = 1; i <= n; i++) total += i;
return total;
}
Future<void> main() async {
// Isolate.run offloads CPU work to a separate heap.
final result = await Isolate.run(() => heavySum(1000));
print(result);
} Dart’s concurrency model matches JavaScript’s single-threaded event loop for I/O, but its answer for CPU parallelism — the isolate — is cleaner than a Web Worker:
Isolate.run takes an ordinary closure, and because isolates share no memory, data races are impossible by construction.Error Handling
throw / try / catch
class ParseError extends Error {}
function parse(text: string): number {
if (text === "") throw new ParseError("empty");
return Number(text);
}
try {
parse("");
} catch (error) {
console.log(`failed: ${(error as Error).message}`);
} class ParseError implements Exception {
final String message;
ParseError(this.message);
@override
String toString() => "ParseError: $message";
}
int parse(String text) {
if (text.isEmpty) throw ParseError("empty");
return int.parse(text);
}
void main() {
try {
parse("");
} catch (error) {
print("failed: $error");
}
} Both languages have unchecked exceptions — nothing in a signature marks that a function can throw, and a bare
catch catches anything. Dart lets you throw any object, though implementing Exception is the convention; the analog of TypeScript’s Error subclass.Typed catch & finally
class TimeoutError extends Error {}
function fetch(): void { throw new TimeoutError(); }
try {
fetch();
} catch (error) {
if (error instanceof TimeoutError) console.log("timed out");
else console.log("other");
} finally {
console.log("cleaned up");
} class TimeoutError implements Exception {}
void fetch() => throw TimeoutError();
void main() {
try {
fetch();
} on TimeoutError {
print("timed out");
} catch (error) {
print("other: $error");
} finally {
print("cleaned up");
}
} Dart selects a handler by type with
on TypeName, avoiding TypeScript’s catch (error) { if (error instanceof …) } ladder — a real win now that reified types make the check reliable. finally behaves the same in both.