/typescripten

typescripten is a compiler that creates C++ header files from TypeScript interface definitions.

Primary LanguageC++Apache License 2.0Apache-2.0

CI

typescripten

Calling JavaScript code from C++ via emscripten::val offers no type-safety.

typescripten uses TypeScript interface definition files to generate type-safe C++ interfaces for JavaScript and TypeScript libraries. Compare the JavaScript statements in the comments to the C++ code:

int main() {
    // var elem = document.createElement("p")
    auto elem = js::document()->createElement(js::string("p"));

    // elem.innerText = "Hello CppCon 2021"
    elem->innerText(js::string("Hello CppCon 2021"));

    // elem.style.fontSize = 20.0
    elem->style()->fontSize(js::string("20vh"));

    // document.body.appendChild(elem)
    js::document()->body()->appendChild(elem);
}

typescripten uses the parser API provided by typescript. It is now self-hosting, i.e., it can the parse interface definition file for the typescript parser itself.

typescripten has been presented at CppCon 2021 and in a slightly extended talk at CppNow 2022.

Example

Let's say you have a typescript module MyLib.d.ts for a JavaScript library you have written yourself, or maybe for a JavaScript API to some web service:

declare namespace MyLib {
    function appendNumber(a: string, b: number): string;
}

Running the typescripten compiler on this file will produce the C++ header MyLib.d.h

namespace tc::js_defs {
    using namespace jst; // no ADL
    struct _impl_js_jMyLib;
    using _js_jMyLib = ref<_impl_js_jMyLib>;
    struct _impl_js_jMyLib : virtual object_base {
        struct _tcjs_definitions {
            static auto appendNumber(string a, double b) noexcept;
        };
    };
    inline auto _impl_js_jMyLib::_tcjs_definitions::appendNumber(string a, double b) noexcept {
        return emscripten::val::global("MyLib")["appendNumber"](a, b).template as<string>();
    }
}; // namespace tc::js_defs
namespace tc::js {
    using MyLib = js_defs::_js_jMyLib;
} // namespace tc::js

By including the generated header you can use MyLib from C++ in a type-safe way.

#include "MyLib.d.h"
#include <iostream>

int main() {
    std::cout << tc::explicit_cast<std::string>( // Unpack the JavaScript string 
        tc::js::MyLib::appendNumber(
            tc::js::string("foobar"), // Create a JavaScript string
            20 // JavaScript number type maps to double
        )
    ) << std::endl;
}

See typescripten/tests and typescriptenc/tests for more examples.

Close analogues are Rust's stdweb and wasm-bindgen.

TypeScript language support

typescripten/ contains the code to support basic Typescript types and language features

  • any, undefined, null and string types
  • Support for mixed enums like
    enum E {
        a, 
        b = "I'm a string",
        c = 5.0
    }

typescripten supports

  • type guards
  • type aliases
  • optional arguments and optional member properties
  • generics, see #3

Some TypeScript constructs are not yet supported. Better support for generic type constraints and indexed access types are high priority and coming soon.

Exploring the TypeScript compiler API

Both the TypeScript Playground and the TypeScript AST Viewer (Check the JS debug console!) are invaluable when you want to improve typescripten.

Dependencies

Setup

It is easiest to download typescripten from npm.

npm install -g @think-cell/typescripten

I am working on integrating typescripten with emscripten.

If you want to include it in a project of yours, add the following to your Cmake file:

# Download typescripten (The name must not be changed)
include(FetchContent)
FetchContent_Declare(
  typescripten
  GIT_REPOSITORY https://github.com/think-cell/typescripten
)
FetchContent_MakeAvailable(typescripten)

# Include the CMake helper function
include(${typescripten_SOURCE_DIR}/cmake/TypeScripten.cmake)

# Install the types you need via npm (at configure time)
execute_process(COMMAND npm install WORKING_DIRECTORY ${PROJECT_SOURCE_DIR})

# Add your executable target
add_executable(test ...)

# Compile the downloaded TypeScript interface definitions (here lib.dom.d.ts)
# into a header before 'test' is built.
add_typescripten_target(
  TARGET test
  INPUTS ${PROJECT_SOURCE_DIR}/node_modules/typescript/lib/lib.dom.d.ts
  OUTPUT lib.dom.d.h
)

Now you can include <lib.dom.d.h> in your C++ files. In order to build your project, download emscripten and boost, and run

source path/to/emsdk/emsdk_env.sh
emcmake cmake -S . -B build 
cmake --build build

See example and its associated README.

Debugging

typescriptenc debug builds are built with DWARF debug information. Google Chrome Dev builds can interactively debug WebAssembly applications with debug info. See this Chrome blog entry on how to setup Chrome for debugging.

In order to build with debugging support set the DEBUG_DEVTOOLS options. If you build the typescriptenc_debug target, cmake will package typescripten with webpack so it can run in a browser and it will start a web server for you:

source path/to/emsdk/emsdk_env.sh
emcmake cmake -S . -B build -DDEBUG_DEVTOOLS=ON 
cmake --build build --target typescriptenc_debug

Now you should be able to open [http://localhost:8000/debug] in Chrome and in the DevTools console you should be able to see the C++ sources, set breakpoints, look at local variables etc. This debug build cannot read local files from disk, so the input file is hard-coded in debug/index.html. Change that file and rebuild or change the live version in build/debug/index.html and reload the running page.

Naming conventions

We use Hungarian Notation in our code base, i.e.,

  • Global variables start with g_
  • Member fields start with m_
  • Constants start with c_

All variables are prefixed with a type tag <type-tag>Name

  • Name is camel-case
  • int's type tag is n, e.g. nArguments
  • std::string's type tag is str
  • std::vector<T>'s type tag is vec<type-tag-of-T>, name is singular, e.g. vecstrArguments
  • jst::ref<T>'s type tag is j<type-tag-of-T>
  • ts::Symbol's type tag is sym.
  • jst::optional<T>'s type tag is o<type-tag-of-T>.

Example: jst::ref<jst::optional<ts::Symbol>> josymDeclaration;

There should be no duplicates between type tag and name, e.g. vecstrArguments, not vecstrArgumentsVector