What is the Any type and when should I use it?
← Advanced Type System · Ref: Q254
Any is the universal base interface in EK9. Every type implicitly inherits from Any, making it the root of the entire type hierarchy. This is similar to Java's Object or Python's object, but Any is an interface, not a class.
OPERATORS AND ANY
Any carries no usable operator except ? (isSet). A value held as Any can be asked x? but not x == y or $x: those are E07620 (operator not defined).
Your own types do NOT inherit operators from Any either. Six operators form the canonical set every type is expected to provide for itself:
- ? (isSet) - check if object has a meaningful value
- == (equality) - compare two objects for equality
- <=> (comparison) - three-way comparison for ordering
- $ (string) - convert to human-readable String representation
- $$ (json) - convert to JSON representation
- #? (hashcode) - compute a hash code for the object
A class provides them with 'default operator' or explicit definitions (sameness rules: E07181/E07184), and a type with fields must define ? (E07235). Until it does, a == b on it is E07620. Built-in types such as String define them already, which is why the program below can use ?, $ and #? on a String.
ANY IN THE DISPATCHER PATTERN
The most common use of Any is as the fallback parameter in a dispatcher method. The dispatcher entry point takes an Any parameter, and specific overloads handle known types:
describe() as dispatcher -> item as Any <- rtn as String: "Unknown" private describe() -> item as Integer <- rtn as String: "Integer"
The Any overload catches everything not handled by specific overloads.
COST-BASED MATCHING WITH ANY
When the compiler resolves which dispatcher overload to call, Any matches score HIGH_COST (20.0). This means Any is always the LEAST preferred match. Exact type matches score 0.0, superclass matches score 0.05 per level, trait matches score 0.10 per level, and promotion matches score 0.5. Any's 20.0 cost ensures specific overloads always win.
WHEN TO USE ANY
- Dispatcher entry points - the base method that catches unhandled types
- Generic programming - when you truly need to work with any type
- Polymorphic containers - when mixing types is genuinely required; declare it: items as List of Any: [42, "hello"] (an undeclared [42, "hello"] shares no type but Any and is E50030)
- Framework-level code - infrastructure that must handle arbitrary types
WHEN NOT TO USE ANY
- Specific type handling - use the actual type for type safety
- API parameters - use specific types so callers know what to pass
- Return types - specific return types help callers avoid type checks
- When you know the type - Any loses compile-time type information
Using Any too broadly is a code smell. If you find yourself using Any everywhere and then dispatching on types, consider whether your design could use traits or abstract classes to express the relationship directly.
See Q24 for the dispatcher pattern and type conversion. See Q211 for double dispatch. See Q238 for the finite operator set. See Q255 for method resolution costs. See Q267 for anti-patterns including overusing Any.
Example
defines module qa.advancedtypes.anytype defines class TypeDescriber describe() as dispatcher -> item as Any <- rtn as String: "Unknown type" private describe() -> item as Integer <- rtn as String: `Integer: ${item}` private describe() -> item as Float <- rtn as String: `Float: ${item}` private describe() -> item as String <- rtn as String: `String: ${item}` private describe() -> item as Boolean <- rtn as String: `Boolean: ${item}` defines program AnyTypeDemo() stdout <- Stdout() describer <- TypeDescriber() // A heterogeneous list DECLARES its element type: elements that share no type but Any are E50030 items as List of Any: [42, 3.14, "hello", true] for listItem in items stdout.println(describer.describe(listItem)) // Any fallback for types without specific overloads mixedItems as List of Any: [Date(), Duration()] for mixedItem in mixedItems stdout.println(describer.describe(mixedItem)) // String defines ?, $ and #? itself - Any does not supply them greeting <- "Hello" stdout.println(`isSet: ${greeting?}`) stdout.println(`string: ${greeting}`) stdout.println(`hashcode: ${#?greeting}`)
Common mistakes
E50030 — Integer, Float, String and Boolean share no type but Any, and EK9 never invents a List of Any - an undeclared mix is E50030 at the literal. Declare the element type when a heterogeneous list is what you want.
Incorrect:
items <- [42, 3.14, "hello", true]
Correct:
items as List of Any: [42, 3.14, "hello", true]
E50060 — TypeDescriber does not have a process() method. The correct method name is describe(). Using a non-existent method triggers E50060 — method not resolved. See ek9 -h E50060 for details.
Incorrect:
stdout.println(describer.process(listItem))
Correct:
stdout.println(describer.describe(listItem))
Other ways to ask this
- How does the universal base type work in EK9?
- What is the root of the EK9 type hierarchy?
- When should I use Any instead of a specific type?
- How does Any relate to dispatchers?
Coming from another language?
Java: Object is the root class, all classes extend Object, has equals/hashCode/toString. Python: object is the root, all classes inherit from it, has __eq__/__hash__/__str__. C#: object/System.Object is universal base, has Equals/GetHashCode/ToString. Rust: no universal base type, uses trait objects (dyn Any) for type erasure. Go: interface{} (or 'any' in Go 1.18+) is the empty interface, no methods guaranteed. Kotlin: Any is the root type with equals/hashCode/toString. EK9: Any is a universal base interface (not class) that carries no usable operator except ?; each type provides ==, <=>, $, $$, #? itself (default operator or explicit), HIGH_COST (20.0) matching ensures specific types always preferred in dispatchers.
Keywords: advanced, default, type, cost, hierarchy, operator, inherit, interface, root, dispatcher, any, fallback, base, migrate, type-system, universal, object