Skip to content

Coming from Apex

If you already know Apex, you already know most of what Vertex looks like: braces, named arguments, @Annotations, public, and so on. This page is a quick translation table for the constructs that differ. For the full walkthrough, start with the Language Tour.

ApexVertex
IntegerInt
LongLong
DoubleDouble
DecimalDecimal
StringString
BooleanBool
IdId
voidVoid
ApexVertex
String name = 'Alice';let name = "Alice"
Integer x = 1; x = 2; (mutable)mutable let x = 1; x = 2
public static final String ENV = 'prod';const ENV = "prod" (module level)
// unused variable warningprefix with _: let _tmp = ...

Vertex bindings are immutable by default. Add mutable when you really need reassignment, so mutation is always visible in the source.

ApexVertex
'Hello, ' + name + '!'"Hello, ${name}!"
String.valueOf(x)"${x}" (interpolation coerces)
s.toUpperCase()s.toUpperCase()
s.contains('foo')s.contains("foo")

Strings are always double-quoted. Single quotes are not string literals.

Apex:

String label;
if (age < 18) label = 'minor';
else if (age < 65) label = 'adult';
else label = 'senior';

Vertex (blocks are expressions, so if returns a value):

let label =
if age < 18 { "minor" }
else if age < 65 { "adult" }
else { "senior" }

There is no ternary operator. An if expression is the ternary.

Apex uses C-style for. Vertex uses for..in.

for (Integer i : numbers) {
System.debug(i);
}
for n in numbers {
debug n
}

There is no while in user code; idiomatic Vertex expresses iteration with for..in, recursion, or collection combinators (map, filter, fold).

Apex:

List<String> names = new List<String>{ 'Alice', 'Bob' };
Set<Integer> ids = new Set<Integer>{ 1, 2, 3 };
Map<String, Integer> ages = new Map<String, Integer>{
'Alice' => 30, 'Bob' => 25
};

Vertex:

let names: List<String> = ["Alice", "Bob"]
let ids: Set<Int> = #{1, 2, 3}
let ages: Map<String, Int> = {"Alice": 30, "Bob": 25}

Vertex collections come with the combinators you expect (map, filter, fold, any, all), so a lot of explicit for-loops go away. See Collections.

Apex uses null for absence. Vertex has no null: use Option<T>.

String name = lookup(id);
if (name != null) {
System.debug('Found: ' + name);
} else {
System.debug('Missing');
}
match lookup(id) {
Some(name) => debug "Found: ${name}",
None => debug "Missing",
}

Shorthand: .unwrapOr(default), .map(...), .flatMap(...). See Option.

Apex uses throw / try / catch. Vertex uses Result<T, E>.

public static Integer safeDivide(Integer a, Integer b) {
if (b == 0) {
throw new IllegalArgumentException('cannot divide by zero');
}
return a / b;
}
fn safe_divide(a: Int, b: Int): Result<Int, String> {
if b == 0 {
Error("cannot divide by zero")
} else {
Ok(a / b)
}
}

Propagate with the ? operator:

fn halve_then_double(a: Int, b: Int): Result<Int, String> {
let q = safe_divide(a, b)? // returns early on Error
Ok(q * 2)
}

See Result.

Apex lives inside classes. Vertex does not have static methods: a .vtx file is a module, and its pub fn declarations are its API.

// Apex
public class MathUtils {
public static Integer add(Integer a, Integer b) { return a + b; }
}
Integer sum = MathUtils.add(1, 2);
math_utils.vtx
pub fn add(a: Int, b: Int): Int { a + b }
// in another file
import math_utils
let sum = math_utils.add(1, 2)

See Functions and Modules & Imports.

Apex has classes (with fields, getters/setters, constructors) and enums. Vertex has type, which covers both.

public class Account {
public String name;
public Decimal revenue;
public Account(String name, Decimal revenue) {
this.name = name; this.revenue = revenue;
}
}
type Account {
Account(name: String, revenue: Decimal)
}
let a = Account(name: "ACME", revenue: 1000000.0d)
debug a.name

Multi-variant types (sum types) have no Apex equivalent; the closest thing is an enum, but sum types carry data per variant:

type Payment {
Card(last4: String),
Bank(routing: String, account: String),
Cash,
}

See Sum Types and Pattern Matching.

Account a = new Account(Name = 'ACME');
insert a;
@SObject
type Account {
Account(Name: String)
}
let acc = Account(Name: "ACME")
let result = Database.insert(acc) // Result<Id, DmlError>

Apex’s “throws on failure” becomes an explicit Result you must match on. Database.insert, update, delete, and undelete all return Result. See Salesforce Integration.

@AuraEnabled(cacheable=true)
public static String getAccountName(Id id) { ... }
@AuraEnabled(cacheable: true)
fn getAccountName(id: String): Result<String, AuraError> { ... }

The function must return Result<T, AuraError>. Ok is returned to the LWC; Error is translated into an AuraHandledException at the Apex boundary. See Annotations.

@IsTest
private class InvoiceTest {
@IsTest static void totals_include_tax() {
System.assertEquals(110, Invoice.total(100, 10));
}
}
invoice_test.vtx
@Test
fn totals_include_tax(): Void {
assert Invoice.total(100, 10) == 110
}

Test files end in _test.vtx. Run them with vertex test (see Testing).

public without sharing class AdminJob { ... }
@WithoutSharing
pub fn run(): Void { ... }

The sharing annotation goes at the top of the file. Default is @WithSharing, which matches Apex security best practices.

ApexVertex
@IsTest (method)@Test
@AuraEnabled@AuraEnabled
@AuraEnabled(cacheable=true)@AuraEnabled(cacheable: true)
with sharing@WithSharing
without sharing@WithoutSharing
inherited sharing@InheritedSharing

You do not have to rewrite anything. Vertex can call Apex through extern declarations:

extern type Messaging.SingleEmailMessage
extern new Messaging.SingleEmailMessage(): Messaging.SingleEmailMessage
extern method Messaging.SingleEmailMessage.setSubject(
self: Messaging.SingleEmailMessage,
subject: String,
): Void
let msg = Messaging.SingleEmailMessage.new()
msg.setSubject("Hello from Vertex")

See Apex FFI.

  • Getting Started: install the toolchain and run your first program.
  • Language Tour: a guided walk through the language from the top.
  • Guides: task-oriented how-tos (error handling, testing, pattern matching).