Types
E# has a small set of type kinds. The split that matters most is value vs. identity: struct
is value-semantic by default, class is the object world you opt into.
struct — the value-semantic default
Section titled “struct — the value-semantic default”struct is the kind you reach for most. It carries a value-semantic contract:
- Copy-on-assign —
let b = acopies; mutatingbnever touchesa. - No object identity — two
structvalues with equal fields are equal; there’s no reference identity. - Value-shaped equality — field-wise by default.
- No shared mutation through aliases — every binding is its own value.
nilis invalid on plainT— nullability requires*TorT?.
struct Point { x: int, y: int }
let a = Point { x: 1, y: 2 }let b = a // a copy — mutating b never touches aTo a C# eye, struct reads a lot like a record — value equality, with, copy semantics — but
record is a C#-language construct, not a CLR primitive. struct is E#‘s own take: it lowers to a plain CLR struct, with the value-equality and with machinery generated directly.
Fields can be written on one line (comma-separated) or one per line. Construction is the composite
literal T { field: value }; there are no constructors on struct — an init block on a struct is an
error (ES3012). If construction needs logic, write a factory function:
func makePoint(x: int, y: int) -> Point = Point { x: x, y: y }A struct can’t contain itself by value (that would be infinitely large) — break the cycle with a pointer
(ES2002). See Pointers & memory.
struct Node { value: int, next: *Node } // *Node, not Nodestruct Tree { value: int, children: List<*Tree> } // through a generic container tooDeclaration order is free. A type may reference another declared later in the file, and two types may reference each other — the compiler resolves all type names regardless of order:
struct Field { key: string, value: Json } // references Json, declared belowref union Json { jnull jobj(fields: List<Field>) // ...which references Field — a mutual cycle}Field mutability
Section titled “Field mutability”Fields are mutable by default and use the bare declaration form. let and var are properties at member
scope, so they provide the read/construction and read/write property forms rather than alternate field
spellings:
class Cursor { position: int, let label: string, var offset: int }Positional form
Section titled “Positional form”Sugar for the full form plus positional construction — you still get the composite form too:
struct Vec2(x: int, y: int) // construct as Vec2(3, 4) or Vec2 { x: 3, y: 4 }readonly struct
Section titled “readonly struct”Makes every bare field readonly, and it also emits [IsReadOnly] on the struct, telling the JIT to skip
defensive copies on in parameters:
readonly struct RegisterFile { rax: long, rbx: long, rip: long, flags: int }with — non-destructive update
Section titled “with — non-destructive update”Copy a value and overwrite specific fields, producing a new value — zero allocation, zero heap. with
is value-only (using it on a class is an error):
let p1 = Point { x: 3, y: 4 }let p2 = p1 with { x: 10 } // p1.x == 3, p2.x == 10, both y == 4Struct embedding
Section titled “Struct embedding”A bare type name as a field embeds it; the embedded type’s fields and methods are promoted — reachable
directly on the outer type (t.x desugars to t.Vec2.x; the outer type’s own members shadow promoted
names). Pointer embedding (*T) promotes through auto-deref:
struct Transform { Vec2, scale: double } // t.x, t.magnitude() reach into Vec2struct Entity { *Vec2, name: string } // promoted through the pointer (nullable)Generic struct
Section titled “Generic struct”Type parameters are reified — each instantiation is a real closed type at runtime, not an erasure:
struct Pair<A, B> { first: A, second: B }let p = Pair<int, string> { first: 1, second: "x" }Positional form, Deconstruct, and required fields
Section titled “Positional form, Deconstruct, and required fields”struct Vec2(x: int, y: int) is the positional shape — it declares the fields and a positional
constructor (Vec2(3, 4)) and a synthesized Deconstruct, so a value destructures by position:
struct Vec2(x: int, y: int)let v = Vec2(3, 4)let (x, y) = v // x == 3, y == 4 — via the synthesized DeconstructA field may be marked required — then a composite literal must set it; omitting one is a
compile error (other fields keep their silent zero-default). It emits [RequiredMember], so a C#
object initializer enforces the same coverage:
struct Span { required lo: int required hi: int label: string // still optional}let s = Span { lo: 2, hi: 9 } // ok// let bad = Span { lo: 2 } // error: required field 'hi' not setBrowse struct examples → · embedding →
class — identity and the object world
Section titled “class — identity and the object world”class is a CLR class: heap-allocated, reference equality, GC-tracked. Reach for it when you
genuinely want identity, shared mutable state, framework interop, or constructors. Methods live
directly in the body, init(...) blocks are real constructors, and members can carry defaults that run
before the init body.
class Server { let host: string = "localhost" // read property var port: int = 8080 init(port: int) { self.port = port } func describe() -> string = "{self.host}:{self.port}"}Fields, properties, and location-aware properties
Section titled “Fields, properties, and location-aware properties”| Form | Meaning |
|---|---|
name: T | mutable field |
let name: T | stored read property with implicit readonly loca |
var name: T | stored read/write property with implicit writable loca |
let name: T => expression | computed read property; no implicit location |
var name: T { get => read() set(v) => write(v) } | behavioral read/write property; no hidden storage |
self.name: T = value in init | constructor-owned field |
Use loca when a property deliberately exposes stable identity. Use scoped mut when it lends a working
location and must run policy afterward:
namespace Example
class Meter { let label: string = "requests" var limit: int = 100 let full: bool => self.value >= self.limit
init(value: int) { priv self.storage: int = value }
var value: int { loca => &self.storage }
let guarded: int { mut { var working: int = self.storage yield &working if working < 0 { self.storage = 0 } else { self.storage = working } } }}
func addOne(value: *int) { value += 1 }
func run() -> int { let meter = Meter(40) addOne(&meter.value) addOne(&meter.guarded) return meter.value // 42}The constructor declares private storage without putting a private field at the top of the class. value
exposes that storage durably; guarded lends it only for one borrowing call and resumes even if that call
throws. The exact capability and escape rules are in
Properties and object initialization.
An interface can require that durable identity with var value: int { get set loca }; callers may then borrow
&counter.value through the interface itself, while the implementation keeps its storage private.
Interface properties must be implemented by let/var properties. A bare name: T field remains a field;
declaring conformance never manufactures property accessors for it.
Constructors compose. A class may declare several init blocks (distinguished by arity and
argument names, never types), a secondary may delegate to a sibling with : this(...), and parameters
take defaults so one signature covers many call shapes; visibility is priv init / protected init:
class Conn { var host: string var port: int init(host: string, port: int = 80) { self.host = host self.port = port } init(host: string) : this(host, 443) { } // delegates to the 2-arg init}let c = Conn(host: "db", port: 5432) // named argumentsPrimary-constructor capture. A positional header on a class is its primary constructor; a
header parameter used in a method is captured into a synthesized private field — no field declaration,
no self.x = x. A param-less init { } is the primary’s epilogue:
class UserService(store: IUserStore, cache: ICache, maxRetries: int = 3) { var tokens: int = maxRetries // a member default may read a header param init { if maxRetries <= 0 { tokens = 1 } } // epilogue runs after capture + field defaults func lookup(id: Guid) -> User = cache.get(id) ?? store.find(id) // cache/store captured}let svc = UserService(store: sqlStore, cache: memCache)Inheritance is opt-in and sealed by default (open / abstract, virtual / abstract / : func,
init(...) : base(...)) — it has its own page: Inheritance.
Browse inheritance examples →
union — tagged unions
Section titled “union — tagged unions”A union is a sum type: a value that is exactly one of several named variants, each carrying its own
payload. Emits as a tag enum + struct with factory methods (always a struct). It pairs with
match.
union AuthError { invalidCredentials accountLocked(untilUtc: DateTimeOffset) // multi-payload cases allowed rateLimited(retryAfterMs: int)}
let err = AuthError.invalidCredentials() // factory formGeneric union is reified — Option<int> is a real closed generic struct, not an erasure to
object; a match binds the payload at its substituted type:
union Option<T> { some(value: T), none }let o = Option<int>.some(99) // typed Option<int>; .some(v) → v : intDot-case shorthand works when the type is known from context:
func findUser(id: Guid) -> Option<User> { if user == nil { return .none } return .some(user)}ref union — sealed class hierarchy
Section titled “ref union — sealed class hierarchy”The identity-carrying variant — an abstract base with a sealed subclass per case — for recursive, polymorphic structures (ASTs, UI trees, state machines).
ref union Expr { literal(value: int) add(left: Expr, right: Expr) neg(inner: Expr)}Two construction forms emit equivalent IL — the factory (mirrors the dot-shorthand) and the per-case
subtype composite literal (the underlying CLR type per case is Outer_case):
let tree = Expr.add(Expr.literal(3), Expr.literal(4)) // factorylet sum = Expr_add { left: Expr_literal { value: 3 }, right: Expr_literal { value: 4 } } // compositeA match over a ref union uses an isinst type pattern:
match expr { .literal(v) { return v } .add(l, r) { return eval(l) + eval(r) } .neg(inner) { return 0 - eval(inner) }}enum — a closed set of constants
Section titled “enum — a closed set of constants”A plain set of named constants with no payloads. Emits as a real CLR System.Enum (int32 underlying),
interchangeable with C# enums. Variants without an explicit value start at 0 and increment; after an
explicit value, auto-numbering resumes from value + 1:
enum Direction { north, south, east, west }enum Level { a, b = 10, c } // a=0, b=10, c=11Construct with the factory form — the trailing () is required, so the same call shape works across
enum, union, and ref union:
let d = Direction.north()A match on an enum needs a type hint, because variants flow through the dot-case shorthand:
match (d: Direction) { .north { return "N" } .south { return "S" } default { return "?" }}interface — nominal conformance
Section titled “interface — nominal conformance”Interfaces emit as standard CLR interfaces. Conformance is nominal (struct and class alike): a
type implements an interface only when it names it after :. There’s no structural auto-satisfaction —
methods attach via receiver blocks, but the type declares which interfaces it fulfills. The check is
exact: every method matched by name, parameter types, and return type. Write E# interface names with the
familiar .NET I prefix (IDescribable, IMap<K, V>). It is a convention, so existing CLR interfaces and
deliberately non-prefixed declarations still work.
interface IDescribable { func describe() -> string }
struct Client : IDescribable { name: string }
func (c: Client) describe() -> string = "client: {c.name}"Why nominal: the CLR is nominal underneath, declaring the interface makes the (value-type) boxing site
explicit, and it lines up with how dependency registration is written. A type that would satisfy an
interface it doesn’t declare gets a warning (ES2153) naming the : IFoo to
add — a structural coincidence is never silently treated as conformance.
When only the pointer method set satisfies a declared interface (a pointer-receiver method,
func (x: *T) f()), the generated __Ptr_T wrapper implements the interface and the value type does not —
the Go pointer-receiver case. See Pointers & memory.
Nullable T?
Section titled “Nullable T?”T? is optional presence — distinct from Result<T, E>, which is for fallible
operations.
T? of… | Emits as |
|---|---|
value type (int?, struct?) | Nullable<T> |
reference type (string?, class?) | unwrapped (already nullable) — holds as a generic arg too: Func<string, string?> is Func<string, string> |
nil fills either (initobj Nullable<T> for value types, ldnull for reference types).
func find(id: int) -> User? { if id == 0 { return nil } return lookupUser(id)}Attributes
Section titled “Attributes”[Name] and [Name(args)] pass straight through as CLR custom attributes — same syntax as C#, with
constructor arguments resolved at bind time:
[Obsolete("use v2")][StructLayout(LayoutKind.Explicit)]class Config { name: string }derive — generated members
Section titled “derive — generated members”derive emits real members at compile time (not runtime reflection). Place it above the type;
combine directives with a comma:
derive equality, debugstruct Packet { header: uint, length: int }derive equalitygeneratesEquals(object),GetHashCode(), and==/!=.derive debuggeneratesToString()→Packet { header: 1, length: 2 }.
Primitive types
Section titled “Primitive types”| E# | CLR | Size | E# | CLR | Size | |
|---|---|---|---|---|---|---|
int | Int32 | 4 | float | Single | 4 | |
uint | UInt32 | 4 | double | Double | 8 | |
long | Int64 | 8 | bool | Boolean | 1 | |
ulong | UInt64 | 8 | char | Char | 2 | |
short | Int16 | 2 | string | String | ref | |
ushort | UInt16 | 2 | void | Void | 0 | |
byte / sbyte | Byte / SByte | 1 |
Built-in types
Section titled “Built-in types”Result<T, E>— error-as-value;ok(...)/error(...),?propagation, combinators. See Errors.Spawned/Spawned<T>— concurrent-work handles fromspawn/task func.Chan<T>— typed channels.TaskScope— structured concurrency.
The last three are covered in Async & concurrency. Type resolution order:
primitives → built-in generics (Result, Chan) → current unit → other files → external .NET types.
Visibility
Section titled “Visibility”Everything is internal by default; pub makes a declaration visible outside its assembly. Two
levels only — there’s no private / protected. The module is the privacy boundary. Fields inherit the
enclosing type’s visibility unless overridden with pub.
pub struct Order { symbol: string, qty: int } // public typestruct Internal { secret: int, pub name: string } // internal type, one public fieldFor the exact grammar and normative rules behind every type kind, see the Specification.