Type Casting (The to Operator)¶
The to operator performs type casting, converting a value from one type to another at runtime.
Two Forms¶
| Syntax | Behavior on Failure | Result Type |
|---|---|---|
value to T |
Throws InvalidCastException |
T |
value to T? |
Returns None |
T? |
Examples¶
# Reference type downcasting
animal: Animal = get_animal()
dog = animal to Dog # Throws if not a Dog
dog = animal to Dog? # None if not a Dog
# Interface casting
obj: object = get_object()
drawable = obj to IDrawable # Throws if doesn't implement IDrawable
drawable = obj to IDrawable? # None if doesn't implement IDrawable
# Unboxing
boxed: object = 42
value = boxed to int # Throws if not an int
value = boxed to int? # None if not an int
# Numeric conversions
big: int64 = 1_000_000
small = big to int # Throws on overflow
small = big to int? # None on overflow
precise: float = 3.14159
rounded = precise to int # Truncates toward zero (3), throws if out of range
rounded = precise to int? # None if out of range
Using Result[T, E] or Optional[T]¶
Casting can be chained with try and maybe expressions to wrap
the cast behavior in safe tagged unions:
my_dog: object = Dog()
some_result = try my_dog to Cat # some_result = Result[Cat, InvalidCastException].Err
some_result = try my_dog to Cat? # some_result = Result[Cat?, Exception].Ok(None). Compiler will warn user to use a `maybe` expression instead
some_optional = maybe my_dog to Cat # Throws. Compiler will warn user to use a `try` expression instead
some_optional = maybe my_dog to Cat? # some_optional = Optional[str].None()
Safe Casting Pattern¶
The nullable form integrates naturally with type narrowing:
animal: Animal = get_animal()
if (dog := animal to Dog?) is not None:
# dog is narrowed to Dog in this block
print(dog.bark())
# Or with simple None check
result = animal to Dog?
if result is not None:
use_dog(result)
Upcasting¶
Upcasts (derived → base) are always safe and can be implicit through assignment:
dog: Dog = Dog("Buddy")
# Explicit upcast (allowed but unnecessary)
animal = dog to Animal
# Implicit upcast (preferred)
animal: Animal = dog
The compiler may emit a warning when to is used for compile-time-safe upcasts, since they're implicit anyway.
Numeric Conversions¶
The to operator handles numeric type conversions including narrowing conversions:
| Conversion | Behavior |
|---|---|
Widening (e.g., int32 → int64) |
Always succeeds |
Narrowing (e.g., int64 → int32) |
Throws/None on overflow |
| Float → Integer | Truncates toward zero, throws/None if out of range |
| Integer → Float | May lose precision (no failure) |
# Widening - always safe
x: int = 42
y = x to int64 # Always succeeds
# Narrowing - may fail
big: int64 = 10_000_000_000
small = big to int # Throws: value too large for int
small = big to int? # None: value too large for int
# Float to integer truncation
pi: float = 3.99
n = pi to int # 3 (truncates toward zero)
neg: float = -3.99
m = neg to int # -3 (truncates toward zero)
# Out of range
huge: float = 1e100
n = huge to int? # None: out of int range
Relationship to Conversion Functions¶
The built-in conversion functions (int(), str(), float(), etc.) remain available and are equivalent to the throwing form of to for their respective types:
# These are equivalent
x = int(value)
x = value to int
# These are equivalent
s = str(value)
s = value to str
# But only `to` provides the safe nullable form
x = value to int? # No equivalent with int()
The conversion functions are retained for Pythonic familiarity, but to is the general-purpose casting mechanism that works with any type:
# Only `to` works for arbitrary types
dog = animal to Dog?
point = data to Point
processor = obj to IProcessor?
Operator Precedence¶
The to operator binds looser than member access, function calls, and arithmetic operators, but tighter than comparison and logical operators:
| Precedence | Operators |
|---|---|
| (higher) | (), [], ., ?. |
** |
|
+x, -x, ~x |
|
*, /, //, % |
|
+, - |
|
<<, >>, &, ^, \| |
|
to |
|
in, is, <, >, ==, etc. |
|
not, and, or, ?? |
|
try, maybe |
|
| (lower) | x if c else y, lambda |
This means:
# Parentheses needed for member access on cast result
name = (animal to Dog).name
result = (obj to IProcessor).process(data)
# Arithmetic binds tighter than `to`
x = value + 1 to int64 # Parsed as: (value + 1) to int64
# No parentheses needed for comparisons
if animal to Dog? is not None:
pass
# Chained with None check
if (dog := animal to Dog?) is not None and dog.age > 5:
pass
# `try` and `maybe` capture the entire cast expression
result = try animal to Dog # Parsed as: try (animal to Dog)
opt = maybe obj to Widget? # Parsed as: maybe (obj to Widget?)
Invalid Casts¶
The compiler rejects casts that are statically known to be impossible:
x: int = 42
s = x to str # ERROR: int cannot be cast to str (use str(x))
dog: Dog = Dog("Buddy")
cat = dog to Cat # ERROR: Dog cannot be cast to Cat (no inheritance relationship)
Casting None¶
Casting None always fails:
Implementation: Lowered
- value to T → (T)value (C# cast expression)
- value to T? → C# value as T for reference types, try-pattern for value types
// value to Dog (throwing)
(Dog)value
// value to Dog? (safe, reference type)
value as Dog
// value to int? (safe, value type - requires pattern)
value is int _temp ? (int?)_temp : null
Note on as¶
as is not a casting operator in Sharpy. The as keyword is reserved for other contexts (exception binding, context managers, import aliases, and match/case pattern binding). See SRP-0005 (rejected proposal) for rationale.
See Also¶
- Conversion Operators — User-defined conversions
- Type Narrowing — Narrowing types with
is not Noneandisinstance() - Nullable Types — Nullable type semantics