Types and Expr
This page is the reference for the type vocabulary and the Expr value handle. For the
narrative version (and the staging-misuse failure modes), see
values and types.
The t namespace
Section titled “The t namespace”import type { EvsType, StringType, TupleType } from '@maxencerb/evs';import type { Abi, AbiParameter } from 'viem';
declare const t: { readonly [k in Exclude<StringType, `${string}[]`>]: k } & { array(elem: StringType | TupleType): StringType | TupleType; struct(spec: Record<string, EvsType>): TupleType; tuple(...items: readonly EvsType[]): TupleType; fromOutputs(abi: Abi, name: string): EvsType; // derive a type from a function's outputs fromAbiParameter(param: AbiParameter): EvsType; // derive a type from one ABI parameter};t is a frozen object whose every scalar property is its own literal type string — pure
autocomplete sugar — plus three functions that build composite types. Raw type strings (and raw
readonly AbiParameter[] arrays, anywhere a tuple type is expected) are accepted everywhere
t.* is.
| Members | Value | Notes |
|---|---|---|
t.address |
'address' |
160-bit word |
t.bool |
'bool' |
canonical 0 or 1 |
t.uint8 through t.uint256 |
'uint8' … 'uint256' |
one key per width in UintBits (every multiple of 8), 32 keys |
t.int8 through t.int256 |
'int8' … 'int256' |
same 32 widths, signed (two’s complement) |
t.bytes1 through t.bytes32 |
'bytes1' … 'bytes32' |
left-aligned fixed bytes, 32 keys |
t.string |
'string' |
dynamic, UTF-8 |
t.bytes |
'bytes' |
dynamic byte string |
t.array(elem) |
`${elem}[]` / TupleType |
function: a dynamic array of a scalar/array element (uint256[], string[], uint256[][]), or a tuple[] of a tuple element |
t.struct(spec) |
TupleType (named components) |
function: a struct — Object.keys(spec) order is the encode order |
t.tuple(...items) |
TupleType (positional) |
function: an unnamed positional tuple |
t.fromOutputs(abi, name) |
EvsType (a TupleType for many/tuple outputs) |
function: derive a type from an as const ABI function’s outputs — one output → that output’s type, several → a named-component struct in ABI order (FromAbiOutputs); overloaded/unknown name throws (a non-const ABI degrades to EvsType) |
t.fromAbiParameter(param) |
EvsType |
function: derive a type from one ABI parameter (AbiParamToEvsType) — a tuple… param → a TupleType, else the type string |
t.array validates its element at the call site. t.struct/t.tuple build a
TupleType descriptor — an abitype AbiParameter-shaped object
viem infers directly.
import { t } from '@maxencerb/evs';
const fees = t.array(t.uint24); // 'uint24[]'const Position = t.struct({ liquidity: t.uint128, owner: t.address }); // named struct → objectconst Pair = t.tuple(t.address, t.uint256); // positional tuple → [address, bigint]const amount = 'uint128'; // raw strings work identicallyvoid [fees, Position, Pair, amount];Type aliases
Section titled “Type aliases”The full exported vocabulary, exactly as defined:
type Hex = `0x${string}`;type UintBits = | 8 | 16 | 24 | 32 | 40 | 48 | 56 | 64 | 72 | 80 | 88 | 96 | 104 | 112 | 120 | 128 | 136 | 144 | 152 | 160 | 168 | 176 | 184 | 192 | 200 | 208 | 216 | 224 | 232 | 240 | 248 | 256;type BytesSize = | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32;type UintType = `uint${UintBits}`;type IntType = `int${UintBits}`;type BytesNType = `bytes${BytesSize}`;type WordType = UintType | IntType | 'address' | 'bool' | BytesNType;type DynType = 'string' | 'bytes';type ScalarType = WordType | DynType;type ArrayType = `${ScalarType}[]` | `${ScalarType}[][]` | `${ScalarType}[][][]`;type StringType = ScalarType | ArrayType; // every string-encoded typeinterface NamedType { readonly name: string; // '' for a positional tuple member readonly type: string; // 'uint256', 'tuple', 'tuple[]', 'uint256[]', … readonly components?: readonly NamedType[]; // present iff type starts with 'tuple'}interface TupleType { readonly type: 'tuple' | 'tuple[]' | 'tuple[][]'; readonly components: readonly NamedType[];}type EvsType = WordType | DynType | ArrayType | TupleType;type ArgType = EvsType; // script arg types — word, dynamic, string array, or a TupleTypetype NumericType = UintType | IntType;type BitsType = UintType | BytesNType;| Alias | Meaning |
|---|---|
Hex |
A 0x-prefixed hex string. |
Address |
The address string type, re-exported from abitype (the same type viem uses). |
UintBits |
The 32 legal integer widths: every multiple of 8 from 8 to 256. |
BytesSize |
The legal bytesN sizes: 1 through 32. |
UintType |
uint8 … uint256 as a template-literal union. |
IntType |
int8 … int256. |
BytesNType |
bytes1 … bytes32. |
WordType |
Every type that fits one EVM word: unsigned and signed integers, address, bool, bytesN. |
DynType |
The dynamic byte-string types: string and bytes. |
ScalarType |
WordType or DynType — every non-array, non-tuple type. |
ArrayType |
String-encoded arrays of a scalar element, up to three [] deep — uint256[], address[][]. |
StringType |
Every string-encoded type: a scalar or a string array. |
NamedType |
One tuple/struct member — an abitype AbiParameter-shaped descriptor (recursive). |
TupleType |
A struct/tuple type — { type, components }. Built by t.struct/t.tuple; see Composite types. |
EvsType |
Every value type: word, dynamic, string array, or a TupleType. |
ArgType |
The script-argument value type — an alias for EvsType (word, dynamic, string array, or TupleType); types ScriptIr.args and ArgSpec.type. Named args/params are narrower (string-encoded only) — that bound lives on namedArg(), not here. |
NumericType |
The arithmetic domain: uintN or intN. |
BitsType |
The bitwise/shift domain: uintN or bytesN. |
LitOf<t> |
The host literal type accepted where a t is expected (table below). |
IntoExpr<t> |
Expr of t, or a LitOf<t> literal. |
Expr<t> |
The branded staged-value handle (below). |
Composite types (TupleType)
Section titled “Composite types (TupleType)”A TupleType is a struct or tuple type — an abitype AbiParameter-shaped descriptor that viem
infers directly. Build one with t.struct (named members, inferred as an object) or t.tuple
(positional members, inferred as an array); t.array lifts a tuple to a tuple[]. A raw
readonly AbiParameter[] is accepted anywhere a TupleType is expected:
import { t } from '@maxencerb/evs';import type { TupleType } from '@maxencerb/evs';
// t.struct → named components; runtime encode order is Object.keys insertion order.const Position = t.struct({ liquidity: t.uint128, owner: t.address });// ^? { type: 'tuple'; components: readonly [...] } (LitOf → { liquidity: bigint; owner: `0x${string}` })
// t.tuple → positional, unnamed components (LitOf → [bigint, `0x${string}`]).const Pair = t.tuple(t.uint128, t.address);
// t.array of a tuple → a tuple[].const positions = t.array(Position);
// nested struct: a member can itself be a TupleType.const Outer = t.struct({ pos: Position, x: t.uint256 });
const types: readonly TupleType[] = [Position, Pair, positions, Outer];void types;LitOf<TupleType> delegates to abitype: an all-named tuple becomes an object, a positional
tuple an array, recursing through nested components. TupleType flows everywhere a value type
does — as a script arg (decoded into a Tuple handle), as a
s.read argument or output, and as an s.return value. The handle surface — Tuple, Field,
s.tuple — lives in the builder reference.
t.array(Position) lifts a struct to a tuple[] (a TupleType with type: 'tuple[]'). A
composite-element array — tuple[], a one-level nested array (uint256[][]), or a dynamic-leaf
array (string[]/bytes[]) — is an array of pointers in memory: the [length] prefix is
followed by one pointer word per element, each pointing at the element’s own block (a tuple
block, an inner array, or a bytes block). It shares the word-array handle surface — .length(),
.at(i), s.newArray — and the same reference semantics; an s.read tuple[] output is an
array handle whose .at(i) yields a typed Tuple element. The
build/read/pass examples live in composite arrays.
Still rejected at recording (UNSUPPORTED_V0): two-level tuple arrays (tuple[][]), arrays
nested deeper than [][], and fixed-size T[N].
Several type-level helpers are exported for advanced inference: StructTypeOf, TupleTypeOf,
TupleArrayOf, TypeToComponent, TupleLitOf, and TupleAsParam. You rarely name them — they
power t.struct/t.tuple inference — but they are public for building your own type-level
glue.
Deriving a type from an ABI
Section titled “Deriving a type from an ABI”t.fromOutputs(abi, name) and t.fromAbiParameter(param) build a t.* type from existing ABI
material instead of a hand-written t.struct/t.tuple — see
deriving types from an ABI for the
narrative and a runnable example. Their return types are exported:
FromAbiOutputs<abi, name>— the type oft.fromOutputs(abi, name): a single output’s type, or aTupleTypestruct over several outputs (named, in ABI declaration order). An unknown name or a non-constABI widens toEvsType; an overloaded name throws at recording.AbiParamToEvsType<p>— the type oft.fromAbiParameter(p): aTupleTypefor atuple…parameter, else the scalar/array type string.
The component-level mirrors AbiParamToComponent and AbiParamsToComponents are exported too, for
building your own ABI→type glue.
Literal coercion: LitOf and IntoExpr
Section titled “Literal coercion: LitOf and IntoExpr”Every builder position typed IntoExpr accepts either an Expr or a plain host literal:
type LitOf<t extends EvsType> = t extends NumericType ? bigint | number : t extends 'address' ? `0x${string}` : t extends 'bool' ? boolean : t extends BytesNType ? `0x${string}` : t extends 'string' ? string : t extends 'bytes' ? `0x${string}` : t extends TupleType ? TupleLitOf<t> // delegated to abitype: object (all-named) or array (positional) : t extends `${infer e extends StringType}[]` ? readonly LitOf<e>[] : never;
type IntoExpr<t extends EvsType> = Expr<t> | LitOf<t>;Literals are validated at recording time, with the call-site location (EvsTypeError on
violation):
| Literal | Rule |
|---|---|
number for uintN or intN |
must be a safe integer; range-checked against N |
bigint for uintN or intN |
range-checked; negatives two’s-complemented for intN |
boolean |
only for bool |
0x string for address |
exactly 20 bytes; checksum NOT enforced (viem-permissive) |
0x string for bytesN |
exactly N bytes |
0x string for bytes |
any even-length hex |
string for string |
UTF-8 encoded |
JS array for T[] |
element-wise rules of T |
Word literals canonicalize at recording; dynamic literals and literal arrays become bytecode
data segments. All-literal pure operations fold at recording, and a fold that would certainly
panic throws EvsTypeError (CERTAIN_PANIC) at that line instead. When inference needs help,
construct the literal explicitly with s.lit (builder reference).
import type { EvsType } from '@maxencerb/evs';
declare const exprBrand: unique symbol;interface Expr<t extends EvsType = EvsType> { readonly [exprBrand]: t; // nominal, covariant phantom — you never construct an Expr yourself readonly type: t; // runtime-readable type tag, e.g. 'uint256'}(Abridged — the methods are tabulated below.) An Expr is a branded handle to a recorded
program value. The brand makes it nominal: an Expr<'uint8'> is not assignable where
Expr<'uint16'> is expected. The only runtime-readable member is type.
Each method uses a this-parameter to restrict itself to the types it is defined on — calling
add on an Expr<'address'> is a TypeScript error, not a runtime surprise. Every binary
method also exists as a free function on the builder (s.add(a, b), s.lt(a, b), …) for
literal-left operands; see the builder reference.
Arithmetic (checked)
Section titled “Arithmetic (checked)”Available on numeric Exprs (this: Expr of t where t is a NumericType). All arithmetic
is checked, matching solc 0.8 semantics — see arithmetic.
| Method | Signature | Checked semantics |
|---|---|---|
add |
add(rhs: IntoExpr<t>): Expr<t> |
Panic 0x11 on overflow |
sub |
sub(rhs: IntoExpr<t>): Expr<t> |
Panic 0x11 on underflow |
mul |
mul(rhs: IntoExpr<t>): Expr<t> |
Panic 0x11 on overflow |
div |
div(rhs: IntoExpr<t>): Expr<t> |
Panic 0x12 on zero divisor; Panic 0x11 on signed minN / -1 |
mod |
mod(rhs: IntoExpr<t>): Expr<t> |
Panic 0x12 on zero divisor |
Comparisons
Section titled “Comparisons”| Method | Signature | Notes |
|---|---|---|
lt, gt, lte, gte |
lt(rhs: IntoExpr<t>): Expr<'bool'> |
Numeric types only; LT/GT vs SLT/SGT chosen from the static signedness |
eq, neq |
eq(rhs: IntoExpr<t>): Expr<'bool'> |
Any word type; no string/bytes/array equality in v0 |
Boolean logic
Section titled “Boolean logic”| Method | Signature | Notes |
|---|---|---|
and |
and(rhs: IntoExpr<'bool'>): Expr<'bool'> |
Eager, NOT short-circuiting — both sides always execute |
or |
or(rhs: IntoExpr<'bool'>): Expr<'bool'> |
Eager, NOT short-circuiting |
not |
not(): Expr<'bool'> |
Logical negation |
For conditional execution use s.if (control flow).
Bitwise and shifts
Section titled “Bitwise and shifts”Available on BitsType Exprs (uintN or bytesN). Results are re-canonicalized to the
operand’s width; shifts never panic — bits leaving the lane are dropped.
| Method | Signature | Notes |
|---|---|---|
bitAnd, bitOr, bitXor |
bitAnd(rhs: IntoExpr<t>): Expr<t> |
Bitwise within the type’s lane |
bitNot |
bitNot(): Expr<t> |
Complement, re-masked to the type’s width |
shl, shr |
shl(bits: IntoExpr<'uint256'>): Expr<t> |
Shift amount is always uint256 |
Conversions and casts
Section titled “Conversions and casts”Widening is free; narrowing is checked (Panic 0x11 on out-of-range values).
| Method | Signature | Available on | Semantics |
|---|---|---|---|
toUint |
toUint(target: u): Expr<u> |
any numeric Expr |
checked against the target range |
toInt |
toInt(target: i): Expr<i> |
any numeric Expr |
checked against the target range |
asAddress |
asAddress(): Expr<'address'> |
Expr<'uint256'> or Expr<'bytes32'> |
checked: panics unless the top 96 bits are zero |
asUint256 |
asUint256(): Expr<'uint256'> |
Expr<'bytes32'> |
free reinterpret |
asBytes32 |
asBytes32(): Expr<'bytes32'> |
Expr<'uint256'> |
free reinterpret |
import { evscript, t } from '@maxencerb/evs';
const casts = evscript({ name: 'casts', args: [t.bytes32] }, (s, raw) => { const word = raw.asUint256(); // free reinterpret const small = word.toUint(t.uint32); // checked narrowing — Panic 0x11 if out of range const wide = small.toUint(t.uint256); // widening is free const addr = raw.asAddress(); // checked: top 96 bits must be zero return s.return({ small, wide, addr });});Dynamic values and arrays
Section titled “Dynamic values and arrays”| Method | Signature | Available on | Semantics |
|---|---|---|---|
length |
length(): Expr<'uint256'> |
string, bytes, any T[] |
byte length for string/bytes; element count for arrays |
at |
at(i: IntoExpr<'uint256'>): Expr<elem> |
any T[] |
bounds-checked — Panic 0x32 on an out-of-range index |
Putting it together
Section titled “Putting it together”import { evscript, t } from '@maxencerb/evs';
const flags = evscript({ name: 'flags', args: [t.bytes4, t.int128] }, (s, mask, n) => { const isNeg = n.lt(0n); // SLT — signedness comes from the static type const top = mask.shr(16n); // re-masked to the bytes4 lane const both = isNeg.and(mask.neq('0x00000000')); // eager bool logic return s.return({ isNeg, top, both });});