Union and Intersection Types in Ceylon: Building Type‑Safe, Flexible APIs
Discover how Ceylon’s union and intersection types let you design APIs that return success or error values, model multi‑interface objects, and keep type safety intact at runtime. A concrete example shows the compiler’s handling and the trade‑offs you need to weigh.
01 Jul 2026, 06:53 UTC

Problem: Mixing Success and Failure in a Single API
When an API can return either a successful value or an error, developers often resort to checked exceptions, a result wrapper, or a custom error type. Each approach has drawbacks: checked exceptions break the type‑system, result wrappers add boilerplate, and custom error types can become a maintenance nightmare.
In Ceylon, union types (A|B) and intersection types (A&B) offer a cleaner, type‑safe way to model these scenarios without sacrificing runtime safety.
Thesis: Union and Intersection Types as First‑Class API Building Blocks
Union types let a value belong to one of several types, enabling APIs that can return a success value or an error value in a single signature. Intersection types express that a value must satisfy multiple contracts, perfect for objects that implement several interfaces or mix‑in behavior. Together, they give you a concise, expressive, and type‑safe API surface.
1. Union Types for Success/Failure
Define a generic Result union type that captures either a successful value T or an Error:
interface Error { String message; }
shared type Result<T> = T | Error;
shared Result<String> readFile(String path) {
try {
value content = read(path);
return content; // implicit cast to Result<String>
} catch (FileNotFoundException e) {
return Error { message = "File not found: ``path``" };
}
}
When readFile is called, the compiler enforces that the caller handles both branches:
value result = readFile("data.txt");
if (is String result) {
print(result);
} else {
error(result.message);
}
The is check compiles to a Java instanceof at runtime, ensuring that the value truly matches String or Error. No unchecked casts are required.
2. Intersection Types for Multi‑Interface Objects
Suppose you need an object that is both Serializable and Cloneable. Instead of creating a new interface that extends both, use an intersection type:
shared class Person(String name, Integer age) satisfies Serializable, Cloneable {
// implementation
}
shared void process(Person person) {
// person is guaranteed to be Serializable & Cloneable
serialize(person);
value copy = clone(person);
}
Here, Person satisfies both contracts, and the compiler treats any Person value as an intersection of the two interfaces. If you define a variable explicitly as Serializable & Cloneable, the compiler will reject assignments that do not satisfy both.
3. Compiler and Runtime Behavior
Ceylon compiles union and intersection types to Java bytecode that relies on instanceof checks and explicit casts. The generated code is type‑safe at runtime: attempting to cast a value to an incompatible type will throw a ClassCastException just as with normal Java types.
Because the compiler generates these checks, you get full safety without manual runtime validation. However, large or deeply nested type expressions can increase compile‑time overhead, especially in older Ceylon releases (pre‑1.3).
4. Trade‑offs and Limitations
- Compile‑time complexity: Complex union/intersection chains can slow down compilation. Keep expressions readable and avoid excessive nesting.
- IDE support: Refactoring tools may not fully understand union/intersection types, so manual review is advised when moving code around.
- Interoperability: Java callers of Ceylon code that uses union types will see the underlying Java type (e.g.,
Object) and must perform their owninstanceofchecks. - Documentation: Generated API docs display union/intersection types, but some tooling may not render them cleanly. Verify with
ceylon docif documentation clarity is critical.
5. Actionable Next Steps
- Install the Ceylon SDK (v1.3.3 or later) and create a sample module with the code snippets above.
- Run
ceylon compileto ensure the compiler accepts the union/intersection types. - Execute
ceylon runto observe runtime behavior and confirm that type checks work as expected. - Generate documentation with
ceylon docand verify that the signatures showT | ErrorandSerializable & Cloneablecorrectly. - Measure compile times if you plan to use very complex type expressions; consider simplifying or refactoring if needed.
By embracing union and intersection types, you can design APIs that are both expressive and type‑safe, reducing boilerplate and runtime errors. Just be mindful of the compiler and IDE limitations, and test thoroughly to confirm the behavior in your specific project context.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.