Classes, structs, interfaces and generics
Every object has a compile-time shape (a fixed layout), so a field access is one load: no hidden classes and no runtime shape checks.
Classes
- Class expressions bound to a variable,
const Point = class { … };(alsolet,export const, withextends/implements, at module level or in a function), declare the classPoint: an anonymous class takes the variable's name, as in JavaScript (console.logshowsPoint { … }). A class expression may have its own name only when it is the variable's (const Point = class Point { … }); another one is an error (a named class expression must have the name of the variable it initializes). Class expressions elsewhere (an argument, a return value,new (class { … })()) are not supported yet. - Instances are references, as in JS (Memory model):
const b = arefers to the same object.x.clone()makes an independent deep copy of any class, struct or union (likestructuredClone), except values owning a[Symbol.dispose]resource, which may defineclone()themselves. Such a class's ownclone()(no parameters, returning the class) is what every deep copy of it calls:x.clone()of an array or object holding it, and the copy a value gets when it goes to another task (Async), which then deep-copies whatever the returned object still shares with the original. A class that owns no resource is deep-copied field by field wherever it is nested; itsclone()runs only when called (x.clone()on it). - Symbol method names:
[Symbol.iterator]()and[Symbol.asyncIterator]()(the iteration protocol),[Symbol.dispose]()and[Symbol.asyncDispose]()(resource cleanup) name methods as in TypeScript and are called asx[Symbol.iterator](). Other symbols do not exist. - Async methods take
thisby value: the promise owns it. - An overridden method returning a promise reports its errors through the promise: when the
base method or any override can fail, all of them must be
async(Async). - A template literal calls a class's (or struct's) own
toString(): string, as in JS (`total: ${price}`); without one it shows the value the wayconsole.logdoes.
class Account {
static readonly LIMIT: i64 = 1000;
readonly owner: string;
private balance: i64 = 0;
constructor(owner: string) {
this.owner = owner;
}
get total(): i64 {
return this.balance;
}
deposit(amount: i64): bool {
if (amount <= 0 || this.balance + amount > Account.LIMIT) {
return false;
}
this.balance += amount; // modifies `this`: inferred, no annotation
return true;
}
}
class Savings extends Account {
rate: f64 = 0.02;
constructor(owner: string) {
super(owner);
}
override deposit(amount: i64): bool {
return super.deposit(amount + 1);
}
}
const a: Account = new Savings("ada");
const ok = a.deposit(100); // dispatched to Savings.deposit
console.log(ok, a.total, a.owner, Account.LIMIT);Construction
Fields
Fields need a type (count: i64 = 0), or an initializer that states one (count = 0,
done = false, items = new Map<string, i64>()). A field declared from an integer literal
(count = 0) holds a JS number, like let count = 0 (Numbers). A field
without a default must be assigned in the constructor.
A declare field (declare readonly code?: string;, TypeScript's field without an emitted
initializer) is declared like one without declare; it cannot have an initializer.
Order of construction
new C(…) allocates the object on the heap and constructs it in JavaScript's order: a class's
field initializers run once its base class is constructed (right after super(…) returns, or
after the inherited constructor when the class has no constructor of its own), in declaration
order, before the rest of its constructor body. So for class D extends B:
- the arguments,
- B's initializers,
- B's constructor body,
- D's initializers,
- D's constructor body.
new throws whatever the initializers and constructors throw (Errors); a
constructor's throws clause covers the initializers it runs (those of its class and of the
classes between it and the next constructor up). Initializers may construct their own class or
classes whose initializers construct it back (next: Node | null = more() ? new Node() : null); a new in such a cycle throws what every class in the cycle's initializers throws.
Before its initializer runs (for example in an overridden method that a base constructor calls)
a field reads as zero or empty (0, false, "", an empty array); reading a field that holds
an object there (a class instance, a Map) is unsound: nothing checks it yet.
Parameter properties
constructor(private readonly name: string, public age: i64) {} declares the fields and
assigns them, as in TypeScript (protected is accepted there and means public: there are no
protected members).
As TypeScript emits them (ES2022 class fields), they are the class's first fields, before the
declared ones, which decides the key order of console.log and JSON.stringify. The
constructor assigns them first (after super(…)), so they are set after the class's field
initializers ran:
function log(s: string): i64 {
console.log(s);
return 1;
}
class Point {
label: i64 = log("label");
constructor(public x: f64, public y: f64) {
log("constructor");
}
}
console.log(JSON.stringify(new Point(1, 2))); // label, constructor, {"x":1,"y":2,"label":1}Private and protected constructors
TypeScript's rules apply:
private constructor(…)can be called (new C(…)) only inside the class body: its methods, static methods, field initializers and the closures in them. Such a class cannot be extended (cannot extend `C`: its constructor is private); a public static method is the usual way to create one.protected constructor(…)may also be called inside the bodies of subclasses (super(…)in their constructors, ornew C(…)).
Elsewhere new C is an error: the constructor of `C` is private: only the body of `C` can call `new C(...)` (or … is protected: only `C` and its subclasses can …). A subclass
without a constructor of its own inherits its base's, with the same rule.
JSON.parse<C> (and v.as<C>()) cannot decode such a class, alone or inside another type,
since decoding fills the fields without running a constructor; JSON.stringify writes it as
usual (velt/encoding/json).
A private constructor with static factory methods, and a protected one for a base class:
class Celsius {
private constructor(readonly degrees: f64) {}
static of(degrees: f64): Celsius | null {
return degrees < -273.15 ? null : new Celsius(degrees);
}
static fromFahrenheit(f: f64): Celsius {
return new Celsius((f - 32.0) * 5.0 / 9.0);
}
}
class Shape {
protected constructor(readonly name: string) {}
}
class Square extends Shape {
constructor(readonly side: f64) {
super("square");
}
}
console.log(Celsius.fromFahrenheit(212.0).degrees, Celsius.of(-300.0) == null); // 100 true
console.log(new Square(2.0).name); // square
// new Celsius(5.0) and new Shape("x") are errors here; so is `class Kelvin extends Celsius`.super(…)
The constructor of a subclass calls super(…), as in TypeScript, also when the base class has
no constructor (super();). Statements may come before it as long as they don't use this or
super.x ('super' must be called before accessing 'this' in the constructor of a derived class) or return; they run first, then the call's arguments, the base constructor, this
class's field initializers and parameter properties, and the rest of the body. This holds also
when the class has initialized fields or parameter properties, which are set right after
super(…) returns, as in JavaScript (TypeScript 4.6+):
class Shape {
constructor(public name: string) {}
}
class Square extends Shape {
sides: i64 = 4;
constructor(size: f64) {
if (size <= 0) {
throw new Error("size must be positive");
}
const name = `square ${size}`;
super(name);
console.log(this.name, this.sides); // square 2 4
}
}
new Square(2);The call runs exactly once on every path: a statement of the constructor's body, or one in each
branch of an if / else (nested ifs too), as TypeScript accepts it; not in an if without
else, a try, switch, loop or closure, and not part of an expression. The field
initializers run right after the call, in either branch, and every field must be initialized,
so a path that skips the call or makes it twice is an error, where TypeScript only fails at run
time. A class with parameter properties, or with field initializers when no base class has a
constructor, needs the call as a statement of the body itself (choose its arguments with a
conditional: super(c ? a : b)).
class Named {
constructor(public name: string) {}
}
class Tagged extends Named {
tag: string = "t";
constructor(short: boolean) {
if (short) {
super("s");
} else {
super("long");
}
console.log(this.name, this.tag); // s t
}
}
new Tagged(true);When a constructor throws (before super(…), in the base constructor or in a field
initializer), new frees the object it allocated, dropping the fields that were set, without
running the class's [Symbol.dispose](): like JavaScript, nothing disposes an object new
never returned.
Inheritance
class B extends A gives single inheritance. The base's fields are a prefix of the subclass
layout, so upcasts are free. A subclass without a constructor of its own inherits its base's.
A base class may be abstract.
A subclass may declare an inherited field again with the same type and an initializer of its
own (class Dog extends Animal { kind: string = "dog" }): it is the same field, which the
subclass's initializer sets after the base class's, in its turn among the subclass's
initializers.
Overriding methods
Redefining a base method requires override, except a method of a standard-library class
(toString() in a subclass of Error), which TypeScript code redefines without it;
super.m() calls the base version.
An override may give a parameter a default of its own: a call that leaves the argument out
takes the default of the method that runs, also through a base-class type (const s: Shape = new Square(); s.scale() uses Square's), through a method value (const f = s.scale; f(),
s.scale.bind(s)) and when the argument is undefined, as in JavaScript. Such a call goes
through a second vtable slot whose entry in each class applies that class's own default, so a
call passing every argument costs the same as a call of a method without such a default.
Dispatch
A method that is never overridden is called directly (and can be inlined). Only overridden
methods go through a vtable, and only where the static type is a base class. A call on an
object whose class is known from where it was allocated (const s: Shape = new Square()) is
usually made direct while the object has not been passed on.
Abstract classes
TypeScript's rules apply: abstract class C cannot be created with new
(cannot create an instance of abstract class `C` ), only its subclasses can.
- It may declare abstract methods and accessors,
abstract area(): number;/abstract get name(): string;, which have no body and must declare their return type (and take no parameter defaults:x?: T, the subclasses give the default; TypeScript's TS2371), and abstract properties,abstract readonly name: string;/abstract count: number;(an abstract getter, and a setter unlessreadonly). - A subclass implements an abstract property with a field (
readonly name = "q") or accessors, and an abstract method with a method or a field of its function type (run: () => number = () => 1). Reading or writing an abstract property throughthisin its class's own constructor is an error (TypeScript's TS2715): no subclass field is initialized yet. - Every concrete subclass implements each abstract member it inherits, or it is an error naming
the class and the missing members
(
class `Square` does not implement the abstract member `area` (from `Shape`)); a subclass that leaves some unimplemented must itself beabstract. - An implementation overrides the abstract method without
override(writing it is allowed), and calls through the base class, also from the base's own methods and constructor, dispatch to it.abstract override m()makes an inherited method abstract again. - An abstract method cannot be
static,private,async, a generator or generic, and cannot be called throughsuper. - Abstract classes are passed as values through
abstract construct signatures,
abstract new () => Shape.
abstract class Shape {
abstract area(): number;
describe(): string {
return `area ${this.area()}`;
}
}
class Square extends Shape {
constructor(public side: number) {
super();
}
area(): number {
return this.side * this.side;
}
}
const shapes: Shape[] = [new Square(2), new Square(3)];
for (const s of shapes) console.log(s.describe()); // area 4, area 9extend methods in subclasses
Instance methods of an extend A block apply to subclasses too: new B().m() finds m in B
and its bases' bodies, then in B's extend blocks, then in each base class's, nearest
first, and super.m() in B finds A's.
Redefining an extend method in a subclass, in its body or in an
extend block of its own. An extend method is not virtual, so a call through an A would
run the extend method: `m` is an `extend` method of `A` and cannot be redefined in a subclass yet. This covers std's too: bytes() and arrayBuffer() and the body
getter of Response and Request are extend methods.
Returning this
class Registry {
names: string[] = [];
register(name: string): this {
this.names.push(name);
return this;
}
}
class Tagged extends Registry {
tags: string[] = [];
tag(t: string): this {
this.tags.push(t);
return this;
}
}
const t = new Tagged().register("a").tag("x").register("b");
console.log(t.names, t.tags); // [ 'a', 'b' ] [ 'x' ]thisas a method's result type (the polymorphicthistype) is the class of the object the method is called on:registercalled on aTaggedgives aTagged, so chained calls keep the subclass's methods, as in TypeScript; called on a value of a type parameter bounded by the class (x.register(n)withx: TandT extends Registry), it gives aT. An override returnsthistoo: written (override register(name: string): this) or left out, when it returnsthis(orsuper.register(name)). An override with another result type (override register(name: string): Registry) is TypeScript's error TS2416.- Such a method returns
this(or the result of anotherthis-returning method called onthis); returning another object of the class is TypeScript's errortype `A` is not assignable to type `this`. Thethistype elsewhere in an instance method's signature or body (a parameterother: this) is the declaring class. In a static method or outside a class it is TS2526; in an interface it is not supported yet.
instanceof downcasts
x instanceof C on a value of a base class of C tests the object's actual class: it is true
for a C and for instances of C's subclasses. Where it holds, x reads as a C, with the
same narrowing rules as for unions (if/else, early returns, &&, ||, !, ternaries,
until x is reassigned; a variable that a closure assigns is not narrowed, as
for null). The test reads the class id in the object's
vtable: one load and one comparison, whatever the depth of the hierarchy.
The same works on an interface value (a Shape holding a Circle), on C | null (true means
not null) and on a union member whose class is a base of C, so catch (e) chains can tell
error subclasses apart. A path of readonly fields (node.left instanceof Num) narrows too; a
mutable field could change before it is read, so copy it into a local first. A field whose type
is a C that may be null or undefined (o.next instanceof Node with next?: Node | null)
is narrowed to C when the test is true, mutable or not, as o.next != null would (a read
checks it again).
Tests joined by || narrow to the nearest class they both imply: with class S2 extends S,
x instanceof S || x instanceof S2 reads x as an S, and with two subclasses T1 and T2
of S, x instanceof T1 || x instanceof T2 does too (when S is not x's own class).
- The narrowed type keeps the type arguments: a
Box<i64>tested forclass Labeled<T> extends Box<T>is aLabeled<i64>. A generic class whose type arguments do not follow from the tested type is an error. - A test that can never be true is an error, as in a union:
a instanceof Rockon anAnimalwhenRockdoes not extendAnimal, or on an interface value when neither the class nor its subclasses implement the interface. (TypeScript accepts these; in Velt classes are nominal, so the answer is known.)
class AppError extends Error {}
class NotFound extends AppError {
constructor(readonly id: string) {
super(`no item ${id}`);
}
}
class Timeout extends AppError {
constructor(readonly ms: i64) {
super(`timed out after ${ms}ms`);
}
}
function fetchItem(id: string): string throws AppError {
if (id == "") {
throw new Timeout(30);
}
throw new NotFound(id);
}
function describe(id: string): string {
try {
return fetchItem(id);
} catch (e) { // e: AppError
if (e instanceof NotFound) {
return `missing ${e.id}`;
} else if (e instanceof Timeout) {
return `timeout after ${e.ms}ms`;
}
return e.message;
}
}
console.log(describe("a"), "/", describe("")); // missing a / timeout after 30msMembers
Visibility
public: the default.private: usable only inside the declaring type's body, including closures, functions and classes declared in its methods, but not in subclasses:`x` is private.#x, an ES private name: usable only in its class's body.protected: only a constructor can beprotected(Private and protected constructors);protectedmembers don't exist (on a parameter property it means public).
console.log shows private fields, as Node shows a TypeScript private field, except
zero-sized ones such as std's runtime markers. The standard library's own modules may use
the private members of its types, which is how std types build each other's handles; this
covers private only: a #x name is usable only in its class's body, in std too.
readonly fields
A readonly field is assignable only in the constructor:
class User {
readonly id: number;
constructor(id: number) {
this.id = id; // allowed: in the constructor
}
reset(): void {
this.id = 0; // error: `id` is readonly
}
}Static constants and fields
static readonly NAME: T = …; constants (Account.LIMIT, Math.PI) are evaluated like
module constants: once, with the class, when the program
modifies, takes over or compares one, or its value is not a constant.
static fields (static count: number = 0;) are module state
like a module-level let: initialized once, where the class is declared, in source order with
the class's static { … } blocks. They are assigned through their class
(Counter.count++, or ++this.count in a static method of Counter) and read through a
subclass (Sub.count):
class Counter {
static readonly LIMIT: number = 10;
static count: number = 0;
}
class Sub extends Counter {}
Counter.count++; // assign through the class
console.log(Sub.count, Sub.LIMIT); // 1 10: read through a subclassAssigning a static field through a subclass (Sub.count = 5, or
this.count = v in a static method called on a subclass) is an error: in JavaScript it
creates a static field of the subclass itself. Assign Counter.count.
A class declared inside a function or block can't have static fields that are not
readonly, or static blocks: JavaScript evaluates them each time the declaration runs,
Velt once.
Static blocks
static { … } blocks run once, where the class is declared, as in JavaScript, in source order
with the class's static fields. A return in one is an error, as in TypeScript.
class Plugins {
static names: string[] = [];
static {
Plugins.names.push("default");
}
}
console.log(Plugins.names); // [ 'default' ]this in a static block: name the class (Counter.count).
Static inheritance
static methods are called through their class. Static and instance members are separate
namespaces, as in TypeScript: a subclass's static m() doesn't hide an inherited instance
method m(), and a class below it can still override that method.
Static methods are inherited, as in TypeScript: with class B extends A {}, B.f() calls
A's static f() (unless B declares a static f of its own, in its body or in an
extend B block), keeping its visibility (a private static one stays usable only in A's
body). The statics of extend blocks are inherited the same way: B.f() looks at B's own
statics, then B's extend statics, then each base class's statics and extend statics,
nearest first.
A static method of a generic class is called without type arguments (Box.wrap(4) for
class Box<T> { static wrap(n: number): number }): a class type parameter the static's
signature doesn't use (in TypeScript statics can't use them) needs no inference, also a
bounded one (class Box<T extends Named>).
this and super in static methods
In a static method, this is the class the method was called on, as in TypeScript:
class A {
static kind(): string {
return "A";
}
static hello(): string {
return "hello " + this.kind();
}
}
class B extends A {
static kind(): string {
return "B";
}
}
console.log(B.hello()); // hello Bthis.f() calls a static method and this.NAME reads a static readonly constant (own or
inherited); other uses of this there are an error (`this` in a static method can only be used to call or read its class's static members). super.f() in a static method calls the
base class's static f, with this unchanged.
These calls are resolved at compile time: a static method using this or super is compiled
once per class below its own that can run it, so there is no dispatch at run time.
As in TypeScript, the body is typed once, with the declaring class's members: when
class DogShelter extends Shelter overrides static create(): Animal with
static create(): Dog, this.create() in Shelter's statics is an Animal also when they
run for DogShelter (which calls DogShelter.create), and a static kind(): 2 overriding
static kind(): number gives a number there. A subclass static whose type doesn't convert to
the one it overrides is an error naming the subclass (`B.kind` has type `string`, which does not convert to `f64` of the member it overrides, noted in `B`'s copy of `A.run` ).
Statics as values
A static method is also a value: const f = C.kind, xs.map(C.double) (passing it where a
function with more parameters is expected calls it with the leading arguments, as for any
named function). this.f passed that way in a static method is wrapped like C.f
(["a"].map(this.plain)).
class C {
static double(x: number): number {
return x * 2;
}
}
const f = C.double;
console.log(f(4), [1, 2].map(C.double)); // 8 [ 2, 4 ]A static method using this or super is not a value (`C.hello` uses `this` or `super`, so it cannot be used as a value): TypeScript accepts it, but JavaScript calls the function
with this undefined, so this.kind() would throw a TypeError. Call it, or wrap the call:
() => C.hello().
ES private names
ES2022 private names (#x) work on fields (#count = 0, readonly #id: string), methods
(#check()), accessors (get #v() / set #v(v), this.#v++ uses both) and statics
(static #make(), static readonly #K = …, used as C.#make() inside the body).
o.#x names the member that the class whose body the code is in declares, on any instance of
that class or a subclass, not only this. A function or class declared in a method is in the
body too. Where class bodies nest, it is the innermost one declaring #x, so a nested class's
own #x shadows the outer one's: o.#x on an outer-class value there is property `#x` cannot be accessed on type `Outer` within this class because it is shadowed by another private identifier with the same spelling (TypeScript's TS18014).
Elsewhere, also in a subclass, it is an error (property `#x` is not accessible outside class `A` because it has a private name), also for an accessor: reading o.#v, assigning
o.#v = 1 or updating o.#v += 1 reports it once.
A subclass may declare its own #x: a second field, not a redeclaration, and a base class
method keeps using the base's. A #m method is never virtual (a direct call) and overrides
nothing (override #m is an error).
#x in o is a brand check: it is o instanceof C, C the class declaring #x, and narrows
like instanceof. As in TypeScript, o must not be possibly null (the right operand of `in` may be null; JavaScript throws a TypeError for null): test
o !== null && #x in o. Nor may it possibly be a primitive (A | string: the right operand of `in` may be a primitive, TypeScript's TS2322); narrow it first
(typeof o !== "string" && #x in o). A union of objects (A | number[]) is fine.
Private names exist only in class bodies: not in interfaces, object types, structs, extend
blocks or parameters, and not with private / public. At run time #x and private x cost
the same (an ordinary field slot, a direct call); they differ where Node differs:
#x | private x | |
|---|---|---|
console.log, template literals | hidden: A { y: 2 } | shown |
JSON.stringify | skipped | written |
JSON.parse<C> | error: decoding cannot set it | error, the same |
Object.keys | skipped | listed |
{ ...o } | skipped | copied, unless private to another type |
x.clone() | copied | copied |
| name space | #x and x coexist; a subclass may reuse #x | one name per class chain |
class Counter {
#count = 0;
label: string = "clicks";
inc(): number {
this.#count++;
return this.#count;
}
static isCounter(o: Counter | string): boolean {
return typeof o !== "string" && #count in o;
}
}
const c = new Counter();
c.inc();
const n = c.inc();
console.log(c, n, JSON.stringify(c)); // Counter { label: 'clicks' } 2 {"label":"clicks"}Getters and setters
get size(): T { … } is read as a property (x.size) and cannot be called or assigned;
set size(v: T) { … } runs on x.size = v. A getter's type may be inferred from its return
like a method's (Return types); an interface getter writes it.
Getters cannot be static or async.
With both, x.size += 1, x.size++ and x.size ??= v (also ||=, &&=) use both, as in JS:
x is evaluated once, then the getter runs, then the right-hand side, then the setter (which
??=, ||= and &&= skip when the old value decides). The setter runs on the object x
named before the getter and the right-hand side ran, even when one of them assigns another one
to x. A getter may change its object (a signal recording who read it).
As a value, x.size = v is v (converted to the setter's parameter type, but non-null when
v is; the getter is not read again; an object is shared, not copied) and x.size ??= v is
the old value where it decides, else v (so non-null when v is).
Implementations and overrides of a getter or setter must be accessors too. An override of only
the getter of a base class's get/set pair replaces the whole property, as in JS: writing it
through the subclass is an error (TypeScript's TS2540), and through the base class, an
interface or an intersection it stops with Node's
TypeError: Cannot set property n of #<S> which has only a getter. Overriding only the setter
is an error: the setter-only accessor would hide the base getter (reads give undefined in
JS), so override the getter too.
Structs
struct declares an object type with the same members as a class (methods, getters,
private, static). It is built with a struct literal Point { x: 1, y: 2 } or an object
literal where a Point is expected. A struct has no constructor and no inheritance
(implements is allowed). Like every object it is a reference
(Memory model): assigning or passing it shares it. The
compiler stores it inline (no heap allocation, no count) as long as the program never shares a
value of the type, or never changes one in place. == compares structs by identity, as for
every object, but Map and Set keys of a struct type compare and hash by content, field by
field (Map).
Planned: Velt is to have no struct keyword: plain data will be type Name = { … } with
extend Name { … }, and everything else a class.
struct Vec2 {
x: f64;
y: f64;
len(): f64 {
return Math.sqrt(this.x * this.x + this.y * this.y);
}
}
let p = Vec2 { x: 3.0, y: 4.0 };
const q = p; // the same object
p.x = 0.0;
console.log(p.len(), q.len()); // 4 4Interfaces
- An interface lists the methods, getters, setters and fields an implementing type must have,
and may give methods default bodies (mixins).
class C implements I, Jtakes any number;interface B extends A, Cinherits everything. - An interface with methods (or getters or setters, own or inherited) is nominal: a type
implements it only by declaring
implements(or through anextendblock), or by being an object literal where it is expected (Object literals implementing interfaces). - Used as a generic bound (
<T extends Named>), an interface is resolved at compile time (direct calls). Used as a value type (Named[]holding different classes), it is a fat pointer (data plus vtable), like Rust'sdyn. - An optional method
m?(x: T): Ris an optional field of function type (m?: (x: T) => R): an implementing class declares the field asm?: (x: T) => Rorm: ((x: T) => R) | undefined = undefined, and a caller writeso.m?.(x). - A default body can be
async(async load(): Promise<T> { … }), with the rules of an async class method (Async). A method without a body cannot be: like in TypeScript, it declares aPromiseresult, and implementations may beasync. - A method without a body declares that a parameter may be left out with
x?: T; a default value (x: T = 1) is an error, as in TypeScript (a parameter initializer is only allowed in a function or constructor implementation): each implementation gives its own (step(by: number | undefined = 1)in the class). A default body may have defaults, as any method.
interface Named {
name(): string;
greet(): string {
return `hi ${this.name()}`; // default method
}
}
class Dog implements Named {
name(): string {
return "rex";
}
}
class Cat implements Named {
name(): string {
return "tom";
}
greet(): string {
return "meow";
}
}
function loudest<T extends Named>(x: T): string { // monomorphized: no vtable
return x.greet().toUpperCase();
}
const pets: Named[] = [new Dog(), new Cat()]; // interface values: dynamic dispatch
for (const p of pets) {
console.log(p.greet());
}
console.log(loudest(new Dog()));Interfaces with only fields
An interface with only fields (at least one, no methods, extending only such interfaces)
describes data: it is an object type, like type User = { … }, so object literals satisfy it,
field reads are loads and it has a JSON form. It converts to and from object types with the
same fields and stays the same object.
class C implements User checks that the class has the fields, but a class instance is not a
User (it is shared by reference; build one from its fields). As a bound, <T extends HasId>
is satisfied by any type with the fields (a struct, class or object type, not generic).
An interface may refer to itself through a field (next?: Node, parent?: Category, or a
union such as left: Tree): such an object is stored behind a pointer, like a class instance,
so its size is finite. Like JavaScript, JSON.stringify of an object that contains itself
(n.next = n) fails: it panics with "converting circular structure to JSON"; console.log
prints one like Node, <ref *1> { value: 1, next: [Circular *1] }. A struct that contains
itself is still an error (recursive type `S` has infinite size): structs are stored
inline.
interface User { // only fields: an object type
readonly id: i64;
name: string;
email?: string;
}
interface HasId {
id: i64;
}
function idOf<T extends HasId>(x: T): i64 { // any type with an `id: i64` field
return x.id;
}
const u: User = { id: 1, name: "ann" }; // an object literal satisfies it
console.log(JSON.stringify(u), idOf(u));Fields through an interface
Fields are read and written through an interface value or a T extends I as in TypeScript:
hook.owner = null, sub.flags |= DIRTY, sub.runs++, sub.dep ??= d. The write changes the
object behind the value (a struct converted to an interface value is a copy, as structs always
are); a readonly field cannot be assigned (cannot assign to `id`: it is a readonly field).
Reads go through a getter in the interface's table and writes through a setter that only
interfaces the program writes through get, so other programs pay nothing; through a
T extends I both are direct calls. An object literal implementing such an interface with
get f() must give set f(v) too. A class implements an optional field x?: T with a field
x?: T or, as in TypeScript, with a plain x: T: read through the interface it is present, and
writing undefined through the interface stops with a TypeError (the class's field holds no
undefined).
Changing a part of a field (x.t[0] = 5 on a tuple field, x.pos.p[1] += 1, x.t[0]++)
changes the field the object holds, as in JS: the field is read through its getter, the part
changed and the field written back through its setter. The receiver is evaluated once, and a
right-hand side with an effect (a call, an await) runs before the field is read, so a change
it (or another task) makes to the same field is kept.
readonly is shallow, as in TypeScript: x.o.v = 5 and x.t[0] = 5 on a readonly field
work, written back through a setter the interface keeps for this; an object literal that
implements the field with only get o() then needs set o(v) too.
interface Hook {
currentOwner: number | null;
nextId(): number;
}
class DevHook implements Hook {
currentOwner: number | null = 7;
nextId(): number {
return 1;
}
}
function withoutOwner(h: Hook, f: () => void): void {
const prev = h.currentOwner;
h.currentOwner = null; // through the interface value
f();
h.currentOwner = prev;
}
const hook = new DevHook();
withoutOwner(hook, () => console.log(hook.currentOwner)); // null
console.log(hook.currentOwner); // 7Generic methods
Generic methods (apply<U>(f: (x: i64) => U): U[]) are called on a concrete class, on a
T extends I generic (resolved for each instantiation, also when an interface value stands for
T), or on an interface value: there the method's table entry is itself a table, with one entry
per list of type arguments the program calls it with, compiled for every class implementing the
interface. A generic method calling itself through an interface value with a growing type
argument (r.go([x]) in go<T>(x: T)) has infinitely many instantiations and is an error, as
for a direct call.
interface Mapper {
apply<U>(f: (x: number) => U): U[];
}
class Two implements Mapper {
apply<U>(f: (x: number) => U): U[] {
return [f(1), f(2)];
}
}
const m: Mapper = new Two();
console.log(m.apply((x) => `${x}`), m.apply((x) => x * 10)); // [ '1', '2' ] [ 10, 20 ]Converting a class to such an interface where a subclass overrides the generic method (a Base
that may hold a Sub would run Base's method; converting a new Base() or the Sub itself is
fine), also when the value comes from any, unknown or object (which keep the type it had
there), and converting an interface value with a generic method to other type arguments
(H<number> as H<unknown>). Both are errors where the program calls that generic method
through interface values. Generic interface methods cannot have default bodies yet.
The method that runs is the one of the class the type parameter is given: when a subclass of that class overrides it, a value of the class may hold the subclass, so instantiating the generic with that class is the same error as calling the method on it directly (pass the concrete class), and an interface value cannot stand for such a type parameter.
A call that passes exactly that class is allowed: a new C() given directly to a parameter of
type T of a function or of a generic method, or a const initialized with new C(). Other
variables, a value inside another parameter's type (get: () => T, xs: T[]), a default
(y: T = x) and a class instantiated with C (new Holder<C>(new C())) don't count.
Declaration merging
As in TypeScript, an interface declared again in the same module is one interface with the
members of every declaration: the first declaration's members first, then each later one's.
declare module "spec" { interface X { … } } adds members to the interface X that module
declares (also when it re-exports it from another module), wherever the augmentation is: the
interface has the same members in every module. declare global { interface X { … } } merges
into an interface X of its own module, or declares one there.
interface Point {
x: number;
}
interface Point {
y: number;
}
const p: Point = { x: 1, y: 2 };
console.log(p.x + p.y); // 3- The declarations' type parameters must have the same names (TypeScript's TS2428), all of
them are exported or none (TS2395), and their
extendslists are joined. - A member declared in two declarations is an error. TypeScript allows a repeated field of the same type, and makes a repeated method an overload.
- An interface merges only with interfaces, not with a class or enum of its name. Next to a function or constant of its name it is that name's type, the function its value (Types and values of one name).
var NAME: T;indeclare globaldeclares a build-time constant (seedefine).- A namespace of types in
declare globalis declared in its module, as adeclare namespace(Namespaces), like adeclare globalinterface. - Not supported yet: other declarations in
declare globalanddeclare moduleblocks (functions, namespaces indeclare module,varindeclare module), declaring a new interface in another module withdeclare module, adeclare globalinterface visible in other modules, and a shorthanddeclare module "spec";.
Generics
Classes, structs, interfaces and functions take type parameters (class Stack<T>,
interface Box<T>, function f<T extends Comparable<T>>). Every instantiation is compiled
separately (monomorphization): no boxing, and bounds resolve to direct calls. Bounds are
interfaces (an interface with only fields is satisfied by any type with its fields; an
interface value by its own interface and the ones it extends, so f<Person> fits
function f<T extends Named> with interface Person extends Named, and its methods are
called through the value's table) or, as in
TypeScript, other types a type argument must be assignable to (T extends Record<string, V>,
T extends string | number; Generic functions).
A class bound (T extends Base, E extends Error) is satisfied by the class and its
subclasses (classes are nominal: another class with the same fields is not one), and inside the
generic body a T has the class's fields, getters, setters and methods, and converts to the
class (const b: Base = x). Each instantiation reads them from its own class: an overriding
method runs, and a T returned keeps the subclass's type (touch(new Sub()).extra).
class Shape {
name: string = "shape";
area(): number {
return 0;
}
}
class Square extends Shape {
side: number = 2;
override area(): number {
return this.side * this.side;
}
}
function label<T extends Shape>(s: T): T {
s.name = `${s.name} of ${s.area()}`;
return s;
}
class NotFound extends Error {}
function describe<E extends Error>(e: E): string {
return `${e.name}: ${e.message}`;
}
const sq = label(new Square());
console.log(sq.name, sq.side); // shape of 4 2
console.log(describe(new NotFound("no user"))); // Error: no userType parameters may have defaults (interface Iterator<T, E = never>), used when a type leaves
the argument out (Iterator<i64> is Iterator<i64, never>) and when nothing infers
it in a new or a static method call (new D(1) of class D<T = i64> is a D<i64>);
functions and methods take them too (Generic functions). As
in TypeScript, a default may use only the parameters declared before it, a parameter without a
default may not follow one with a default, and a default must not need its own declaration's
defaults again (type parameter `T` has a circular default for class S<T = S>).
class Stack<T> {
items: T[] = [];
push(x: T) {
this.items.push(x);
}
pop(): T | undefined {
return this.items.pop();
}
}
const s = new Stack<string>();
s.push("a");
s.push("b");
console.log(s.pop() ?? "empty", s.items.length);extend
An extend block adds methods, getters, setters and static methods to an existing type,
builtins included, at zero cost (the calls are direct):
- Any type:
extend string,extend i64,extend Array<string>, classes, structs and unions (including discriminated unions:extend Shape { area(): f64 { switch (this.kind) … } }). - Generic and blanket extensions:
extend<T> Array<T> { … },extend<T extends Named> T { … }(every implementor gets the method).
Function types
extend<R> (() => R) { … } applies to every function without
parameters (closures, named functions, values of a function type), extend<A, R> ((a: A) => R) to those with one, and extend<A, R> ((...xs: A[]) => R) to those with a rest parameter
only. A function type written without throws in a target fits a function that throws
anything: it has a type parameter of its own for that, so this in the block is the
receiver with its error type, and calling this() throws what the receiver throws. That
parameter has no name in the block (a type E in scope keeps its meaning there); messages
show it as E, or as E1, E2, … when the block, one of its methods or its scope has a
type of that name, and hovers leave it out of the method's type parameters. To name it,
write the parameter (extend<R, E> (() => R throws E)); a fixed error type (() => R throws Error) is an error. A function type written without throws in a method's
signature can't hold this (self(): () => R { return this; } is an error: expected () => R, found () => R throws E), as a function type's error type parameter isn't assignable to
such a site anywhere; write the parameter and return () => R throws E. A function type in a type alias the target names (extend<R> Thunk<R>) or inside another type (extend<R> Array<() => R>) is matched the same way. A
generic function's value is not of a function type, and gets only blanket blocks.
extend<R> (() => R) {
twice(): R[] {
return [this(), this()];
}
}
let n = 0;
const next = () => ++n;
console.log(next.twice()); // [ 1, 2 ]Static methods
extend Point { static origin(): Point { … } } is called as
Point.origin(). A static method of a primitive type (i8 … u64, isize, usize, f32,
f64, bool, string) is called through the type's name, f64.half(3), unless the program
declares a value of that name. As for a class, the static is a value too (const h = f64.half;, f64.half?.(2)), and this in such a static is the type (this.half(x) is
f64.half(x)). The name is no value otherwise (const t = f64; is an error), and f64.m()
of a method that is not static is an error naming it. Blanket extensions (extend<T extends I> T { static … }) give no statics to primitive types, as they give none to classes.
extend f64 {
static half(x: f64): f64 {
return x / 2;
}
}
console.log(f64.half(3)); // 1.5- A type's own member wins over an extension. Among the extensions that apply (target matches,
bounds hold), the most specific wins: block A is more specific than block B when A's target
is an instance of B's and not the other way round, so
extend Array<string>wins overextend<T> Array<T[]>, which wins overextend<T> Array<T>(the prelude'sjoinonstring[][]is its own). With the same target, a block with bounds (extend<T extends Comparable<T>> Array<T>) wins over one without. When no single block is more specific than every other one (extend<T> Map<string, T>andextend<K> Map<K, i64>on aMap<string, i64>, or two blocks with the same target that both have bounds or both have none), a call is an error:ambiguous extension method `m`, naming both blocks. privateis not allowed inextend, and an extension cannot add fields (the layout is fixed).- A type becomes
Comparableby definingcompareToin anextendblock (Comparable), andIterable<T, E>by defining[Symbol.iterator]()returning anIterator<T, E>, or as a generator method (*[Symbol.iterator](): Generator<T, E>);[Symbol.asyncIterator]()makes it anAsyncIterable<T, E>(Iterables). A generator method there that throws must write its error type in its result (Generator<T, E>): the interface's type is fixed before bodies are checked. - Scope: an extension applies wherever its module is loaded;
extendblocks cannot be exported. - Planned:
static readonlyconstants inextend; module scoping (an extension is visible where it is imported) with ambiguity errors.
type Shape = { kind: "circle"; r: f64 } | { kind: "square"; side: f64 };
extend Shape {
area(): f64 {
switch (this.kind) {
case "circle":
return Math.PI * this.r * this.r;
case "square":
return this.side * this.side;
}
}
}
extend string {
get initial(): string {
return this.slice(0, 1).toUpperCase();
}
}
extend<T> Array<T> {
count(): i64 {
return this.length as i64;
}
}
class Point {
x: f64 = 0.0;
y: f64 = 0.0;
}
extend Point {
static at(x: f64, y: f64): Point {
const p = new Point();
p.x = x;
p.y = y;
return p;
}
}
function describe(s: Shape): string {
return `${s.kind} ${s.area()}`;
}
console.log(describe({ kind: "square", side: 2.0 }), "velt".initial, [1, 2, 3].count(), Point.at(1.0, 2.0).y);extend … implements
extend Target implements I, J { … } makes the block's methods implement interfaces for every
type the target matches, built-in types included: the type then satisfies I as a bound and
converts to an I value, as a class with implements I does. The block is checked as a class
is: each method of I must be in it with the same parameter and result types (a static method
does not count), and a method I gives a default for may be left out.
- Blanket blocks (
extend<T extends Comparable<T>> T implements Describe) give the interface to every type that meets their bounds, type parameters included: infunction f<K extends Comparable<K>>(k: K),k.describe()works andkmeets aT extends Describebound (show(k)withfunction show<T extends Describe>(x: T)). A value of a type parameter does not convert to aDescribevalue, whatever its bounds. - A type's own implementation comes first: a class that says
implements Describe, or inherits it from a base class, keeps its own methods, and a blanket block applies to the types without one. A block for a type that already implements the interface is an error. - A type implements an interface once, with the arguments of the block that wins: two
blocks for
WimplementingG<i64>andG<string>conflict, and withextend<T> Box<T> implements G<T>andextend<A> A implements G<i64>, aBox<string>is aG<string>only. A block with bounds must give the same arguments as the blocks it wins over (those it is more specific than, and those for a base class of a type it fits): withextend<T extends Q> T implements G<i64>andextend<T> T implements G<string>, whether a type is aG<string>would depend on whether it is aQ, so the pair is refused. - Blanket blocks chain: with
extend<T extends K> T implements Jandextend<T extends J> T implements I, aT extends Kin generic code is anItoo, through chains of any length. - Interface values meet bounds on their interface and the ones it extends: with
extend<T extends P> T implements Q, aPvalue (or aP2value,interface P2 extends P) is aQ, and so is aBox<P>withextend<T extends P> Box<T> implements Q. - A block's bounds hold when the type meets them at all, through its own implementation or
another block: with
extend<T extends Q> T implements P,extend<T> T implements Pandextend<T extends P> T implements Q, every type is aPand aQ, and gets the boundedPblock. A type never meets a bound only through itself. - The most specific block wins for each other type, by the rule above: with the blanket block and
extend u64 implements Describe, au64uses theu64block everywhere, in generic code too (f(5 as u64)calls it throughK), and every other comparable type uses the blanket one. Generic code is compiled for each type it is used with, so the call is direct: specializing a generic function or class for some types costs nothing at run time.
Errors
A missing or mismatched method; two blocks for one interface that can both fit a
type where neither is more specific (conflicting implementations of `I` , naming a type
both fit: extend<A> Pair<A, i64> and extend<B> Pair<i64, B> both fit Pair<i64, i64>),
in one module or in two; a block with bounds that wins over another with other interface
arguments (the block for `G<i64>` has bounds and takes precedence over the other); a block for an interface the type already implements
(`P` already implements `I`, also for an interface type and an interface it extends); an
interface with fields or optional methods (cannot implement `I`: `I` declares fields: an
extend block cannot add fields); a target that is any, unknown, never or a union (a
union's value has one member's type: implement the interface for each member); a blanket
block bounded by the interface it implements (extend<T extends I> T implements I), which
no type could get; a bound of any, unknown, object or Function on a block that lists
implements (leave out a bound every type meets). Blocks whose bounds only lead to each other
(extend<T extends I> T implements J with extend<U extends J> U implements I) give a type
neither interface unless it has one of its own.
- A value converted from
anyorunknowntoIuses the implementation a direct conversion of the value would. extend X implements Comparable<X>may be written out too; a block that only definescompareToor[Symbol.iterator]()implementsComparableorIterablewithout it (above).
interface Describe {
describe(): string;
}
extend<T extends Comparable<T>> T implements Describe {
describe(): string {
return "a comparable value";
}
}
extend u64 implements Describe {
describe(): string {
return `the u64 ${this}`;
}
}
function label<K extends Comparable<K>>(k: K): string {
return k.describe();
}
const items: Describe[] = [7 as u64, "x"];
console.log(label(5 as u64), label("abc"), items.map((d) => d.describe()));
// the u64 5 a comparable value [ 'the u64 7', 'a comparable value' ]Comparable
The prelude declares interface Comparable<T> { compareTo(other: T): i64; } and implements it
for every number type, string (UTF-16 code-unit order, as <) and bool. With T extends Comparable<T>, the
operators <, <=, > and >= work on T (static dispatch after monomorphization), and
sort() orders Comparable elements (floats put NaN last). User types implement it with
implements Comparable<X> or an extend block, and then <, <=, > and >= work on their
values too (v1 < v2 is v1.compareTo(v2) < 0).
class Version implements Comparable<Version> {
major: i64;
minor: i64;
constructor(major: i64, minor: i64) {
this.major = major;
this.minor = minor;
}
compareTo(other: Version): i64 {
return this.major != other.major ? this.major - other.major : this.minor - other.minor;
}
}
function max<T extends Comparable<T>>(xs: T[]): T | undefined {
let best = xs.pop();
for (const x of xs) {
if (best !== undefined && x > best) {
best = x;
}
}
return best;
}
console.log(max([3, 9, 2]), max(["pear", "zoo"]), max([new Version(1, 2), new Version(1, 10)])?.minor);