How do I convert between types?
← Getting Started · Ref: Q24
EK9 has NO casting, NO instanceof, and NO multi-catch blocks. These were deliberately excluded because they encourage fragile type-checking code that breaks when new types are added. Instead, EK9 provides three clean mechanisms for type conversion and type-specific processing.
TYPE PROMOTION: THE #^ OPERATOR
The promote operator converts a value to a wider compatible type. This happens automatically when you assign to a variable of a wider type:
intValue <- 42 floatResult as Float: intValue
The compiler inserts a call to Integer's #^ operator, which returns a Float. Only ONE level of promotion is allowed (no chaining). Built-in promotions include:
Integer to Float (safe numeric widening) Character to String (single char to text) Date to DateTime (date to full timestamp) Millisecond to Duration (time unit widening)
You can define #^ on your own types for custom promotions.
STRING CONVERSION: THE $ OPERATOR
The $ operator converts any value to its String representation:
count <- 42 countText <- $count
Every built-in type defines $ for readable output. Override it on your own types to control how they display. Used automatically in string interpolation: `Value is ${count}`
CONSTRUCTOR CONVERSION
Explicit conversion uses constructors that accept other types:
parsed <- Integer("99") precise <- Float(42)
This is the explicit, intentional conversion path when you know what you want.
THE DISPATCHER: TYPE-SPECIFIC PROCESSING (Key Mechanism)
The dispatcher is how EK9 replaces instanceof, type switches, visitor patterns, and multi-catch blocks. It is the fundamental mechanism for processing values based on their actual runtime type.
Declare one method 'as dispatcher' taking a base type (typically Any). Then define private overloads for each specific type:
describe() as dispatcher -> mainValue as Any <- rtn as String: "Unknown type" private describe() -> mainValue as Integer <- rtn as String: "Got an Integer" private describe() -> mainValue as Float <- rtn as String: "Got a Float"
At runtime, the actual type of the argument determines which overload executes. The Any handler acts as the default for unhandled types.
Dispatcher rules:
Exactly ONE method marked 'as dispatcher' (the entry point) Overloads have the same name but different parameter types 1 or 2 parameters only (not 0, not 3+) All overloads must match purity (all pure or all non-pure) Adding a new type means adding a new overload, not modifying existing code
DISPATCHER FOR EXCEPTION HANDLING
EK9 has only ONE catch block per try. There are no multi-catch blocks like Java's catch(IOException | SQLException e). Instead, use a dispatcher:
try riskyOperation() catch -> ex as Exception handleError(ex) private handleError() as dispatcher -> ex as Exception <- rtn as String: "General error" private handleError() -> ex as AnException <- rtn as String: "Known error" private handleError() -> ex as OtherException <- rtn as String: "Other error"
This is fundamentally different from Java, Python, and C#. There is no escape hatch. You cannot check 'is this an X?' anywhere in EK9. You MUST use a dispatcher or the promotion operator.
WHY NO INSTANCEOF OR CASTING? (Design Philosophy)
Needing to know the exact type of an object, or needing to cast it, is a code smell. It means you are not designing polymorphically. If your code says 'if this is a Dog, do X; if this is a Cat, do Y', you have pushed type-specific logic into the caller instead of the type itself. This violates the Open-Closed Principle: every new type forces changes to existing code.
In 30+ years of production systems, wild casts and type assumptions are a major source of defects. Java's ClassCastException, Python's AttributeError after isinstance checks, C++'s undefined behavior from bad casts, all stem from the same root problem: code that should not care about concrete types is forced to care.
EK9 eliminates this entire class of defects by design. The dispatcher forces type-specific logic into the receiver (the overloaded methods), not the caller. Adding a new type means adding a new overload. Existing code is untouched. Some developers coming from Java or Python will find this very hard at first because it requires genuine polymorphic thinking, but it produces fundamentally better designs.
Why this matters:
No forgotten-case bugs when a new type is added No instanceof chains that grow with every new type No ClassCastException or casting errors at runtime Each type handler is a separate, testable method The compiler validates dispatcher consistency at compile time Forces genuine polymorphic design, not just syntax convenience
See also Q23 (What basic types does EK9 have?) for the full type catalog, and use 'ek9 -h TypeName' to see which operators each type supports.
See Q23 for basic types. See Q25 for promote operator. See Q242 for conversion and introspection operators ($, $$, #?, #^, #<, #>). See Q250 for fixing type mismatch errors. See Q254 for the Any type and dispatcher fallback. See Q255 for cost-based method resolution.
Example
defines module qa.type.conversion defines class AnException extends Exception AnException() -> reason as String super(reason) default operator ? OtherException extends Exception OtherException() -> reason as String super(reason) default operator ? TypeProcessor describe() as dispatcher -> mainValue as Any <- rtn as String: "Unknown type" private describe() -> mainValue as Integer <- rtn as String: `Integer: ${mainValue}` private describe() -> mainValue as Float <- rtn as String: `Float: ${mainValue}` private describe() -> mainValue as String <- rtn as String: `String: ${mainValue}` private handleError() as dispatcher -> ex as Exception <- rtn as String: "General error: " + $ex private handleError() -> ex as AnException <- rtn as String: "Known error: " + $ex private handleError() -> ex as OtherException <- rtn as String: "Other error: " + $ex processWithErrorHandling() -> mainValue as Any <- rtn as String: String() stdout <- Stdout() try rtn: describe(mainValue) catch -> ex as Exception rtn: handleError(ex) stdout.println(rtn) defines program TypeConversionDemo() stdout <- Stdout() // Promotion: Integer to Float via #^ operator intValue <- 42 floatResult as Float: intValue stdout.println(`Promoted: ${floatResult}`) // Promotion: Character to String letter <- 'Z' textResult as String: letter stdout.println(`Promoted: ${textResult}`) // String conversion with $ operator count <- 100 countText <- $count stdout.println(`String: ${countText}`) // Constructor conversion parsed <- Integer("99") stdout.println(`Parsed: ${parsed}`) // Dispatcher: process different types processor <- TypeProcessor() items <- [1, 2.5, "hello"] for listItem in items description <- processor.describe(listItem) stdout.println(description) // Dispatcher for exception handling (single catch, dispatched) result <- processor.processWithErrorHandling(42) stdout.println(result)
Common mistakes
E07520 — The ? operator is inherited from the base type (Exception). Use 'default operator ?' to auto-generate it. Declaring bare 'operator ?' triggers E07520 because operator semantics require a Boolean return. See ek9 -h E07520 for details.
Incorrect:
operator ?
Correct:
default operator ?
Other ways to ask this
- How does type casting work in EK9?
- What is the dispatcher pattern in EK9?
- How do I handle different types without instanceof?
- How does the promote operator work?
Coming from another language?
Java: casting with (Type)obj, instanceof with pattern matching (Java 21), multi-catch blocks, visitor pattern. Python: isinstance() checks, type() comparisons, multiple except clauses. Rust: as keyword, From/Into traits, match with pattern guards. Go: type assertions x.(Type), type switches. C#: is/as operators, pattern matching in switch. EK9: NO casting, NO instanceof, NO multi-catch. Uses #^ promotion, $ string conversion, constructor conversion, and dispatcher pattern. Dispatcher is mandatory for type-specific processing. Forces redesign of type handling from caller-side checking to receiver-side dispatch.
Keywords: start, dispatch, sealed, conversion, visitor, first, promotion, migrate, casting, overload, dispatcher, cast, instanceof, type, exception, handler, intro, any, convert, promote, beginner, catch