Calling C# from JavaScript

Once compiled, your C# types are ordinary JavaScript objects on the global scope, so existing JavaScript can call into them. This page describes the shape the compiler emits, and how to make the surface you expose stable.

Where your types live

A type is registered under its full C# name, and reachable as a global of that name:

using System;

namespace MyProject
{
    public class Utils
    {
        public static void Log(string message)
        {
            Console.WriteLine(message);
        }
    }
}
MyProject.Utils.Log("Hello from JavaScript!");

Static members live on the type itself. Use [Namespace] or [Name] to control the emitted name — see Making the surface stable.

Creating instances

A type's parameterless constructor is reached with plain new:

var p = new MyProject.Person();

Any other constructor is a named factory on the type, numbered in declaration order — $ctor1, $ctor2, … :

namespace MyProject
{
    public class Person
    {
        public string Name { get; set; }

        public Person() { }
        public Person(string name) { Name = name; }

        public void SayHello() => Console.WriteLine("Hello, " + Name);
    }
}
var p = new MyProject.Person.$ctor1("Alice");
p.SayHello();   // Hello, Alice

This numbering is how the runtime models overloaded constructors, which JavaScript does not have. It is stable for a given set of constructors but changes if you add or reorder one, so it is not something to hard-code in JavaScript you maintain separately. See below.

Method overloads

JavaScript has no overloading either, so overloads take numbered slots: the first in declaration order keeps the plain name, and the rest get a $1, $2, … suffix.

public static void Log(string message) { }          // → Log
public static void Log(string message, int level) { } // → Log$1
MyProject.Utils.Log("plain");
MyProject.Utils.Log$1("with level", 2);

Some methods are exempt from numbering and always keep their plain name: ToString(), GetHashCode(), Equals(object), anything with an explicit [Name], and members of an [External] type.

Properties and fields

  • An auto-property and a field are both plain data properties, under their C# name: p.Name.
  • A property with a body becomes get_Name() / set_Name(value).
var p = new MyProject.Person.$ctor1("Alice");
p.Name = "Bob";               // auto-property: a plain field
console.log(p.get_Title());   // computed property: an accessor

Passing callbacks

A JavaScript function can be passed wherever C# expects a delegate:

namespace MyProject
{
    public class App
    {
        public static void Run(Action<string> callback) => callback("Success!");
    }
}
MyProject.App.Run(function (message) {
    alert(message);
});

This is the cleanest direction across the boundary, and it is the recommended way to return an asynchronous result to JavaScript — a C# Task is a runtime object rather than a promise, so it needs Transpose.toPromise to be awaited. See Async Support.

What crosses cleanly

C# Seen from JavaScript
string, bool, int/double and friends the plain JavaScript value
arrays of those a JavaScript array
[ObjectLiteral] types a plain object
Action / Func a function
long, decimal, DateTime, Guid a runtime object — convert first
List<T>, Dictionary<K,V> a runtime object, not an array/plain object
Task a runtime task — wrap with Transpose.toPromise
an ordinary class instance a runtime object with a prototype chain

See Working with Data for the details.

Making the surface stable

The numbering rules above mean that the default emitted names are an implementation detail: adding a constructor or an overload renumbers the ones after it. If JavaScript you maintain elsewhere calls into your C#, do not let it depend on that. Two options:

Pin the names

[Name] fixes the emitted name and opts the member out of overload numbering, and [Namespace] controls where the type lands:

using Transpose;

[Namespace("acme")]
[Name("Api")]
public static class PublicApi
{
    [Name("greet")]
    public static string Greet(string who) => $"Hello, {who}";
}
acme.Api.greet("world");

Because [Name] bypasses overload numbering, two overloads given the same [Name] would collide — give each a distinct name.

Or expose one flat entry point

Simplest of all: declare a single static class of non-overloaded methods with primitive and [ObjectLiteral] parameters, [Name] it, and treat it as your JavaScript-facing API. Everything else stays internal and can be refactored freely.

using Transpose;

[Namespace(false)]        // no namespace prefix — a bare global
[Name("MyApp")]
public static class JsApi
{
    [Name("start")]
    public static void Start(string mountSelector) { /* … */ }

    [Name("onEvent")]
    public static void OnEvent(Action<string> handler) { /* … */ }
}
MyApp.start("#root");
MyApp.onEvent(function (e) { console.log(e); });

See also

© 2026 Curiosity. All rights reserved.