PONY λ M2 Modula-2

TypeScript.CodeCompared.To/Dart

An interactive executable cheatsheet comparing TypeScript and Dart

TypeScript 6.0 Dart 3.7
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 includescontains. 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 (pushadd). 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 hascontains and sizelength. 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: filterwhere (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.