Zena compared to TypeScript
Zena builds directly on the syntax, ergonomics, and conventions of JavaScript and TypeScript. If you know TypeScript, most Zena code will read naturally on the first pass, and hopefully most of the differences will intuitively make sense.
TypeScript demonstrated how much static typing could bring to a dynamic language like JavaScript, and how good of a language JavaScript can be with more structure and machine-assisted checking.
However, TypeScript has the constraint of being a valid superset of JavaScript and interoperating with existing JavaScript code. It must have a gradual and unsound type system, erase all types at compile time, and can't use types to control emit.
Zena is designed for a different target: ahead-of-time compilation to
WebAssembly GC. Because Zena does not need to execute existing JavaScript
codebases or preserve JavaScript's dynamic nature, it is free to adopt a sound
type system, static classical inheritance, and immutability by default; remove
sharp edges like this in functions, implicit coercions, switch with
fallthrough, etc.; and add many modern language features.
Similarities ​
Zena retains the syntax patterns that make TypeScript pleasant to read and write:
- Type annotations and inference:
let count: i32 = 42;, with local type inference allowing the omission of many annotations. - Arrow functions:
(a: i32, b: i32) => a + bwith contextual typing for callbacks. - Objects/Records: Zena keeps object literal syntax for
lightweight records
{x: 10, y: 20}. - Destructuring: Destructuring assignment and parameters
let {x, y} = point;. - Classes: The basics of Zena classes start from TypeScript:
class,extends, andimplementskeywords. Fields, private fields and methods.thisto reference members. - Generics: Generic classes and functions with
<T>type parameters andextendsconstraints. - Modules: Standard
importandexportdeclarations. - Template literals: String interpolation with
`Hello, ${name}!`. - Optional chaining and null coalescing: Safe navigation with
?.and fallbacks with??. - Flow-based type narrowing: Type checks (
is) and null checks (!= null) narrow types across conditional branches.
Differences ​
Core ​
-
Numeric primitives: Zena supports the core Wasm primitive types of
i32,i64,f32, andf64, plus additional unsigned integer types that select unsigned arithmetic and narrow integer types that pack tightly into Wasm arrays. -
No
undefined: Zena uses a singlenullvalue for absent reference values. -
No implicit coercion or truthiness: Operators do not perform type coercion. Conditions strictly require a
booleanexpression.1 + '1',if ('str')andif (0)are compile errors. -
Immutability by default: Variables use
letfor immutable bindings andvarfor mutable bindings. Class fields are immutable by default. Records and tuples are shallowly immutable. -
Multi-value function returns: Functions returning inline tuples compile directly to Wasm native multi-value returns, placing values directly on the stack without heap allocation.
This powers core standard library APIs:
Map.get(key)returns(found, value)in a single call without mixing primitive keys andnullallocating or throwing.Iterator.next()returns(hasValue, value)without allocating a{done, value}wrapper object or requiring two calls on each iteration.
-
Array literals create FixedArrays: FixedArray is the lowest-cost array type in Zena, mapping directly to unwrapped Wasm GC arrays, so the literal syntax creates them. Growable arrays are created with
new GrowableArray()(soon to be available with a short macro). -
Map Literals: Map literal syntax with fat arrows:
let scores = {'Alice' => 95, 'Bob' => 87};. -
No
++or--operators: Like Swift, Zena omits these often confusing operators. Use+= 1and-= 1instead. -
isinstead ofinstanceof:isis just shorter. Zena'sisoperator is a runtime check as well, and works with classes, interfaces, mixins, and generics. -
No regex literals: Regular expressions are a library and use tagged template literals:
regex`ab+c`.
Classes ​
-
Nominal classes and interfaces: Zena uses nominal typing for classes and interfaces, compared to TypeScript's structural type system. Zena records and tuples are still structurally typed.
-
Mixins, extension classes, case classes: Classes support linearizable mixins (
class Button with Clickable), concise case classes (class Point(x: f64, y: f64)), and extension classes that allow you to add new methods to existing classes and primitives. -
Initializaters: Constructors are divided into member initializer lists and bodies. A class hierarchy's complete set of initializers runs before constructor bodies to ensure that no partially initialized
thisreferences to the object can escape. -
thisis only avalable in classes:thisis always lexical, never settable. There's no confusion about whatthisrefers to. -
Algebraic Data Types:
sealed classhierarchies are Zena's sum types, but built on the existing class system. They work with exhaustivematch(). Case classes get auto-generatedoperator ==andhashCode.
Functions ​
-
Top-level vs arrow functions: Zena separates top-level functions from closures.
functionis used to declare top-level functions which can be passed as values, but can only reference globals. Arrow functions are closures and can capture local variables. -
Optional parameters must have defaults: To avoid mixing
nullwith primitives and to meet Wasm's strict function arity rules, all optional parameters must have defaults. -
Destructured parameters include types: Because Zena uses
asto rename destructured properties instead of JavaScript's:, we can use:in destructured parameters for type annotations:({a: i32, x as b: i32}) => a + b. -
No rest parameters: Also to meet Wasm's strict function arity rules, there are currently no rest parameters. They may be added in the future, by heap allocating a list.
Type system ​
-
Reified, monomorphized generics: Zena generics are reified via monomorphization. Generic types are distinguishable at runtime. A
Box<i32>is not aBox<f64>. -
Distinct and opaque Types: Zero-cost nominal type aliases:
distinct type UserId = i32;. Distinct types keep their base type API, opaque types completely hide it. -
Affine types: For managing non GC resources, Zena includes affine types with owned and borrowed references,
Own<T>andBorrow<T>. Borrowed references are stack-bound, andDisposableowned references are disposed when their owning scope exits. -
Primitives are not objects: There is no top-type that includes primitives and references, and primitives can't be mixed with references. This eliminates auto-boxing and ensures that arithmetic is always on the fast path.
Control flow ​
-
No
switch:switchwith fallthough is a common source of bugs in langagues that have it. Zena uses a pattern-matchingmatch()expression without fallthough instead. -
Expression-oriented control flow:
if,match,try/catch,throw,return,breakandcontinecan all be used in expression position. -
Pattern matching: Pattern matching is available across
match(),if/let, andwhile/let. -
for/ininstead offor/of: Zena doesn't have the legacy of JavaScript's key-enumeratingfor/inloop, so it usesfor/infor regular iteration. -
Pipeline Operator (
|>): Clean left-to-right data transformations with the$placeholder:data |> parse($) |> transform($).
Concurrency ​
- Future instead of Promise: Future is the more common name among languages, and the term using in WASI interfaces.
- Structured Async Cancellation: Instead of manual
AbortSignalforwarding, cancellation is automatically supported via ambient cancellation scopes. Allasyncfunctions can be cancelled, and handle cancellation safely through dedicatedtry/cancel/finallyandshieldedregions.
Cheat sheet ​
| Feature | TypeScript / JavaScript | Zena |
|---|---|---|
| Immutable variable | const x = 10; |
let x = 10; |
| Mutable variable | let x = 10; |
var x = 10; |
| Numeric types | number / bigint |
i32, i64, u32, u64, f32, f64 |
| String type | string |
String |
| Nullable reference | string | null | undefined |
String | null or String?_ |
| Top type | any / unknown |
None. (anyref for references only; use Box<T> for primitives) |
| Conditionals | const v = cond ? a : b; |
let v = if (cond) a else b; |
| Truthiness check | if (str) { ... } |
if (str != null && str.length > 0) { ... } |
| Switch / Match | switch (x) { case 1: ...; break; } |
match (x) { case 1: ... } |
| Iteration | for (const item of items) |
for (let item in items) |
| Increment | i++; / ++i; |
i += 1; |
| Top-level function | function add(a: number, b: number) {} |
function add(a: i32, b: i32): i32 {} |
| Local closure | const add = (a, b) => a + b; |
let add = (a: i32, b: i32) => a + b; |
| Constructor shorthand | constructor(public x: number) {} |
x: f64; new(this.x); |
| Mixins | Mixin factory functions | class Dog with Friendly implements Animal |
| Map literal | new Map([['a', 1]]) |
{'a' => 1} |
| Array literal | [1, 2, 3] |
[1, 2, 3] (immutable), fixed([1, 2, 3]), growable([1, 2, 3]) |
| Type assertion | x as string (erased at runtime) |
x as String (checked downcast) |
| Type test | x instanceof MyClass |
x is MyClass (also x is Array<i32>) |
| Multi-value return | return [val, true]; (heap array) |
return (val, true); (unboxed stack tuple) |
| Resource disposal | using res = getResource(); |
using let res = getResource(); |
Next ​
- Language Overview — a rapid tour of Zena's syntax and concepts
- What is Zena? — an introduction to Zena's design and features
- Quick Reference — every feature with syntax examples