Using Ceylon’s Optional and Union Types on the JVM: A Practical Guide
Discover how Ceylon’s union and optional types (T? and String|Integer) give you compile‑time null safety, and learn a step‑by‑step workflow to build, run, and test a small JVM module with the last stable 1.3.x toolchain.
05 Apr 2026, 17:57 UTC

Desired Outcome
Build a small Ceylon 1.3.x JVM module that demonstrates how optional types (e.g. String?) and union types (e.g. String|Integer) are represented, and how the compiler’s flow‑sensitive narrowing (exists, is, assert) eliminates null‑pointer bugs at compile time.
Prerequisites
- Java 8 (or a compatible JDK that the 1.3.x toolchain supports).
- Download the Ceylon 1.3.x command‑line distribution from the archived GitHub releases and extract it to a directory, e.g.
/opt/ceylon-1.3.0. - Add
/opt/ceylon-1.3.0/bintoPATHand verify withceylon --version(expected output:ceylon 1.3.0). - Create a project root directory, e.g.
~/ceylon-demo, and inside it runceylon initto generate a minimal module structure.
Focused Procedure
Create the module descriptor (
module.ceylon) with the following content:module my.module "1.0.0" { import ceylon.language "1.3.0"; }Write a source file that uses optional and union types (
src/my/module/Hello.ceylon):module my.module "1.0.0"; import ceylon.language "1.3.0"; shared String? maybeHello() => "Hello"; shared Integer? maybeNumber() => null; shared void demo() { // Optional type handling if (exists msg = maybeHello()) { print("Got: ``msg``"); } else { print("No message"); } // Union type handling value data = maybeNumber() else 42; // fallback to a default if (is Integer data) { print("Number: ``data``"); } else if (is String data) { print("String: ``data``"); } }Compile the module:
ceylon compile my.moduleExpected result: the compiler emits no "possibly null" or "uninitialized value" errors. If you intentionally dereference
maybeHello()withoutexists, the compiler will reject the code.Run the demo:
ceylon run my.moduleOutput:
No message Number: 42Write tests covering both branches (
test/my/module/HelloTest.ceylon):module my.module "1.0.0" { import ceylon.test "1.3.0"; import my.module "1.0.0"; } import ceylon.test "1.3.0"; import my.module "1.0.0"; test void testMaybeHello() { value msg = maybeHello(); if (exists m = msg) { assert (m == "Hello"); } else { fail("Expected a message"); } }Run the tests:
ceylon test my.moduleAll tests should pass, confirming that both the optional and union logic work as intended.
Expected Checks
ceylon compilecompletes without errors related to nullability or uninitialized values.- Running the module prints the expected output and does not throw a
NullPointerException. - All
ceylon testcases pass, demonstrating that the compiler enforces flow‑sensitive narrowing at test time.
Recovery Options
- Lost narrowing after mutation: If a value is stored in a mutable attribute, copy it to an immutable local variable before performing
existsorischecks.shared class MutableHolder(shared Integer? value) {} value holder = MutableHolder(null); if (exists v = holder.value) { // safe to use v here } - Missing dependencies after Herd shutdown:
- Vendor the required
ceylon.languagesource into your project and add it to the module path. - Alternatively, use
ceylon.interop.javato call Java libraries that provide the needed functionality, bypassing the need for a Herd module.
- Vendor the required
Practical Verification Checklist
- Run
ceylon --versionto confirm the toolchain is 1.3.x. - Compile the module and verify the compiler output contains no null‑related warnings.
- Run the module and inspect the console for the expected “No message” and “Number: 42” lines.
- Execute
ceylon test my.moduleand confirm all tests succeed. - If any of these steps fail, check that you are using a Java 8 JDK and that the
ceylon-1.3.0distribution is correctly extracted.
Takeaway
Ceylon’s optional (T?) and union (T|U) types together with flow‑sensitive narrowing provide a compile‑time safety net that catches many null‑pointer bugs before the code runs. Even though the 1.3.x line is discontinued, the language’s design remains a useful study for languages that aim to eliminate runtime null errors through static analysis.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.