Type Annotations
Beamtalk uses gradual typing: annotations are optional. Unannotated code runs without type errors. Adding annotations enables tooling (IDE hints, docs, future type checking) but never breaks existing code.
Syntax:
param :: Type — annotate a method parameter
-> ReturnType — annotate the return type
field: slot :: Type — annotate a Value slot (or state: for Actor)
The :: delimiter was chosen because : alone is ambiguous in Smalltalk
keyword syntax (it's the selector delimiter). Double-colon is borrowed from
Haskell where it means "has type".
Note: Type annotations are currently parsed and stored in the AST, but type checking is not yet enforced at compile time. They serve as documentation and will drive future tooling.
Method parameter annotations
Annotate a parameter by adding :: Type after the parameter name.
The return type goes after all parameters, as -> Type before =>.
These all run as normal, they're just self-documenting:
// Unary with return type:
// getValue -> Integer => self.value
// Keyword with typed parameter:
// deposit: amount :: Integer => ...
// Keyword with typed parameter and return type:
// deposit: amount :: Integer -> Integer => ...
// Multiple parameters, all typed:
// transfer: amount :: Integer to: target :: BankAccount => ...
// Mixed: some typed, some untyped:
// inject: initial into: block :: Block => ...
Data slot annotations
Annotate instance variable slots with the keyword for your class kind:
// Value subclass — use field:
// field: x :: Integer = 0
// field: name :: String = ""
// Actor subclass — use state:
// state: balance :: Integer = 0
// state: owner :: String = ""
Union types and special types
Return Integer or String:
// -> Integer | String
Return Self (the same class as the receiver):
// collect: block :: Block -> Self => ...
Nil (no meaningful return):
// do: block :: Block -> Nil => ...
Built-in type names
Common types recognised by the type system:
Integer Float Number (Integer | Float)
String Symbol Character
Boolean Nil
Block Object Self
Array Dictionary Collection
User-defined class names are also valid types: Point, BankAccount, Counter.
Any user class name works — these are resolved at class load time.
Typed classes — the typed keyword
Prefix your class declaration with typed to declare it as a typed class.
This enables the compiler to verify annotation syntax and store type
metadata for tooling.
A typed value class
typed Value subclass: Ch15TypedPoint
field: x :: Integer = 0
field: y :: Integer = 0
getX -> Integer => self.x
getY -> Integer => self.y
distanceSquared -> Integer =>
(self.x * self.x) + (self.y * self.y)
TestCase subclass: Ch15TypedClasses
testTypedValueClass =>
p := Ch15TypedPoint new: #{#x => 3, #y => 4}
self assert: p getX equals: 3
self assert: p getY equals: 4
self assert: p distanceSquared equals: 25
A typed actor
typed Actor subclass: Ch15TypedCounter
state: value :: Integer = 0
increment -> Integer =>
self.value := self.value + 1
self.value
add: amount :: Integer -> Integer =>
self.value := self.value + amount
self.value
current -> Integer => self.value
testTypedActor =>
c := Ch15TypedCounter spawn
self assert: c increment equals: 1
self assert: (c add: 5) equals: 6
self assert: c current equals: 6
Typed vs untyped
| Aspect | Object subclass: | typed Object subclass: |
|---|---|---|
| Annotations | Optional, ignored | Optional, stored as metadata |
| Runtime behavior | Identical | Identical |
| Tooling | None | IDE hints, generated docs |
| Future checking | Not eligible | Opt-in type checking |
Practical examples (runnable)
Since annotations are ignored at runtime in the current implementation, these examples run identically to unannotated versions. We assert on behaviour, not on the annotations themselves.
3 + 4 // => 7
"hello" size // => 5
Why bother with annotations?
Even without enforcement, annotations give you:
- Documentation — readers know what types are expected
- IDE tooling — hover types, autocomplete (when LSP supports it)
- Generated docs —
beamtalk docincludes types in HTML output - Future checking — the type checker can be enabled incrementally
The philosophy: "Annotate what you know, leave unknown things unannotated." Don't annotate everything for the sake of it — annotate public APIs and complex internal methods first.
Canonical formatting
beamtalk fmt normalises annotation spacing:
Canonical: deposit: amount :: Integer -> Integer =>
Also accepted: deposit: amount::Integer -> Integer =>
Always put a space on both sides of ::.
Summary
param :: Type — parameter annotation
methodName -> ReturnType => — return type
field: slot :: Type = default — Value slot annotation
state: slot :: Type = default — Actor slot annotation
typed Value subclass: ... — typed value class
typed Actor subclass: ... — typed actor
- Union:
-> Integer | String - Special:
-> Self,-> Nil
Annotations are optional and currently informational. Type checking is planned for a future release.
Exercises
1. Annotate a method. Write a method signature for add: a :: Integer to: b :: Integer -> Integer
and explain what each :: and -> means.
Hint
add: a :: Integer to: b :: Integer -> Integer =>
a + b
a :: Integer— parameterahas typeIntegerb :: Integer— parameterbhas typeInteger-> Integer— the return type isInteger::means "has type",->means "returns"
2. Typed class. Write a typed Object subclass: Temperature with a
degrees :: Float slot and a isFreezing -> Boolean method that checks if
degrees is below 0.
Hint
typed Value subclass: Temperature
field: degrees :: Float = 0.0
isFreezing -> Boolean => self.degrees < 0
The typed keyword enables type metadata storage. Annotations are currently
informational — they don't enforce types at runtime.
3. When to annotate. What's the practical difference between
typed Object subclass: and plain Object subclass: today? When would you
choose one over the other?
Hint
Both behave identically at runtime. typed stores annotation metadata for
IDE tooling, generated docs (beamtalk doc), and future type checking.
Choose typed for public APIs and libraries. Plain classes are fine for
scripts, tests, and internal code where documentation overhead isn't worth it.
Next: Chapter 16 — BEAM Interop