Skip to content

Interfaces

Interfaces describe the shape a value must have. They are a purely type-level construct — an interface generates no runtime code, it only tells the type checker what properties and methods a value is required to have.

Zeus interfaces are structural: any class that has the required members satisfies the interface, with no explicit implements needed (much like TypeScript).

Declaring an Interface

interface Shape {
radius: i32;
area(): i32;
}

An interface may declare:

  • property signaturesname: Type;
  • method signaturesname(params): ReturnType; (no body)
  • readonly propertiesreadonly id: i32;

Structural Conformance

A class conforms to an interface automatically when it has every required member with a compatible type. There is no implements keyword:

interface Shape {
radius: i32;
area(): i32;
}
class Circle {
public radius: i32;
constructor(r: i32) {
this.radius = r;
}
public area(): i32 {
return this.radius;
}
}
// Circle has `radius: i32` and `area(): i32`, so it satisfies Shape.
function describe(s: Shape): i32 {
return 0;
}
function main(): i32 {
let c: Circle = new Circle(42);
return describe(c); // Circle is accepted as a Shape
}

If a class is missing a required member, passing it where the interface is expected is a compile-time error:

class Square {
public side: i32;
constructor(s: i32) { this.side = s; }
}
// error: argument 1 of type 'Square' does not match expected type 'Shape'
// (Square has no area() method)
describe(new Square(5));

Extending Interfaces

An interface can extend one or more other interfaces. A value must satisfy every member of the interface and all of its parents:

interface Named {
id: i32;
}
interface Entity extends Named {
score(): i32;
}
// A conforming class needs `id: i32` (from Named) and `score(): i32` (from Entity).
class Player {
public id: i32;
constructor(i: i32) { this.id = i; }
public score(): i32 { return this.id; }
}

Interfaces as Types

Use an interface anywhere a type is expected — variable declarations, function parameters, and return types. At runtime an interface value is represented exactly like any object (a single pointer), so passing a conforming object is free:

function main(): i32 {
let c: Circle = new Circle(42);
let s: Shape = c; // a conforming object may be stored in an interface variable
return c.radius;
}

Dynamic Method Dispatch

Calling a method through an interface value dispatches dynamically to whatever concrete class the value actually holds — the essence of polymorphism:

interface Shape {
area(): i32;
}
class Circle {
public r: i32;
constructor(r: i32) { this.r = r; }
public area(): i32 { return this.r * this.r; }
}
class Square {
public side: i32;
constructor(s: i32) { this.side = s; }
public area(): i32 { return this.side * 4; }
}
// `s.area()` calls Circle.area or Square.area depending on the runtime value.
function areaOf(s: Shape): i32 {
return s.area();
}
function main(): i32 {
return areaOf(new Circle(5)) + areaOf(new Square(4)); // 25 + 16 = 41
}

Dispatch works for methods declared directly on the interface and for methods inherited via extends.

Property Access

Properties can be read and written through an interface value; the access resolves to the field on whatever concrete object the value holds:

interface Counter {
count: i32;
}
class Tally {
public count: i32;
constructor() { this.count = 0; }
}
function bump(c: Counter): void {
c.count = c.count + 1; // read and write through the interface
}

A readonly interface property may be read but not assigned through the interface:

interface Config {
readonly id: i32;
}
function rename(c: Config): void {
c.id = 5; // error: cannot assign to readonly property 'id'
}

Implementing Interfaces

Conformance is structural, so implements is optional — but you can add it to document intent and have the compiler verify the class satisfies the interface:

interface Shape {
radius: i32;
area(): i32;
}
class Circle implements Shape {
public radius: i32;
constructor(r: i32) { this.radius = r; }
public area(): i32 { return this.radius * this.radius; }
}

If the class is missing a required member, the implements clause reports it: class 'Square' does not implement interface 'Shape'. A class may implement several interfaces: class Button implements Drawable, Clickable { ... }.

Exporting and Importing

Interfaces can cross module boundaries with export / import, just like classes and functions:

shapes.zs
export interface Shape {
radius: i32;
area(): i32;
}
main.zs
import { Shape } from "./shapes";
function useShape(s: Shape): i32 {
return 0;
}

Dispatch works across modules too — the interface and the concrete class may live in different files.

Built-in classes participate too: string, Error, and other primordials are ordinary classes, so they satisfy an interface whenever they structurally match it.

A property may be backed by a field or an accessor: a class satisfies readonly x: T with a get x() and a writable x: T with both get x() and set x(). Reading or writing the property through the interface dispatches to the field (a direct load/store) or the accessor (a getter/setter call) automatically, depending on the concrete object.

Arrays participate too. An array conforms to any interface its members structurally match — e.g. Point[] satisfies interface { readonly length: i32; get(i: i32): Point; }. Every array element type gets its own runtime type handle (a distinct class id) while sharing the underlying array code, so it can be keyed in the dispatch table like any other class:

interface Sized {
readonly length: i32;
}
function sizeOf(s: Sized): i32 {
return s.length;
}
function main(): i32 {
let xs: i32[] = [10, 20];
return sizeOf(xs); // 2 — an array satisfies Sized
}