Why Dart?
If you're a TypeScript developer, you already appreciate the value of types. Dart takes that appreciation further with features that TypeScript, due to its design constraints, cannot provide.
If you're a Flutter developer, you already know Dart. Now you can use it across the entire JavaScript ecosystem.
TypeScript: A Brilliant Compromise
TypeScript is an engineering marvel. It adds static typing to JavaScript without breaking compatibility with the massive JS ecosystem. This was a deliberate choice - and it was the right one for TypeScript's goals.
However, this choice comes with trade-offs that become apparent in larger applications.
The Type Erasure Problem
TypeScript types exist only at compile time. When your code runs, they're gone.
interface User {
id: number;
name: string;
email: string;
}
// This compiles fine
const user: User = JSON.parse(apiResponse);
// But at runtime, there's no guarantee `user` matches the interface.
// If the API returns { id: "123", name: null }, TypeScript can't help you.
console.log(user.name.toUpperCase()); // Runtime error if name is null!
Dart preserves types at runtime:
class User {
final int id;
final String name;
final String email;
User({required this.id, required this.name, required this.email});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as int, // Fails immediately if wrong type
name: json['name'] as String,
email: json['email'] as String,
);
}
}
// Validation happens at the boundary
final user = User.fromJson(jsonDecode(apiResponse));
// If we get here, we KNOW user.name is a non-null String
print(user.name.toUpperCase()); // Safe!
Sound Null Safety
TypeScript has strictNullChecks, which is excellent. But "strict" still allows escape hatches.
// TypeScript: The ! operator trusts you (sometimes wrongly)
function processUser(user: User | null) {
console.log(user!.name); // You're asserting user isn't null
// TypeScript trusts you. The runtime might not.
}
Dart's null safety is sound:
// Dart: The compiler ensures this
void processUser(User? user) {
print(user.name); // Compile error! user might be null
// You must handle the null case
if (user != null) {
print(user.name); // Now it's safe
}
// Or use null-aware operators
print(user?.name ?? 'Anonymous');
}
The ! operator exists in Dart too, but using it on a null value throws immediately - you can't silently proceed with undefined behavior.
Real-World Implications
Serialization
In TypeScript, you often need runtime validation libraries like Zod or io-ts:
import { z } from 'zod';
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
type User = z.infer<typeof UserSchema>;
// You define the shape twice: once for Zod, once for TypeScript
const user = UserSchema.parse(apiData);
In Dart, the class definition IS the runtime validation:
class User {
final int id;
final String name;
final String email;
User({required this.id, required this.name, required this.email});
factory User.fromJson(Map<String, dynamic> json) => User(
id: json['id'] as int,
name: json['name'] as String,
email: json['email'] as String,
);
}
// One definition, used for both types and validation
final user = User.fromJson(apiData);
Generics
TypeScript generics are erased:
class Box<T> {
constructor(public value: T) {}
isString(): boolean {
// Can't check if T is string at runtime!
// typeof this.value === 'string' checks the value, not T
return typeof this.value === 'string';
}
}
Dart generics exist at runtime:
class Box<T> {
final T value;
Box(this.value);
bool isString() {
// We can check the actual type parameter!
return T == String;
}
// Even better: type-safe operations
R map<R>(R Function(T) fn) => fn(value);
}
Single Language, Multiple Platforms
With dart_node, you write Dart everywhere:
| Platform | TypeScript | Dart (dart_node) |
|---|---|---|
| Backend | Node.js + TypeScript | dart_node_express |
| Web Frontend | React + TypeScript | dart_node_react |
| Mobile | React Native + TypeScript | dart_node_react_native |
| Desktop | Electron + TypeScript | Flutter Desktop |
Share models, validation logic, and business rules across all platforms - with runtime type safety.
Simpler Build Pipeline
A typical TypeScript project:
Source → TypeScript Compiler → Babel → Webpack/Rollup → Bundle
(tsconfig.json) (.babelrc) (webpack.config.js)
A dart_node project:
Source → dart compile js → Bundle
(just works)
No configuration maze. No compatibility issues between tools. One command.
For Flutter Developers
If you already know Dart from Flutter, dart_node opens up the JavaScript ecosystem:
- Use the massive React component ecosystem
- Access npm packages (millions of them)
- Deploy to Node.js hosting (cheaper than server-side Dart)
- Build with familiar tools (same language, same patterns)
When TypeScript Makes Sense
TypeScript remains an excellent choice when:
- You're working with an existing JavaScript codebase
- Your team is deeply invested in the TypeScript ecosystem
- You need maximum compatibility with JS libraries
- You prefer TypeScript's structural typing over Dart's nominal typing
Conclusion
Dart and TypeScript both add type safety to dynamic languages. TypeScript chose maximum JavaScript compatibility. Dart chose maximum type safety.
For new projects where runtime safety matters, Dart offers guarantees that TypeScript cannot provide - not because TypeScript is flawed, but because it was designed with different constraints.
With dart_node, you get the best of both worlds: Dart's type safety with access to the JavaScript ecosystem.
Ready to try it? Get started with dart_node