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