Type Casting

Transpose implements C# casting and type-testing over the runtime's type system. This page describes what is checked at runtime, what is erased, and where the behaviour differs from .NET.

is and as

is, as and pattern matching all work, and are backed by a runtime type check against the object's registered type:

public void Process(object obj)
{
    if (obj is string s)
    {
        Console.WriteLine("String: " + s);
    }
    else if (obj is int i)
    {
        Console.WriteLine("Int: " + i);
    }

    if (obj is IList<string> list)
    {
        Console.WriteLine("List count: " + list.Count);
    }
}

as yields null when the test fails, exactly as in .NET, and is with a pattern handles the type test and the binding together.

Explicit casts

An explicit cast is emitted as a checked cast — it verifies the runtime type and throws InvalidCastException on failure, matching .NET:

object o = "hello";
int i = (int)o;   // throws InvalidCastException

This applies to explicit reference conversions (a downcast like (Dog)someAnimal, an interface cast like (IFoo)obj) and to unboxing conversions ((int)someObject).

Casts that are erased

Not every cast needs a runtime check, and Transpose emits nothing for the ones that cannot fail or cannot be verified. A cast is erased when the target is:

  • An upcast, an identity conversion, or a boxing conversion — it cannot fail.
  • object — everything is an object.
  • A numeric or enum conversion — these are value conversions, not type tests; they are emitted as the appropriate conversion instead (see below).
  • A type parameter (T) or dynamic — there is no concrete type to check.
  • An array type — a JavaScript array carries no element-type metadata, so (Emoji[])Enum.GetValues(…) cannot be verified.
  • A delegate type — a delegate is a plain JavaScript function with no constructible runtime type token.
  • A tuple type.
  • An external (native-JavaScript) type from a binding library — a DOM node or a third-party object has no Transpose type identity to test.
  • A user-defined conversion operator — the operator is called instead.
  • null.

The practical consequence: a cast to a DOM or binding-library type always succeeds, so a wrong one surfaces later as an undefined member rather than as an InvalidCastException at the cast. Casts to your own types and to System.* types are checked.

Numeric conversions

An explicit numeric cast is emitted as the conversion C# requires, not as a type test:

  • To a ≤32-bit integer or char — the value is truncated and wrapped to that bit width, so (byte)300 gives 44 as it does on .NET.
  • To long/ulong — the value is wrapped into the runtime's 64-bit integer type.
  • From long/ulong to a floating type or an enum — the numeric magnitude is read out.
  • To decimal — the value is wrapped into the runtime's decimal type.
  • From decimal to a floating type — the value is read out as a number.
  • charint — erased; a char is its code unit.

Because long and decimal are runtime objects rather than JavaScript numbers, these conversions allocate. See Working with Data.

Casting across the interop boundary

To reinterpret a value whose type only the JavaScript side knows — a payload from an external API, or a value you are treating structurally — a double cast through object is the idiomatic form, and it erases to nothing:

var options = (MyOptions)(object)someExternalValue;

That works because MyOptions here would typically be an [ObjectLiteral] type — a plain JavaScript object with no runtime identity to check.

For an [External] type the cast is erased outright, so a helper needs no attribute:

[External]
public class JsValue
{
    public extern T As<T>();   // erased: a cast to T is never checked
}

Attributes

[IgnoreCast]

In h5, [IgnoreCast] removed the runtime check for casts to the annotated type. Transpose already erases casts to every external (native-JavaScript) type, which was that attribute's dominant use, so [IgnoreCast] is accepted but has no effect — the behaviour it asked for is the default.

[Cast]

h5's [Cast] supplied a custom JavaScript template for a cast. Transpose does not implement it. Use an ordinary method with a [Template] instead, which gives you the same control with an explicit call site:

[External]
public static class Convert
{
    [Template("parseInt({0}, 10)")]
    public static extern int ToInt(object value);
}
There is no global switch for cast checking

h5 had an ignoreCast option in h5.json. Transpose has no equivalent setting: the erasure rules above are fixed. If a checked cast is measurably costing you in a hot path, restructure to a pattern test (is) whose result you reuse, rather than looking for a flag.

See also

  • How It Works — the type system the checks run against, and the struct value-copy caveat.
  • Attribute Reference — the status of every code-generation attribute.
© 2026 Curiosity. All rights reserved.