Classes & Generics
Classes, interfaces, and virtual dispatch work like upstream AssemblyScript’s; generics are specialized to concrete types at compile time.
Classes, inheritance, interfaces
class Base {
name(): i32 { return 1; }
describe(): i32 { return this.name() * 2; }
}
class A extends Base {
name(): i32 { return 3; }
}
interface Shape { area(): i32; }
class Square implements Shape {
side: i32 = 3;
area(): i32 { return this.side * this.side; }
}
export function demo(): i32 {
// Virtual dispatch via rtId: 6 + 9.
return (new A() as Base).describe() + (new Square() as Shape).area();
}
Fields with initializers, constructors (implicitly returning
this), methods, statics, getters/setters, extends with
super(), instanceof via rtti. Calls through a base
reference dispatch virtually, matching upstream AssemblyScript. Abstract methods compile to unreachable
stubs with stock-asc enforcement — no new of an abstract class,
missing implementations are errors.
Cascades
class Paint {
color: u32 = 0;
width: f32 = 1;
child: Paint | null = null;
thicker(by: f32): Paint {
this.width += by;
return this;
}
}
function draw(p: Paint): f32 {
return p.width;
}
export function demo(): f32 {
// The receiver is evaluated once, each section runs on it in order,
// and the whole expression is that receiver, now 3 wide.
let stroke = new Paint()
..color = 0xff0000ff
..width = 2
..thicker(1);
// Any expression position works: arguments, returns, initializers.
return draw(stroke) + draw(new Paint()..thicker(4)..child = stroke);
}
Dart’s cascade operator. receiver..section..section
evaluates the receiver once, runs each section on it as a statement, and
yields the receiver, so setting up an object field by field stays an
expression. A section is a member chain rooted at the receiver, optionally
followed by an assignment: ..x = v, ..x += v,
..f(a), ..[i] = v, ..a.b = v. The
receiver is the whole conditional expression, as in Dart: the cascade binds
looser than postfix, binary, as and ternary operators and tighter
than assignment, so v as Paint..color = c casts first and
x = a..b = c sets a.b and then assigns a
to x; parenthesize to cascade on an assignment result. A
section’s right side never starts a nested cascade: in
a..child = b..width = 2 both sections apply to a,
as in Dart, and a nested cascade needs parentheses. Sections may continue
on following lines; rasc fmt puts each section of a multi
section cascade on its own line.
Receivers are reference classes and interfaces: numbers, strings and
function values are rejected, a nullable receiver is an error (assert with
! first), and struct receivers are rejected since structs
construct by value. Number literals keep their trailing dot, so after
..x = 1 the next section goes on a new line or after a space.
Generics
Generic functions, classes, and methods are fully monomorphized at compile time, the way C++ templates are: each distinct set of type arguments compiles to its own specialized copy, so a generic call costs exactly what the hand-written version would — no boxing, no dynamic dispatch. Type arguments are usually inferred from the parameters:
function largest<T>(a: T, b: T): T {
return a > b ? a : b;
}
export function demo(): f64 {
let i = largest(3, 9); // largest<i32>, inferred from the arguments
let f = largest(0.25, 2.5); // largest<f64> — a second compiled copy
return (i as f64) + f; // 9 + 2.5
}
Compilation is lazy and whole-program, so only the instantiations code actually reaches get compiled — a combination nothing calls costs nothing. Classes and methods take type parameters the same way, and a method can add its own on top of the class’s:
class Box<T> {
value: T;
constructor(value: T) { this.value = value; }
map<U>(f: (v: T) => U): Box<U> {
return new Box<U>(f(this.value));
}
}
export function demo(): f64 {
let b = new Box<i32>(21);
let scaled = b.map<f64>((v: i32): f64 => (v as f64) * 2.0);
return scaled.value; // 42
}
One inference gap to know: a type parameter used only in a
callback’s return type is not inferred from a lambda — the
lambda’s own annotations do not flow backward into the call. So
arr.map<i32>(...) needs its explicit argument, while
filter, reduce, and forEach — whose
type parameters all appear in parameter positions — infer fine:
export function demo(): i32 {
let xs = [1, 2, 3, 4];
let evens = xs.filter((x: i32): bool => (x & 1) == 0); // infers fine
let doubled = xs.map<i32>((x: i32): i32 => x * 2); // needs its <i32>
return doubled[3] + evens.length; // 8 + 2
}