diff --git a/README.md b/README.md
index abe2e3c6..e995dce3 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,7 @@ Djinni generator parses an interface definition file and generates:
 - Objective-C++ code to convert between C++ and Objective-C
 - Python and C code to convert between C++ and Python over CFFI
 - C++/CLI code to convert between C++ and C#
+- C++ code to convert between WebAssembly and TS/JS
 
 
 ## Installation
@@ -86,3 +87,5 @@ The code in this repository is in large portions copied from [dropbox/djinni](ht
 - Jacob Potter
 - Iulia Tamas
 - Andrew Twyman
+
+WebAssembly support is borrowed in large part from [snapchat/djinni](https://github.com/snapchat/djinni).
\ No newline at end of file
diff --git a/docs/cli-usage.md b/docs/cli-usage.md
index b05d5bbf..f9e9840b 100644
--- a/docs/cli-usage.md
+++ b/docs/cli-usage.md
@@ -145,6 +145,18 @@ djinni \
 | `--cppcli-namespace ...`               | The namespace name to use for generated C++/CLI classes.                   |
 | `--cppcli-include-cpp-prefix <prefix>` | The prefix for `#include` of the main C++ header files from C++/CLI files. |
 
+### WebAssembly/Typescript/Javascript
+
+| Argument                               | Description                                                                |
+|----------------------------------------|----------------------------------------------------------------------------|
+| `--wasm-out <out-folder>`              | WebAssembly bridge code output folder.                                     |
+| `--wasm-include-prefix`                | The prefix for #includes of WASM bridge C++ header files.                  |
+| `--wasm-include-cpp-prefix`            | The prefix for #includes of C++ header files.                              |
+| `--wasm-base-lib-include-prefix`       | The path prefix to be added to djinni support library inlcude lines in generated files |
+| `--ts-out <out-folder>`                | Path to the Typescript type definitions output folder                      |
+| `--ts-module <module>`                 | Name of the module for the Typescript types. `module.ts` by default.       |
+| `--ts-support-files-out <out-folder>`  | Path for where the support files `DjinniModule.[js\|ts]` should be generated. No support files are generated if the path is not specified. |
+
 
 ### Yaml Generation
 
@@ -230,6 +242,19 @@ Possible values: `FooBar`, `fooBar`, `foo_bar`, `FOO_BAR`, `m_fooBar`.
 | `--ident-cppcli-const`      | `FooBar` |
 | `--ident-cppcli-file`       | `FooBar` |
 
+#### Javascript / Typescript
+
+| Argument                | Default   |
+|-------------------------|-----------|
+| `--ident-js-type`       | `FooBar`  |
+| `--ident-js-type-param` | `FooBar`  |
+| `--ident-js-method`     | `fooBar` |
+| `--ident-js-local`      | `fooBar` |
+| `--ident-js-enum`       | `FOO_BAR` |
+| `--ident-js-const`      | `FOO_BAR` |
+| `--ident-js-field`      | `fooBar`  |
+
+
 Example:
 
 The djinni idl for an enum
diff --git a/docs/generated-code-usage.md b/docs/generated-code-usage.md
index c2f88304..ec72ea75 100644
--- a/docs/generated-code-usage.md
+++ b/docs/generated-code-usage.md
@@ -166,6 +166,42 @@ Add all generated files to your build target, and link against the [djinni-suppo
 
 C++/CLI sources have to be compiled with MSVC and the [`/clr` (Common Language Runtime Compilation)](https://docs.microsoft.com/en-us/cpp/build/reference/clr-common-language-runtime-compilation?view=msvc-160) option.
 
+## TS/JS C++/WASM Project
+
+Djinni can generate code that bridges C++ (that compiles to Web Assembly) and Javascript/TypeScript in web browsers.
+
+For WASM, Djinni generates:
+- C++ code, which should be compiled into the WASM bindary
+- TypeScript code, provides optional TypeScript interface definitions
+
+The generated code can be used with both plain Javascript and TypeScript.
+
+Almost all Djinni features are supported, including importing external types via
+yaml.
+
+Notable differences when comparing to the Java/ObjC support:
+
+- deriving(ord, eq) is not applicable to Javascript because JS doesn't support
+  overloading standard comparison methods.
+
+### Includes & Build target
+
+The following code will be generated for each defined type:
+
+| Type       | C++ header               | C++ source                 | WASM header/sources                 |  TS definitions           |
+|------------|--------------------------|----------------------------|-------------------------------------|---------------------------|
+| Enum/Flags | my\_enum.hpp             |                            | my_enum.hpp, my_enum.cpp            | module.ts :three:         |
+|            | my\_enum+json.hpp :two:  |                            |                                     | DjinniModule.ts/js :four: |
+| Record     | my\_record.hpp           | my\_record.cpp             | my_record.hpp, my_enum.cpp          |                           |
+|            | my\_record+json.hpp :two:|                            |                                     |                           |
+| Interface  | my\_interface.hpp        | my\_interface.cpp :one:    | my_interface.hpp, my_interface.cpp  |                           |
+
+- :one: Generated only for types that contain constants.
+- :two: Generated only if cpp json serialization is enabled.
+- :three: Name of file configurable via command-line options.
+- :four: Generated if `ts-support-files-out` is specified at command line.
+
+
 ## C++ JSON Serialization support
 
 Serialization from C++ types to/from JSON is supported. This feature is currently only enabled for `nlohmann/json`, and if enabled creates `to_json`/`from_json` methods for all djinni records and enums.
@@ -219,4 +255,4 @@ namespace nlohmann {
         }
     };
 }
-```
\ No newline at end of file
+```
diff --git a/src/it/resources/cpp_interface_dependency.yml b/src/it/resources/cpp_interface_dependency.yml
index bd697c50..32b32405 100644
--- a/src/it/resources/cpp_interface_dependency.yml
+++ b/src/it/resources/cpp_interface_dependency.yml
@@ -36,3 +36,10 @@ cs:
   header: '"InterfaceDependency.hpp"'
   typename: InterfaceDependency^
   reference: true
+wasm:
+  translator: '::InterfaceDependency'
+  header: '"InterfaceDependency.hpp"'
+  typename: InterfaceDependency
+ts:
+  typename: InterfaceDependency
+  module: "InterfaceDependency.ts"
diff --git a/src/it/resources/expected/all_datatypes/generated-files.txt b/src/it/resources/expected/all_datatypes/generated-files.txt
index f5b6ec3a..ccea0aea 100644
--- a/src/it/resources/expected/all_datatypes/generated-files.txt
+++ b/src/it/resources/expected/all_datatypes/generated-files.txt
@@ -38,3 +38,11 @@ src/it/resources/result/all_datatypes/cwrapper/dh__map_int8_t_bool.cpp
 src/it/resources/result/all_datatypes/cwrapper-headers/dh__all_datatypes.h
 src/it/resources/result/all_datatypes/cwrapper/dh__all_datatypes.hpp
 src/it/resources/result/all_datatypes/cwrapper/dh__all_datatypes.cpp
+src/it/resources/result/all_datatypes/wasm/enum_data.hpp
+src/it/resources/result/all_datatypes/wasm/enum_data.cpp
+src/it/resources/result/all_datatypes/wasm/all_datatypes.hpp
+src/it/resources/result/all_datatypes/wasm/all_datatypes.cpp
+src/it/resources/result/all_datatypes/ts/support-lib/DjinniModule.ts
+src/it/resources/result/all_datatypes/ts/support-lib/DjinniModule.js
+src/it/resources/result/all_datatypes/ts/module.ts
+
diff --git a/src/it/resources/expected/all_datatypes/ts/module.ts b/src/it/resources/expected/all_datatypes/ts/module.ts
new file mode 100644
index 00000000..16f7e333
--- /dev/null
+++ b/src/it/resources/expected/all_datatypes/ts/module.ts
@@ -0,0 +1,33 @@
+// AUTOGENERATED FILE - DO NOT MODIFY!
+// This file was generated by Djinni from all_datatypes.djinni
+
+
+export enum EnumData {
+    A,
+    B,
+}
+
+export interface /*record*/ AllDatatypes {
+    booleanData: boolean;
+    integer8Data: number;
+    integer16Data: number;
+    integer32Data: number;
+    integer64Data: bigint;
+    float32Data: number;
+    float64Data: number;
+    stringData: string;
+    binaryData: Uint8Array;
+    dateData: Date;
+    listData: Array<boolean>;
+    setData: Set<boolean>;
+    mapData: Map<number, boolean>;
+    optionalData?: boolean;
+    enumData: EnumData;
+}
+
+export interface ns_testsuite {
+}
+export interface Module_statics {
+
+    testsuite: ns_testsuite;
+}
diff --git a/src/it/resources/expected/all_datatypes/wasm/all_datatypes.cpp b/src/it/resources/expected/all_datatypes/wasm/all_datatypes.cpp
new file mode 100644
index 00000000..ad5b46f2
--- /dev/null
+++ b/src/it/resources/expected/all_datatypes/wasm/all_datatypes.cpp
@@ -0,0 +1,46 @@
+// AUTOGENERATED FILE - DO NOT MODIFY!
+// This file was generated by Djinni from all_datatypes.djinni
+
+#include "all_datatypes.hpp"  // my header
+#include "enum_data.hpp"
+
+namespace djinni_generated {
+
+auto AllDatatypes::toCpp(const JsType& j) -> CppType {
+    return {::djinni::Bool::Boxed::toCpp(j["booleanData"]),
+            ::djinni::I8::Boxed::toCpp(j["integer8Data"]),
+            ::djinni::I16::Boxed::toCpp(j["integer16Data"]),
+            ::djinni::I32::Boxed::toCpp(j["integer32Data"]),
+            ::djinni::I64::Boxed::toCpp(j["integer64Data"]),
+            ::djinni::F32::Boxed::toCpp(j["float32Data"]),
+            ::djinni::F64::Boxed::toCpp(j["float64Data"]),
+            ::djinni::String::Boxed::toCpp(j["stringData"]),
+            ::djinni::Binary::Boxed::toCpp(j["binaryData"]),
+            ::djinni::Date::Boxed::toCpp(j["dateData"]),
+            ::djinni::List<::djinni::Bool>::Boxed::toCpp(j["listData"]),
+            ::djinni::Set<::djinni::Bool>::Boxed::toCpp(j["setData"]),
+            ::djinni::Map<::djinni::I8, ::djinni::Bool>::Boxed::toCpp(j["mapData"]),
+            ::djinni::Optional<std::optional, ::djinni::Bool>::Boxed::toCpp(j["optionalData"]),
+            ::djinni_generated::EnumData::Boxed::toCpp(j["enumData"])};
+}
+auto AllDatatypes::fromCpp(const CppType& c) -> JsType {
+    em::val js = em::val::object();
+    js.set("booleanData", ::djinni::Bool::Boxed::fromCpp(c.booleanData));
+    js.set("integer8Data", ::djinni::I8::Boxed::fromCpp(c.integer8Data));
+    js.set("integer16Data", ::djinni::I16::Boxed::fromCpp(c.integer16Data));
+    js.set("integer32Data", ::djinni::I32::Boxed::fromCpp(c.integer32Data));
+    js.set("integer64Data", ::djinni::I64::Boxed::fromCpp(c.integer64Data));
+    js.set("float32Data", ::djinni::F32::Boxed::fromCpp(c.float32Data));
+    js.set("float64Data", ::djinni::F64::Boxed::fromCpp(c.float64Data));
+    js.set("stringData", ::djinni::String::Boxed::fromCpp(c.stringData));
+    js.set("binaryData", ::djinni::Binary::Boxed::fromCpp(c.binaryData));
+    js.set("dateData", ::djinni::Date::Boxed::fromCpp(c.dateData));
+    js.set("listData", ::djinni::List<::djinni::Bool>::Boxed::fromCpp(c.listData));
+    js.set("setData", ::djinni::Set<::djinni::Bool>::Boxed::fromCpp(c.setData));
+    js.set("mapData", ::djinni::Map<::djinni::I8, ::djinni::Bool>::Boxed::fromCpp(c.mapData));
+    js.set("optionalData", ::djinni::Optional<std::optional, ::djinni::Bool>::Boxed::fromCpp(c.optionalData));
+    js.set("enumData", ::djinni_generated::EnumData::Boxed::fromCpp(c.enum_data));
+    return js;
+}
+
+}  // namespace djinni_generated
diff --git a/src/it/resources/expected/all_datatypes/wasm/all_datatypes.hpp b/src/it/resources/expected/all_datatypes/wasm/all_datatypes.hpp
new file mode 100644
index 00000000..e2f6a108
--- /dev/null
+++ b/src/it/resources/expected/all_datatypes/wasm/all_datatypes.hpp
@@ -0,0 +1,21 @@
+// AUTOGENERATED FILE - DO NOT MODIFY!
+// This file was generated by Djinni from all_datatypes.djinni
+
+#pragma once
+
+#include "all_datatypes.hpp"
+#include "djinni_wasm.hpp"
+
+namespace djinni_generated {
+
+struct AllDatatypes
+{
+    using CppType = ::testsuite::AllDatatypes;
+    using JsType = em::val;
+    using Boxed = AllDatatypes;
+
+    static CppType toCpp(const JsType& j);
+    static JsType fromCpp(const CppType& c);
+};
+
+}  // namespace djinni_generated
diff --git a/src/it/resources/expected/all_datatypes/wasm/enum_data.cpp b/src/it/resources/expected/all_datatypes/wasm/enum_data.cpp
new file mode 100644
index 00000000..95761b71
--- /dev/null
+++ b/src/it/resources/expected/all_datatypes/wasm/enum_data.cpp
@@ -0,0 +1,30 @@
+// AUTOGENERATED FILE - DO NOT MODIFY!
+// This file was generated by Djinni from all_datatypes.djinni
+
+#include "enum_data.hpp"  // my header
+#include <mutex>
+
+namespace djinni_generated {
+
+namespace {
+    EM_JS(void, djinni_init_testsuite_enum_data_consts, (), {
+        Module.testsuite_EnumData =  {
+            A : 0,
+            B : 1,
+        }
+    })
+}
+
+void EnumData::staticInitializeConstants() {
+    static std::once_flag initOnce;
+    std::call_once(initOnce, [] {
+        djinni_init_testsuite_enum_data_consts();
+        ::djinni::djinni_register_name_in_ns("testsuite_EnumData", "testsuite.EnumData");
+    });
+}
+
+EMSCRIPTEN_BINDINGS(testsuite_enum_data) {
+    EnumData::staticInitializeConstants();
+}
+
+}  // namespace djinni_generated
diff --git a/src/it/resources/expected/all_datatypes/wasm/enum_data.hpp b/src/it/resources/expected/all_datatypes/wasm/enum_data.hpp
new file mode 100644
index 00000000..43d82fee
--- /dev/null
+++ b/src/it/resources/expected/all_datatypes/wasm/enum_data.hpp
@@ -0,0 +1,15 @@
+// AUTOGENERATED FILE - DO NOT MODIFY!
+// This file was generated by Djinni from all_datatypes.djinni
+
+#pragma once
+
+#include "djinni_wasm.hpp"
+#include "enum_data.hpp"
+
+namespace djinni_generated {
+
+struct EnumData: ::djinni::WasmEnum<::testsuite::EnumData> {
+    static void staticInitializeConstants();
+};
+
+}  // namespace djinni_generated
diff --git a/src/it/resources/expected/deprecation/generated-files.txt b/src/it/resources/expected/deprecation/generated-files.txt
index f31025c9..8ff1673d 100644
--- a/src/it/resources/expected/deprecation/generated-files.txt
+++ b/src/it/resources/expected/deprecation/generated-files.txt
@@ -3,3 +3,7 @@ src/it/resources/result/deprecation/cpp-headers/my_flags.hpp
 src/it/resources/result/deprecation/cpp-headers/my_record.hpp
 src/it/resources/result/deprecation/cpp-headers/my_interface.hpp
 src/it/resources/result/deprecation/cpp/my_interface.cpp
+src/it/resources/result/deprecation/ts/support-lib/DjinniModule.ts
+src/it/resources/result/deprecation/ts/support-lib/DjinniModule.js
+src/it/resources/result/deprecation/ts/module.ts
+
diff --git a/src/it/resources/expected/deprecation/ts/module.ts b/src/it/resources/expected/deprecation/ts/module.ts
new file mode 100644
index 00000000..66be5e52
--- /dev/null
+++ b/src/it/resources/expected/deprecation/ts/module.ts
@@ -0,0 +1,78 @@
+// AUTOGENERATED FILE - DO NOT MODIFY!
+// This file was generated by Djinni from deprecation.djinni
+
+
+/**
+ * enum comment
+ *
+ * @deprecated Use something else
+ */
+export enum MyEnum {
+    /** @deprecated Use something else */
+    OPTION1,
+    /** not deprecated */
+    OPTION2,
+}
+
+/**
+ * flags comment
+ *
+ * @deprecated Use someother flags
+ */
+export enum MyFlags {
+    /** @deprecated Use someother flag */
+    FLAG1 = 1 << 0,
+    /** not deprecated */
+    FLAG2 = 1 << 1,
+}
+
+/**
+ * record comment
+ *
+ * @deprecated Use someother record
+ */
+export interface /*record*/ MyRecord {
+    /** @deprecated Use someother attribute */
+    attribute: string;
+    /** not deprecated */
+    another: string;
+    /** @deprecated Use someother attribute */
+    again: string;
+}
+export namespace MyRecord {
+    /** @deprecated Use someother constant */
+    export const VERSION = 1;
+}
+
+/**
+ * interface comment
+ *
+ * @deprecated Use someother interface
+ */
+export interface MyInterface {
+    /** @deprecated Use someother method */
+    methodA(value: number): void;
+    /** @deprecated Use someother method */
+    methodB(value: number): void;
+    /** not deprecated */
+    methodD(): void;
+    /** really im not */
+    methodE(): void;
+}
+export namespace MyInterface {
+    /** @deprecated Use someother constant */
+    export const VERSION = 1;
+}
+export interface MyInterface_statics {
+    /** @deprecated Use someother method */
+    methodC(value: number): void;
+}
+
+export interface ns_testsuite {
+    MyInterface: MyInterface_statics;
+}
+export interface Module_statics {
+    testsuite_MyInterface: MyInterface_statics;
+
+    testsuite: ns_testsuite;
+}
diff --git a/src/it/resources/expected/my_client_interface/generated-files.txt b/src/it/resources/expected/my_client_interface/generated-files.txt
index 09c24044..09e9aaad 100644
--- a/src/it/resources/expected/my_client_interface/generated-files.txt
+++ b/src/it/resources/expected/my_client_interface/generated-files.txt
@@ -14,3 +14,8 @@ src/it/resources/result/my_client_interface/cwrapper-headers/cw__my_client_inter
 src/it/resources/result/my_client_interface/cwrapper/cw__my_client_interface.hpp
 src/it/resources/result/my_client_interface/cwrapper/cw__my_client_interface.cpp
 src/it/resources/result/my_client_interface/cffi/pycffi_lib_build.py
+src/it/resources/result/my_client_interface/wasm/my_client_interface.hpp
+src/it/resources/result/my_client_interface/wasm/my_client_interface.cpp
+src/it/resources/result/my_client_interface/ts/support-lib/DjinniModule.ts
+src/it/resources/result/my_client_interface/ts/support-lib/DjinniModule.js
+src/it/resources/result/my_client_interface/ts/module.ts
diff --git a/src/it/resources/expected/my_client_interface/ts/module.ts b/src/it/resources/expected/my_client_interface/ts/module.ts
new file mode 100644
index 00000000..f9258509
--- /dev/null
+++ b/src/it/resources/expected/my_client_interface/ts/module.ts
@@ -0,0 +1,10 @@
+// AUTOGENERATED FILE - DO NOT MODIFY!
+// This file was generated by Djinni from my_client_interface.djinni
+
+
+export interface MyClientInterface {
+    logString(str: string): boolean;
+}
+
+export interface Module_statics {
+}
diff --git a/src/it/resources/expected/my_client_interface/wasm/my_client_interface.cpp b/src/it/resources/expected/my_client_interface/wasm/my_client_interface.cpp
new file mode 100644
index 00000000..8863c847
--- /dev/null
+++ b/src/it/resources/expected/my_client_interface/wasm/my_client_interface.cpp
@@ -0,0 +1,16 @@
+// AUTOGENERATED FILE - DO NOT MODIFY!
+// This file was generated by Djinni from my_client_interface.djinni
+
+#include "my_client_interface.hpp"  // my header
+
+namespace djinni_generated {
+
+
+EMSCRIPTEN_BINDINGS(_my_client_interface) {
+    em::class_<::MyClientInterface>("MyClientInterface")
+        .smart_ptr<std::shared_ptr<::MyClientInterface>>("MyClientInterface")
+        .function("nativeDestroy", &MyClientInterface::nativeDestroy)
+        ;
+}
+
+}  // namespace djinni_generated
diff --git a/src/it/resources/expected/my_client_interface/wasm/my_client_interface.hpp b/src/it/resources/expected/my_client_interface/wasm/my_client_interface.hpp
new file mode 100644
index 00000000..477cc61c
--- /dev/null
+++ b/src/it/resources/expected/my_client_interface/wasm/my_client_interface.hpp
@@ -0,0 +1,27 @@
+// AUTOGENERATED FILE - DO NOT MODIFY!
+// This file was generated by Djinni from my_client_interface.djinni
+
+#pragma once
+
+#include "djinni_wasm.hpp"
+#include "my_client_interface.hpp"
+
+namespace djinni_generated {
+
+struct MyClientInterface : ::djinni::JsInterface<::MyClientInterface, MyClientInterface> {
+    using CppType = std::shared_ptr<::MyClientInterface>;
+    using CppOptType = std::shared_ptr<::MyClientInterface>;
+    using JsType = em::val;
+    using Boxed = MyClientInterface;
+
+    static CppType toCpp(JsType j) { return _fromJs(j); }
+    static JsType fromCppOpt(const CppOptType& c) { return {_toJs(c)}; }
+    static JsType fromCpp(const CppType& c) {
+        ::djinni::checkForNull(c.get(), "MyClientInterface::fromCpp");
+        return fromCppOpt(c);
+    }
+
+
+};
+
+}  // namespace djinni_generated
diff --git a/src/it/resources/expected/my_cpp_interface/generated-files.txt b/src/it/resources/expected/my_cpp_interface/generated-files.txt
index 85da5b3a..082de032 100644
--- a/src/it/resources/expected/my_cpp_interface/generated-files.txt
+++ b/src/it/resources/expected/my_cpp_interface/generated-files.txt
@@ -16,3 +16,9 @@ src/it/resources/result/my_cpp_interface/cwrapper-headers/cw__my_cpp_interface.h
 src/it/resources/result/my_cpp_interface/cwrapper/cw__my_cpp_interface.hpp
 src/it/resources/result/my_cpp_interface/cwrapper/cw__my_cpp_interface.cpp
 src/it/resources/result/my_cpp_interface/cffi/pycffi_lib_build.py
+src/it/resources/result/my_cpp_interface/wasm/my_cpp_interface.hpp
+src/it/resources/result/my_cpp_interface/wasm/my_cpp_interface.cpp
+src/it/resources/result/my_cpp_interface/ts/support-lib/DjinniModule.ts
+src/it/resources/result/my_cpp_interface/ts/support-lib/DjinniModule.js
+src/it/resources/result/my_cpp_interface/ts/module.ts
+
diff --git a/src/it/resources/expected/my_cpp_interface/ts/module.ts b/src/it/resources/expected/my_cpp_interface/ts/module.ts
new file mode 100644
index 00000000..b6c2db7a
--- /dev/null
+++ b/src/it/resources/expected/my_cpp_interface/ts/module.ts
@@ -0,0 +1,22 @@
+// AUTOGENERATED FILE - DO NOT MODIFY!
+// This file was generated by Djinni from my_cpp_interface.djinni
+
+
+/** interface comment */
+export interface MyCppInterface {
+    /** method comment */
+    methodReturningNothing(value: number): void;
+    methodReturningSomeType(key: string): number;
+    methodChangingNothing(): number;
+}
+export namespace MyCppInterface {
+    /** Interfaces can also have constants */
+    export const VERSION = 1;
+}
+export interface MyCppInterface_statics {
+    getVersion(): number;
+}
+
+export interface Module_statics {
+    MyCppInterface: MyCppInterface_statics;
+}
diff --git a/src/it/resources/expected/my_cpp_interface/wasm/my_cpp_interface.cpp b/src/it/resources/expected/my_cpp_interface/wasm/my_cpp_interface.cpp
new file mode 100644
index 00000000..4ab83b7c
--- /dev/null
+++ b/src/it/resources/expected/my_cpp_interface/wasm/my_cpp_interface.cpp
@@ -0,0 +1,83 @@
+// AUTOGENERATED FILE - DO NOT MODIFY!
+// This file was generated by Djinni from my_cpp_interface.djinni
+
+#include "my_cpp_interface.hpp"  // my header
+
+namespace djinni_generated {
+
+em::val MyCppInterface::cppProxyMethods() {
+    static const em::val methods = em::val::array(std::vector<std::string> {
+        "methodReturningNothing",
+        "methodReturningSomeType",
+        "methodChangingNothing",
+    });
+    return methods;
+}
+
+void MyCppInterface::method_returning_nothing(const CppType& self, int32_t w_value) {
+    try {
+        self->method_returning_nothing(::djinni::I32::toCpp(w_value));
+    }
+    catch(const std::exception& e) {
+        return ::djinni::ExceptionHandlingTraits<void>::handleNativeException(e);
+    }
+}
+int32_t MyCppInterface::method_returning_some_type(const CppType& self, const std::string& w_key) {
+    try {
+        auto r = self->method_returning_some_type(::djinni::String::toCpp(w_key));
+        return ::djinni::I32::fromCpp(r);
+    }
+    catch(const std::exception& e) {
+        return ::djinni::ExceptionHandlingTraits<::djinni::I32>::handleNativeException(e);
+    }
+}
+int32_t MyCppInterface::method_changing_nothing(const CppType& self) {
+    try {
+        auto r = self->method_changing_nothing();
+        return ::djinni::I32::fromCpp(r);
+    }
+    catch(const std::exception& e) {
+        return ::djinni::ExceptionHandlingTraits<::djinni::I32>::handleNativeException(e);
+    }
+}
+int32_t MyCppInterface::get_version() {
+    try {
+        auto r = ::MyCppInterface::get_version();
+        return ::djinni::I32::fromCpp(r);
+    }
+    catch(const std::exception& e) {
+        return ::djinni::ExceptionHandlingTraits<::djinni::I32>::handleNativeException(e);
+    }
+}
+
+EMSCRIPTEN_BINDINGS(_my_cpp_interface) {
+    em::class_<::MyCppInterface>("MyCppInterface")
+        .smart_ptr<std::shared_ptr<::MyCppInterface>>("MyCppInterface")
+        .function("nativeDestroy", &MyCppInterface::nativeDestroy)
+        .function("methodReturningNothing", MyCppInterface::method_returning_nothing)
+        .function("methodReturningSomeType", MyCppInterface::method_returning_some_type)
+        .function("methodChangingNothing", MyCppInterface::method_changing_nothing)
+        .class_function("getVersion", MyCppInterface::get_version)
+        ;
+}
+
+namespace {
+    EM_JS(void, djinni_init__my_cpp_interface_consts, (), {
+        if (!('MyCppInterface' in Module)) {
+            Module.MyCppInterface = {};
+        }
+        Module.MyCppInterface.VERSION = 1;
+    })
+}
+void MyCppInterface::staticInitializeConstants() {
+    static std::once_flag initOnce;
+    std::call_once(initOnce, [] {
+        djinni_init__my_cpp_interface_consts();
+    });
+}
+
+EMSCRIPTEN_BINDINGS(_my_cpp_interface_consts) {
+    MyCppInterface::staticInitializeConstants();
+}
+
+}  // namespace djinni_generated
diff --git a/src/it/resources/expected/my_cpp_interface/wasm/my_cpp_interface.hpp b/src/it/resources/expected/my_cpp_interface/wasm/my_cpp_interface.hpp
new file mode 100644
index 00000000..698e7c2c
--- /dev/null
+++ b/src/it/resources/expected/my_cpp_interface/wasm/my_cpp_interface.hpp
@@ -0,0 +1,34 @@
+// AUTOGENERATED FILE - DO NOT MODIFY!
+// This file was generated by Djinni from my_cpp_interface.djinni
+
+#pragma once
+
+#include "djinni_wasm.hpp"
+#include "my_cpp_interface.hpp"
+
+namespace djinni_generated {
+
+struct MyCppInterface : ::djinni::JsInterface<::MyCppInterface, MyCppInterface> {
+    using CppType = std::shared_ptr<::MyCppInterface>;
+    using CppOptType = std::shared_ptr<::MyCppInterface>;
+    using JsType = em::val;
+    using Boxed = MyCppInterface;
+
+    static CppType toCpp(JsType j) { return _fromJs(j); }
+    static JsType fromCppOpt(const CppOptType& c) { return {_toJs(c)}; }
+    static JsType fromCpp(const CppType& c) {
+        ::djinni::checkForNull(c.get(), "MyCppInterface::fromCpp");
+        return fromCppOpt(c);
+    }
+
+    static em::val cppProxyMethods();
+
+    static void method_returning_nothing(const CppType& self, int32_t w_value);
+    static int32_t method_returning_some_type(const CppType& self, const std::string& w_key);
+    static int32_t method_changing_nothing(const CppType& self);
+    static int32_t get_version();
+
+    static void staticInitializeConstants();
+};
+
+}  // namespace djinni_generated
diff --git a/src/it/resources/expected/my_enum/generated-files.txt b/src/it/resources/expected/my_enum/generated-files.txt
index 4a177538..301dedf4 100644
--- a/src/it/resources/expected/my_enum/generated-files.txt
+++ b/src/it/resources/expected/my_enum/generated-files.txt
@@ -11,3 +11,9 @@ src/it/resources/result/my_enum/python/my_enum.py
 src/it/resources/result/my_enum/cwrapper-headers/dh__my_enum.h
 src/it/resources/result/my_enum/cwrapper/dh__my_enum.hpp
 src/it/resources/result/my_enum/cwrapper/dh__my_enum.cpp
+src/it/resources/result/my_enum/wasm/my_enum.hpp
+src/it/resources/result/my_enum/wasm/my_enum.cpp
+src/it/resources/result/my_enum/ts/support-lib/DjinniModule.ts
+src/it/resources/result/my_enum/ts/support-lib/DjinniModule.js
+src/it/resources/result/my_enum/ts/module.ts
+
diff --git a/src/it/resources/expected/my_enum/ts/module.ts b/src/it/resources/expected/my_enum/ts/module.ts
new file mode 100644
index 00000000..dc3710fb
--- /dev/null
+++ b/src/it/resources/expected/my_enum/ts/module.ts
@@ -0,0 +1,18 @@
+// AUTOGENERATED FILE - DO NOT MODIFY!
+// This file was generated by Djinni from my_enum.djinni
+
+
+/** enum comment */
+export enum MyEnum {
+    /** enum option comment */
+    OPTION1,
+    OPTION2,
+    OPTION3,
+}
+
+export interface ns_testsuite {
+}
+export interface Module_statics {
+
+    testsuite: ns_testsuite;
+}
diff --git a/src/it/resources/expected/my_enum/wasm/my_enum.cpp b/src/it/resources/expected/my_enum/wasm/my_enum.cpp
new file mode 100644
index 00000000..34b79374
--- /dev/null
+++ b/src/it/resources/expected/my_enum/wasm/my_enum.cpp
@@ -0,0 +1,32 @@
+// AUTOGENERATED FILE - DO NOT MODIFY!
+// This file was generated by Djinni from my_enum.djinni
+
+#include "my_enum.hpp"  // my header
+#include <mutex>
+
+namespace djinni_generated {
+
+namespace {
+    EM_JS(void, djinni_init_testsuite_my_enum_consts, (), {
+        Module.testsuite_MyEnum =  {
+            /** enum option comment */
+            OPTION1 : 0,
+            OPTION2 : 1,
+            OPTION3 : 2,
+        }
+    })
+}
+
+void MyEnum::staticInitializeConstants() {
+    static std::once_flag initOnce;
+    std::call_once(initOnce, [] {
+        djinni_init_testsuite_my_enum_consts();
+        ::djinni::djinni_register_name_in_ns("testsuite_MyEnum", "testsuite.MyEnum");
+    });
+}
+
+EMSCRIPTEN_BINDINGS(testsuite_my_enum) {
+    MyEnum::staticInitializeConstants();
+}
+
+}  // namespace djinni_generated
diff --git a/src/it/resources/expected/my_enum/wasm/my_enum.hpp b/src/it/resources/expected/my_enum/wasm/my_enum.hpp
new file mode 100644
index 00000000..ac3221c0
--- /dev/null
+++ b/src/it/resources/expected/my_enum/wasm/my_enum.hpp
@@ -0,0 +1,15 @@
+// AUTOGENERATED FILE - DO NOT MODIFY!
+// This file was generated by Djinni from my_enum.djinni
+
+#pragma once
+
+#include "djinni_wasm.hpp"
+#include "my_enum.hpp"
+
+namespace djinni_generated {
+
+struct MyEnum: ::djinni::WasmEnum<::testsuite::MyEnum> {
+    static void staticInitializeConstants();
+};
+
+}  // namespace djinni_generated
diff --git a/src/it/resources/expected/my_flags/generated-files.txt b/src/it/resources/expected/my_flags/generated-files.txt
index 197936ff..991833a6 100644
--- a/src/it/resources/expected/my_flags/generated-files.txt
+++ b/src/it/resources/expected/my_flags/generated-files.txt
@@ -11,3 +11,9 @@ src/it/resources/result/my_flags/python/my_flags.py
 src/it/resources/result/my_flags/cwrapper-headers/dh__my_flags.h
 src/it/resources/result/my_flags/cwrapper/dh__my_flags.hpp
 src/it/resources/result/my_flags/cwrapper/dh__my_flags.cpp
+src/it/resources/result/my_flags/wasm/my_flags.hpp
+src/it/resources/result/my_flags/wasm/my_flags.cpp
+src/it/resources/result/my_flags/ts/support-lib/DjinniModule.ts
+src/it/resources/result/my_flags/ts/support-lib/DjinniModule.js
+src/it/resources/result/my_flags/ts/module.ts
+
diff --git a/src/it/resources/expected/my_flags/ts/module.ts b/src/it/resources/expected/my_flags/ts/module.ts
new file mode 100644
index 00000000..88b35769
--- /dev/null
+++ b/src/it/resources/expected/my_flags/ts/module.ts
@@ -0,0 +1,16 @@
+// AUTOGENERATED FILE - DO NOT MODIFY!
+// This file was generated by Djinni from my_flags.djinni
+
+
+/** flag comment */
+export enum MyFlags {
+    NO_FLAGS = 0,
+    /** flag option comment */
+    FLAG1 = 1 << 0,
+    FLAG2 = 1 << 1,
+    FLAG3 = 1 << 2,
+    ALL_FLAGS = 0 | (1 << 0) | (1 << 1) | (1 << 2),
+}
+
+export interface Module_statics {
+}
diff --git a/src/it/resources/expected/my_flags/wasm/my_flags.cpp b/src/it/resources/expected/my_flags/wasm/my_flags.cpp
new file mode 100644
index 00000000..f7faf82e
--- /dev/null
+++ b/src/it/resources/expected/my_flags/wasm/my_flags.cpp
@@ -0,0 +1,33 @@
+// AUTOGENERATED FILE - DO NOT MODIFY!
+// This file was generated by Djinni from my_flags.djinni
+
+#include "my_flags.hpp"  // my header
+#include <mutex>
+
+namespace djinni_generated {
+
+namespace {
+    EM_JS(void, djinni_init__my_flags_consts, (), {
+        Module.MyFlags =  {
+            NO_FLAGS : 0,
+            /** flag option comment */
+            FLAG1 : 1 << 0,
+            FLAG2 : 1 << 1,
+            FLAG3 : 1 << 2,
+            ALL_FLAGS : 0 | (1 << 0) | (1 << 1) | (1 << 2),
+        }
+    })
+}
+
+void MyFlags::staticInitializeConstants() {
+    static std::once_flag initOnce;
+    std::call_once(initOnce, [] {
+        djinni_init__my_flags_consts();
+    });
+}
+
+EMSCRIPTEN_BINDINGS(_my_flags) {
+    MyFlags::staticInitializeConstants();
+}
+
+}  // namespace djinni_generated
diff --git a/src/it/resources/expected/my_flags/wasm/my_flags.hpp b/src/it/resources/expected/my_flags/wasm/my_flags.hpp
new file mode 100644
index 00000000..73ff8b78
--- /dev/null
+++ b/src/it/resources/expected/my_flags/wasm/my_flags.hpp
@@ -0,0 +1,15 @@
+// AUTOGENERATED FILE - DO NOT MODIFY!
+// This file was generated by Djinni from my_flags.djinni
+
+#pragma once
+
+#include "djinni_wasm.hpp"
+#include "my_flags.hpp"
+
+namespace djinni_generated {
+
+struct MyFlags: ::djinni::WasmEnum<::MyFlags> {
+    static void staticInitializeConstants();
+};
+
+}  // namespace djinni_generated
diff --git a/src/it/resources/expected/my_record/generated-files.txt b/src/it/resources/expected/my_record/generated-files.txt
index 026b9239..aae0971a 100644
--- a/src/it/resources/expected/my_record/generated-files.txt
+++ b/src/it/resources/expected/my_record/generated-files.txt
@@ -24,3 +24,9 @@ src/it/resources/result/my_record/cwrapper/dh__map_string_int32_t.cpp
 src/it/resources/result/my_record/cwrapper-headers/dh__my_record.h
 src/it/resources/result/my_record/cwrapper/dh__my_record.hpp
 src/it/resources/result/my_record/cwrapper/dh__my_record.cpp
+src/it/resources/result/my_record/wasm/my_record.hpp
+src/it/resources/result/my_record/wasm/my_record.cpp
+src/it/resources/result/my_record/ts/support-lib/DjinniModule.ts
+src/it/resources/result/my_record/ts/support-lib/DjinniModule.js
+src/it/resources/result/my_record/ts/module.ts
+
diff --git a/src/it/resources/expected/my_record/ts/module.ts b/src/it/resources/expected/my_record/ts/module.ts
new file mode 100644
index 00000000..82525bff
--- /dev/null
+++ b/src/it/resources/expected/my_record/ts/module.ts
@@ -0,0 +1,22 @@
+// AUTOGENERATED FILE - DO NOT MODIFY!
+// This file was generated by Djinni from my_record.djinni
+
+
+/** record comment */
+export interface /*record*/ MyRecord {
+    /** record property comment */
+    id: number;
+    info: string;
+    store: Set<string>;
+    hash: Map<string, number>;
+}
+export namespace MyRecord {
+    export const STRING_CONST = "Constants can be put here";
+}
+
+export interface ns_testsuite {
+}
+export interface Module_statics {
+
+    testsuite: ns_testsuite;
+}
diff --git a/src/it/resources/expected/my_record/wasm/my_record.cpp b/src/it/resources/expected/my_record/wasm/my_record.cpp
new file mode 100644
index 00000000..1d3d157d
--- /dev/null
+++ b/src/it/resources/expected/my_record/wasm/my_record.cpp
@@ -0,0 +1,43 @@
+// AUTOGENERATED FILE - DO NOT MODIFY!
+// This file was generated by Djinni from my_record.djinni
+
+#include "my_record.hpp"  // my header
+
+namespace djinni_generated {
+
+auto MyRecord::toCpp(const JsType& j) -> CppType {
+    return {::djinni::I32::Boxed::toCpp(j["id"]),
+            ::djinni::String::Boxed::toCpp(j["info"]),
+            ::djinni::Set<::djinni::String>::Boxed::toCpp(j["store"]),
+            ::djinni::Map<::djinni::String, ::djinni::I32>::Boxed::toCpp(j["hash"])};
+}
+auto MyRecord::fromCpp(const CppType& c) -> JsType {
+    em::val js = em::val::object();
+    js.set("id", ::djinni::I32::Boxed::fromCpp(c.id));
+    js.set("info", ::djinni::String::Boxed::fromCpp(c.info));
+    js.set("store", ::djinni::Set<::djinni::String>::Boxed::fromCpp(c.store));
+    js.set("hash", ::djinni::Map<::djinni::String, ::djinni::I32>::Boxed::fromCpp(c.hash));
+    return js;
+}
+
+namespace {
+    EM_JS(void, djinni_init_testsuite_my_record_consts, (), {
+        if (!('testsuite_MyRecord' in Module)) {
+            Module.testsuite_MyRecord = {};
+        }
+        Module.testsuite_MyRecord.STRING_CONST = "Constants can be put here";
+    })
+}
+void MyRecord::staticInitializeConstants() {
+    static std::once_flag initOnce;
+    std::call_once(initOnce, [] {
+        djinni_init_testsuite_my_record_consts();
+        ::djinni::djinni_register_name_in_ns("testsuite_MyRecord", "testsuite.MyRecord");
+    });
+}
+
+EMSCRIPTEN_BINDINGS(testsuite_my_record_consts) {
+    MyRecord::staticInitializeConstants();
+}
+
+}  // namespace djinni_generated
diff --git a/src/it/resources/expected/my_record/wasm/my_record.hpp b/src/it/resources/expected/my_record/wasm/my_record.hpp
new file mode 100644
index 00000000..d557bb72
--- /dev/null
+++ b/src/it/resources/expected/my_record/wasm/my_record.hpp
@@ -0,0 +1,22 @@
+// AUTOGENERATED FILE - DO NOT MODIFY!
+// This file was generated by Djinni from my_record.djinni
+
+#pragma once
+
+#include "djinni_wasm.hpp"
+#include "my_record.hpp"
+
+namespace djinni_generated {
+
+struct MyRecord
+{
+    using CppType = ::testsuite::MyRecord;
+    using JsType = em::val;
+    using Boxed = MyRecord;
+
+    static CppType toCpp(const JsType& j);
+    static JsType fromCpp(const CppType& c);
+    static void staticInitializeConstants();
+};
+
+}  // namespace djinni_generated
diff --git a/src/it/resources/expected/using_custom_datatypes/generated-files.txt b/src/it/resources/expected/using_custom_datatypes/generated-files.txt
index afc8d921..29bfea27 100644
--- a/src/it/resources/expected/using_custom_datatypes/generated-files.txt
+++ b/src/it/resources/expected/using_custom_datatypes/generated-files.txt
@@ -30,3 +30,10 @@ src/it/resources/result/using_custom_datatypes/cwrapper/dh__custom_datatype.cpp
 src/it/resources/result/using_custom_datatypes/cwrapper-headers/dh__other_record.h
 src/it/resources/result/using_custom_datatypes/cwrapper/dh__other_record.hpp
 src/it/resources/result/using_custom_datatypes/cwrapper/dh__other_record.cpp
+src/it/resources/result/using_custom_datatypes/wasm/custom_datatype.hpp
+src/it/resources/result/using_custom_datatypes/wasm/custom_datatype.cpp
+src/it/resources/result/using_custom_datatypes/wasm/other_record.hpp
+src/it/resources/result/using_custom_datatypes/wasm/other_record.cpp
+src/it/resources/result/using_custom_datatypes/ts/support-lib/DjinniModule.ts
+src/it/resources/result/using_custom_datatypes/ts/support-lib/DjinniModule.js
+src/it/resources/result/using_custom_datatypes/ts/module.ts
diff --git a/src/it/resources/expected/using_custom_datatypes/ts/module.ts b/src/it/resources/expected/using_custom_datatypes/ts/module.ts
new file mode 100644
index 00000000..e8a946f3
--- /dev/null
+++ b/src/it/resources/expected/using_custom_datatypes/ts/module.ts
@@ -0,0 +1,14 @@
+// AUTOGENERATED FILE - DO NOT MODIFY!
+// This file was generated by Djinni from using_custom_datatypes.djinni
+
+
+export interface /*record*/ CustomDatatype {
+    recordData: string;
+}
+
+export interface /*record*/ OtherRecord {
+    customDatatypeData: CustomDatatype;
+}
+
+export interface Module_statics {
+}
diff --git a/src/it/resources/expected/using_custom_datatypes/wasm/custom_datatype.cpp b/src/it/resources/expected/using_custom_datatypes/wasm/custom_datatype.cpp
new file mode 100644
index 00000000..43bdc04f
--- /dev/null
+++ b/src/it/resources/expected/using_custom_datatypes/wasm/custom_datatype.cpp
@@ -0,0 +1,17 @@
+// AUTOGENERATED FILE - DO NOT MODIFY!
+// This file was generated by Djinni from using_custom_datatypes.djinni
+
+#include "custom_datatype.hpp"  // my header
+
+namespace djinni_generated {
+
+auto CustomDatatype::toCpp(const JsType& j) -> CppType {
+    return {::djinni::String::Boxed::toCpp(j["recordData"])};
+}
+auto CustomDatatype::fromCpp(const CppType& c) -> JsType {
+    em::val js = em::val::object();
+    js.set("recordData", ::djinni::String::Boxed::fromCpp(c.recordData));
+    return js;
+}
+
+}  // namespace djinni_generated
diff --git a/src/it/resources/expected/using_custom_datatypes/wasm/custom_datatype.hpp b/src/it/resources/expected/using_custom_datatypes/wasm/custom_datatype.hpp
new file mode 100644
index 00000000..87fdcd06
--- /dev/null
+++ b/src/it/resources/expected/using_custom_datatypes/wasm/custom_datatype.hpp
@@ -0,0 +1,21 @@
+// AUTOGENERATED FILE - DO NOT MODIFY!
+// This file was generated by Djinni from using_custom_datatypes.djinni
+
+#pragma once
+
+#include "custom_datatype.hpp"
+#include "djinni_wasm.hpp"
+
+namespace djinni_generated {
+
+struct CustomDatatype
+{
+    using CppType = ::CustomDatatype;
+    using JsType = em::val;
+    using Boxed = CustomDatatype;
+
+    static CppType toCpp(const JsType& j);
+    static JsType fromCpp(const CppType& c);
+};
+
+}  // namespace djinni_generated
diff --git a/src/it/resources/expected/using_custom_datatypes/wasm/other_record.cpp b/src/it/resources/expected/using_custom_datatypes/wasm/other_record.cpp
new file mode 100644
index 00000000..7f2941f6
--- /dev/null
+++ b/src/it/resources/expected/using_custom_datatypes/wasm/other_record.cpp
@@ -0,0 +1,18 @@
+// AUTOGENERATED FILE - DO NOT MODIFY!
+// This file was generated by Djinni from using_custom_datatypes.djinni
+
+#include "other_record.hpp"  // my header
+#include "custom_datatype.hpp"
+
+namespace djinni_generated {
+
+auto OtherRecord::toCpp(const JsType& j) -> CppType {
+    return {::djinni_generated::CustomDatatype::Boxed::toCpp(j["customDatatypeData"])};
+}
+auto OtherRecord::fromCpp(const CppType& c) -> JsType {
+    em::val js = em::val::object();
+    js.set("customDatatypeData", ::djinni_generated::CustomDatatype::Boxed::fromCpp(c.customDatatypeData));
+    return js;
+}
+
+}  // namespace djinni_generated
diff --git a/src/it/resources/expected/using_custom_datatypes/wasm/other_record.hpp b/src/it/resources/expected/using_custom_datatypes/wasm/other_record.hpp
new file mode 100644
index 00000000..7332226f
--- /dev/null
+++ b/src/it/resources/expected/using_custom_datatypes/wasm/other_record.hpp
@@ -0,0 +1,21 @@
+// AUTOGENERATED FILE - DO NOT MODIFY!
+// This file was generated by Djinni from using_custom_datatypes.djinni
+
+#pragma once
+
+#include "djinni_wasm.hpp"
+#include "other_record.hpp"
+
+namespace djinni_generated {
+
+struct OtherRecord
+{
+    using CppType = ::OtherRecord;
+    using JsType = em::val;
+    using Boxed = OtherRecord;
+
+    static CppType toCpp(const JsType& j);
+    static JsType fromCpp(const CppType& c);
+};
+
+}  // namespace djinni_generated
diff --git a/src/it/scala/djinni/GeneratorIntegrationTest.scala b/src/it/scala/djinni/GeneratorIntegrationTest.scala
index 9e12584c..c88b6ff2 100644
--- a/src/it/scala/djinni/GeneratorIntegrationTest.scala
+++ b/src/it/scala/djinni/GeneratorIntegrationTest.scala
@@ -29,7 +29,9 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen {
         "pyCffiFilenames",
         "cWrapperFilenames",
         "cWrapperHeaderFilenames",
-        "cppcliFilenames"
+        "cppcliFilenames",
+        "wasmFilenames",
+        "tsFileNames"
       ),
       (
         "my_enum",
@@ -46,7 +48,9 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen {
         PyCffi(),
         CWrapper("dh__my_enum.cpp", "dh__my_enum.hpp"),
         CWrapperHeaders("dh__my_enum.h"),
-        CppCli("MyEnum.hpp", "MyEnum.cpp")
+        CppCli("MyEnum.hpp", "MyEnum.cpp"),
+        Wasm("my_enum.hpp", "my_enum.cpp"),
+        Ts("module.ts")
       ),
       (
         "my_flags",
@@ -63,7 +67,9 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen {
         PyCffi(),
         CWrapper("dh__my_flags.cpp", "dh__my_flags.hpp"),
         CWrapperHeaders("dh__my_flags.h"),
-        CppCli("MyFlags.hpp", "MyFlags.cpp")
+        CppCli("MyFlags.hpp", "MyFlags.cpp"),
+        Wasm("my_flags.hpp", "my_flags.cpp"),
+        Ts("module.ts")
       ),
       (
         "my_record",
@@ -96,7 +102,9 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen {
           "dh__my_record.h",
           "dh__set_string.h"
         ),
-        CppCli("MyRecord.hpp", "MyRecord.cpp")
+        CppCli("MyRecord.hpp", "MyRecord.cpp"),
+        Wasm("my_record.hpp", "my_record.cpp"),
+        Ts("module.ts")
       ),
       (
         "my_cpp_interface",
@@ -113,7 +121,9 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen {
         PyCffi("pycffi_lib_build.py"),
         CWrapper("cw__my_cpp_interface.cpp", "cw__my_cpp_interface.hpp"),
         CWrapperHeaders("cw__my_cpp_interface.h"),
-        CppCli("MyCppInterface.hpp", "MyCppInterface.cpp")
+        CppCli("MyCppInterface.hpp", "MyCppInterface.cpp"),
+        Wasm("my_cpp_interface.hpp", "my_cpp_interface.cpp"),
+        Ts("module.ts")
       ),
       (
         "my_client_interface",
@@ -130,7 +140,9 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen {
         PyCffi("pycffi_lib_build.py"),
         CWrapper("cw__my_client_interface.cpp", "cw__my_client_interface.hpp"),
         CWrapperHeaders("cw__my_client_interface.h"),
-        CppCli("MyClientInterface.hpp", "MyClientInterface.cpp")
+        CppCli("MyClientInterface.hpp", "MyClientInterface.cpp"),
+        Wasm("my_client_interface.hpp", "my_client_interface.cpp"),
+        Ts("module.ts")
       ),
       (
         "all_datatypes",
@@ -176,7 +188,14 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen {
           "AllDatatypes.cpp",
           "EnumData.cpp",
           "EnumData.hpp"
-        )
+        ),
+        Wasm(
+          "all_datatypes.hpp",
+          "all_datatypes.cpp",
+          "enum_data.hpp",
+          "enum_data.cpp"
+        ),
+        Ts("module.ts")
       ),
       (
         "using_custom_datatypes",
@@ -207,7 +226,9 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen {
           "dh__other_record.hpp"
         ),
         CWrapperHeaders("dh__custom_datatype.h", "dh__other_record.h"),
-        CppCli("CustomDatatype.hpp", "CustomDatatype.cpp")
+        CppCli("CustomDatatype.hpp", "CustomDatatype.cpp"),
+        Wasm("custom_datatype.hpp", "custom_datatype.cpp"),
+        Ts("module.ts")
       )
     )
     forAll(djinniTypes) {
@@ -226,7 +247,9 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen {
           pyCffiFilenames: PyCffi,
           cWrapperFilenames: CWrapper,
           cWrapperHeaderFilenames: CWrapperHeaders,
-          cppcliFilenames: CppCli
+          cppcliFilenames: CppCli,
+          wasmFilenames: Wasm,
+          tsFileNames: Ts
       ) =>
         it(s"should generate valid language bridges for `$idlFile`-types") {
           Given(s"`$idlFile.djinni`")
@@ -337,6 +360,7 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen {
         python = false,
         cWrapper = false,
         cppCLI = false,
+        wasm = false,
         useNNHeader = true
       )
       djinni(cmd)
@@ -370,6 +394,7 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen {
         python = false,
         cWrapper = false,
         cppCLI = true,
+        wasm = false,
         useNNHeader = false
       )
       djinni(cmd)
@@ -407,6 +432,7 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen {
         python = false,
         cWrapper = false,
         cppCLI = true,
+        wasm = false,
         useNNHeader = false
       )
       djinni(cmd)
@@ -439,7 +465,8 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen {
         python = false,
         cWrapper = false,
         cppCLI = false,
-        cppOmitDefaultRecordCtor = true
+        cppOmitDefaultRecordCtor = true,
+        wasm = false
       )
       djinni(cmd)
 
@@ -474,6 +501,7 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen {
         python = false,
         cWrapper = false,
         cppCLI = true,
+        wasm = false,
         useNNHeader = true
       )
       djinni(cmd)
@@ -556,6 +584,7 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen {
         python = false,
         cWrapper = false,
         cppCLI = false,
+        wasm = false,
         cppOmitDefaultRecordCtor = true
       )
       djinni(cmd)
@@ -587,6 +616,7 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen {
         python = false,
         cWrapper = false,
         cppCLI = true,
+        wasm = false,
         cppOmitDefaultRecordCtor = true
       )
 
@@ -618,6 +648,7 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen {
         python = false,
         cWrapper = false,
         cppCLI = false,
+        wasm = false,
         cppOmitDefaultRecordCtor = true
       )
 
@@ -787,6 +818,7 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen {
         python = false,
         cWrapper = false,
         cppCLI = false,
+        wasm = false,
         cppJsonSerialization = Some("nlohmann_json")
       )
       djinni(cmd)
@@ -861,6 +893,7 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen {
       python = false,
       cWrapper = false,
       cppCLI = false,
+      wasm = false,
       cppOmitDefaultRecordCtor = true
     )
 
@@ -891,6 +924,7 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen {
       python = false,
       cWrapper = false,
       cppCLI = false,
+      wasm = false,
       cppOmitDefaultRecordCtor = true
     )
 
@@ -925,6 +959,7 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen {
       python = false,
       cWrapper = false,
       cppCLI = false,
+      wasm = false,
       cppOmitDefaultRecordCtor = true
     )
 
@@ -935,4 +970,35 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen {
     )
     assertFileContentEquals(idlFile, OBJC_HEADERS, objcHeaderFilenames)
   }
+
+  it(
+    "should generate @Deprecated annotations for TS from @deprecated notes in comments"
+  ) {
+    Given(
+      "an IDL-file that documents deprecation using @deprecated in comments"
+    )
+    val idlFile = "deprecation"
+
+    When("generating Wasm/TS source")
+    val tsFilenames =
+      Ts("module.ts")
+    val cmd = djinniParams(
+      idlFile,
+      cpp = false,
+      objc = false,
+      java = false,
+      python = false,
+      cWrapper = false,
+      cppCLI = false,
+      wasm = true,
+      cppOmitDefaultRecordCtor = true
+    )
+
+    djinni(cmd)
+
+    Then(
+      "the @deprecated comments are generated as @Deprecated annotations"
+    )
+    assertFileContentEquals(idlFile, TS, tsFilenames)
+  }
 }
diff --git a/src/it/scala/djinni/IntegrationTest.scala b/src/it/scala/djinni/IntegrationTest.scala
index 8db8d13a..685a8d74 100644
--- a/src/it/scala/djinni/IntegrationTest.scala
+++ b/src/it/scala/djinni/IntegrationTest.scala
@@ -28,6 +28,8 @@ class IntegrationTest extends AnyFunSpec {
   final val CWRAPPER = "cwrapper"
   final val CWRAPPER_HEADERS = "cwrapper-headers"
   final val CPPCLI = "cppcli"
+  final val WASM = "wasm"
+  final val TS = "ts"
 
   type Cpp = List[String]
   def Cpp(params: String*) = List(params: _*)
@@ -57,6 +59,10 @@ class IntegrationTest extends AnyFunSpec {
   def CWrapperHeaders(params: String*) = List(params: _*)
   type CppCli = List[String]
   def CppCli(params: String*) = List(params: _*)
+  type Wasm = List[String]
+  def Wasm(params: String*) = List(params: _*)
+  type Ts = List[String]
+  def Ts(params: String*) = List(params: _*)
 
   /** Executes the djinni generator with the given parameters
     * @param parameters
@@ -108,6 +114,7 @@ class IntegrationTest extends AnyFunSpec {
       python: Boolean = true,
       cWrapper: Boolean = true,
       cppCLI: Boolean = true,
+      wasm: Boolean = true,
       useNNHeader: Boolean = false,
       cppOmitDefaultRecordCtor: Boolean = false,
       cppJsonSerialization: Option[String] = None
@@ -145,6 +152,12 @@ class IntegrationTest extends AnyFunSpec {
       cmd += s" --cppcli-out $baseOutputPath/$idl/$CPPCLI"
       cmd += s" --cppcli-include-cpp-prefix ../$CPP_HEADERS/"
     }
+    if (wasm) {
+      cmd += s" --wasm-out $baseOutputPath/$idl/$WASM"
+      cmd += s" --wasm-namespace testsuite"
+      cmd += s" --ts-out $baseOutputPath/$idl/$TS"
+      cmd += s" --ts-support-files-out $baseOutputPath/$idl/$TS/support-lib"
+    }
     if (useNNHeader) {
       cmd += " --cpp-nn-header nn.hpp"
       cmd += " --cpp-nn-type dropbox::oxygen::nn_shared_ptr"
diff --git a/src/main/resources/ts/DjinniModule.js b/src/main/resources/ts/DjinniModule.js
new file mode 100644
index 00000000..0580a9f6
--- /dev/null
+++ b/src/main/resources/ts/DjinniModule.js
@@ -0,0 +1,17 @@
+"use strict";
+/**
+  * Copyright 2021 Snap, Inc.
+  *
+  * Licensed under the Apache License, Version 2.0 (the "License");
+  * you may not use this file except in compliance with the License.
+  * You may obtain a copy of the License at
+  *
+  *    http://www.apache.org/licenses/LICENSE-2.0
+  *
+  * Unless required by applicable law or agreed to in writing, software
+  * distributed under the License is distributed on an "AS IS" BASIS,
+  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  * See the License for the specific language governing permissions and
+  * limitations under the License.
+  */
+exports.__esModule = true;
diff --git a/src/main/resources/ts/DjinniModule.ts b/src/main/resources/ts/DjinniModule.ts
new file mode 100644
index 00000000..d8b0f1d0
--- /dev/null
+++ b/src/main/resources/ts/DjinniModule.ts
@@ -0,0 +1,19 @@
+/**
+  * Copyright 2021 Snap, Inc.
+  *
+  * Licensed under the Apache License, Version 2.0 (the "License");
+  * you may not use this file except in compliance with the License.
+  * You may obtain a copy of the License at
+  *
+  *    http://www.apache.org/licenses/LICENSE-2.0
+  *
+  * Unless required by applicable law or agreed to in writing, software
+  * distributed under the License is distributed on an "AS IS" BASIS,
+  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  * See the License for the specific language governing permissions and
+  * limitations under the License.
+  */
+
+export interface DjinniModule {
+    allocateWasmBuffer(size: number): Uint8Array;
+}
diff --git a/src/main/scala/djinni/CppMarshal.scala b/src/main/scala/djinni/CppMarshal.scala
index 33c377b9..c21c48a7 100644
--- a/src/main/scala/djinni/CppMarshal.scala
+++ b/src/main/scala/djinni/CppMarshal.scala
@@ -271,15 +271,17 @@ class CppMarshal(spec: Spec) extends Marshal(spec) {
           }
         }
         case None =>
-          if (isOptionalInterface(tm)) {
-            // otherwise, interfaces are always plain old shared_ptr
-            expr(tm.args.head)
-          } else {
-            val args =
-              if (tm.args.isEmpty) ""
-              else tm.args.map(expr).mkString("<", ", ", ">")
-            base(tm.base) + args
+          val ty = if (isOptionalInterface(tm)) { tm.args.head }
+          else { tm }
+          val prefix = if (!isInterface(ty)) { "" }
+          else { /* isInterface */
+            if (isOptional(tm)) { "/*nullable*/ " }
+            else { "/*not-null*/ " }
           }
+          val args =
+            if (ty.args.isEmpty) ""
+            else ty.args.map(expr).mkString("<", ", ", ">")
+          prefix + base(ty.base) + args
       }
     }
     expr(tm)
diff --git a/src/main/scala/djinni/JavaMarshal.scala b/src/main/scala/djinni/JavaMarshal.scala
index 2c56b7e8..3d3b47c3 100644
--- a/src/main/scala/djinni/JavaMarshal.scala
+++ b/src/main/scala/djinni/JavaMarshal.scala
@@ -103,9 +103,9 @@ class JavaMarshal(spec: Spec) extends Marshal(spec) {
   }
 
   def isEnumFlags(m: Meta): Boolean = m match {
-    case MDef(_, _, _, Enum(_, true))                      => true
-    case MExtern(_, _, _, Enum(_, true), _, _, _, _, _, _) => true
-    case _                                                 => false
+    case MDef(_, _, _, Enum(_, true))                            => true
+    case MExtern(_, _, _, Enum(_, true), _, _, _, _, _, _, _, _) => true
+    case _                                                       => false
   }
   def isEnumFlags(tm: MExpr): Boolean = tm.base match {
     case MOptional => isEnumFlags(tm.args.head)
diff --git a/src/main/scala/djinni/Main.scala b/src/main/scala/djinni/Main.scala
index a667588c..e3e3b6c3 100644
--- a/src/main/scala/djinni/Main.scala
+++ b/src/main/scala/djinni/Main.scala
@@ -108,6 +108,17 @@ object Main {
     var pycffiOutFolder: Option[File] = None
     var pyImportPrefix: String = ""
     var cppJsonSerialization: Option[String] = None
+    var wasmOutFolder: Option[File] = None
+    var wasmIncludePrefix: String = ""
+    var wasmIncludeCppPrefix: String = ""
+    var wasmBaseLibIncludePrefix: String = ""
+    var wasmOmitConstants: Boolean = false
+    var wasmNamespace: Option[String] = None
+    var wasmOmitNsAlias: Boolean = false
+    var jsIdentStyle = IdentStyle.jsDefault
+    var tsOutFolder: Option[File] = None
+    var tsModule: String = "module"
+    var tsSupportFilesOutFolder: Option[File] = None
 
     val argParser: OptionParser[Unit] = new scopt.OptionParser[Unit]("djinni") {
 
@@ -543,6 +554,64 @@ object Main {
         .text(
           "Way of specifying if file generation should be skipped (default: false)"
         )
+      note("wasm\n")
+      opt[File]("wasm-out")
+        .valueName("<out-folder>")
+        .foreach(x => wasmOutFolder = Some(x))
+        .text(
+          "The output for the WASM bridge C++ files (Generator disabled if unspecified)."
+        )
+      opt[String]("wasm-include-prefix")
+        .valueName("<prefix>")
+        .foreach(wasmIncludePrefix = _)
+        .text(
+          "The prefix for #includes of WASM header files from WASM C++ files."
+        )
+      opt[String]("wasm-include-cpp-prefix")
+        .valueName("<prefix>")
+        .foreach(wasmIncludeCppPrefix = _)
+        .text(
+          "The prefix for #includes of the main header files from WASM C++ files."
+        )
+      opt[String]("wasm-base-lib-include-prefix")
+        .valueName("...")
+        .foreach(x => wasmBaseLibIncludePrefix = x)
+        .text(
+          "The WASM base library's include path, relative to the WASM C++ classes."
+        )
+      opt[Boolean]("wasm-omit-constants")
+        .valueName("<true/false>")
+        .foreach(x => wasmOmitConstants = x)
+        .text(
+          "Omit the generation of consts and enums in wasm, making them only accessible through TypeScript."
+        )
+      opt[String]("wasm-namespace")
+        .valueName("...")
+        .foreach(x => wasmNamespace = Some(x))
+        .text("The namespace to use for generated Wasm classes.")
+      opt[Boolean]("wasm-omit-namespace-alias")
+        .valueName("<true/false>")
+        .foreach(x => wasmOmitNsAlias = x)
+        .text(
+          "Omit the generation of namespace aliases for classes. Namespaces will be prepended to class names instead."
+        )
+      opt[File]("ts-out")
+        .valueName("<out-folder>")
+        .foreach(x => tsOutFolder = Some(x))
+        .text(
+          "The output for the TypeScript interface files (Generator disabled if unspecified)."
+        )
+      opt[String]("ts-module")
+        .valueName("<name>")
+        .foreach(tsModule = _)
+        .text("TypeScript declaration module name (default: \"module\").")
+
+      opt[File]("ts-support-files-out")
+        .valueName("<out-folder>")
+        .foreach(x => tsSupportFilesOutFolder = Some(x))
+        .text(
+          "Folder in which to generate DjinniModule.[ts/js] files. (Not generated if not specified)"
+        )
 
       note(
         "\n\nIdentifier styles (ex: \"FooBar\", \"fooBar\", \"foo_bar\", \"FOO_BAR\", \"m_fooBar\")"
@@ -733,6 +802,43 @@ object Main {
         "FooBar",
         c => { cppCliIdentStyle = cppCliIdentStyle.copy(file = c) }
       )
+
+      note("\nTypescript/Javascript options:")
+      identStyle(
+        "ident-js-type",
+        "FooBar",
+        c => { jsIdentStyle = jsIdentStyle.copy(ty = c) }
+      )
+      identStyle(
+        "ident-js-type-param",
+        "FooBar",
+        c => { jsIdentStyle = jsIdentStyle.copy(typeParam = c) }
+      )
+      identStyle(
+        "ident-js-method",
+        "fooBar",
+        c => { jsIdentStyle = jsIdentStyle.copy(method = c) }
+      )
+      identStyle(
+        "ident-js-local",
+        "fooBar",
+        c => { jsIdentStyle = jsIdentStyle.copy(local = c) }
+      )
+      identStyle(
+        "ident-js-enum",
+        "FOO_BAR",
+        c => { jsIdentStyle = jsIdentStyle.copy(enum = c) }
+      )
+      identStyle(
+        "ident-js-field",
+        "fooBar",
+        c => { jsIdentStyle = jsIdentStyle.copy(field = c) }
+      )
+      identStyle(
+        "ident-js-const",
+        "FOO_BAR",
+        c => { jsIdentStyle = jsIdentStyle.copy(const = c) }
+      )
     }
 
     if (argParser.parse(args, ()).isEmpty) {
@@ -829,7 +935,9 @@ object Main {
       objcppOutRequired = objcppOutFolder.isDefined,
       javaOutRequired = javaOutFolder.isDefined,
       jniOutRequired = jniOutFolder.isDefined,
-      cppCliOutRequired = cppCliOutFolder.isDefined
+      cppCliOutRequired = cppCliOutFolder.isDefined,
+      wasmOutRequired = wasmOutFolder.isDefined,
+      tsOutRequired = wasmOutFolder.isDefined
     ) match {
       case Some(err) =>
         System.err.println(err)
@@ -938,7 +1046,19 @@ object Main {
       cWrapperIncludePrefix,
       cWrapperIncludeCppPrefix,
       pyImportPrefix,
-      cppJsonSerialization
+      cppJsonSerialization,
+      wasmOutFolder,
+      wasmIncludePrefix,
+      wasmIncludeCppPrefix,
+      wasmBaseLibIncludePrefix,
+      wasmOmitConstants,
+      wasmNamespace,
+      wasmOmitNsAlias,
+      jsIdentStyle,
+      tsOutFolder,
+      tsModule,
+      tsSupportFilesOutFolder,
+      idlFile.getName.stripSuffix(".djinni")
     )
 
     try {
diff --git a/src/main/scala/djinni/TsGenerator.scala b/src/main/scala/djinni/TsGenerator.scala
new file mode 100644
index 00000000..11a2f8a7
--- /dev/null
+++ b/src/main/scala/djinni/TsGenerator.scala
@@ -0,0 +1,393 @@
+/** Copyright 2021 Snap, Inc.
+  *
+  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+  * use this file except in compliance with the License. You may obtain a copy
+  * of the License at
+  *
+  * http://www.apache.org/licenses/LICENSE-2.0
+  *
+  * Unless required by applicable law or agreed to in writing, software
+  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+  * License for the specific language governing permissions and limitations
+  * under the License.
+  */
+
+package djinni
+
+import java.io._
+import djinni.ast.Record.DerivingType
+import djinni.ast._
+import djinni.generatorTools._
+import djinni.meta._
+import djinni.writer.IndentWriter
+import scala.collection.mutable.ListBuffer
+import scala.collection.mutable.TreeSet
+import java.util.regex.Pattern
+import java.util.regex.Matcher
+
+import scala.io.Source
+
+class TsGenerator(spec: Spec) extends Generator(spec) {
+  private def tsRetType(m: Interface.Method): String = {
+    return if (m.ret.isEmpty) "void" else toTsType(m.ret.get.resolved)
+  }
+
+  private def tsPrimitiveType(p: MPrimitive): String = p._idlName match {
+    case "bool" => "boolean"
+    case "i64"  => "bigint"
+    case _      => "number"
+  }
+
+  // return the base type if tm is optional otherwise None
+  private def optionalBase(tm: MExpr): Option[MExpr] = {
+    tm.base match {
+      case MOptional => Some(tm.args.head)
+      case _         => None
+    }
+  }
+
+  private def removeOptional(tm: MExpr): MExpr = {
+    tm.base match {
+      case MOptional => tm.args.head
+      case _         => tm
+    }
+  }
+
+  private def nullityAnnotation(tm: MExpr) = tm.base match {
+    case MOptional => " | undefined"
+    case _         => ""
+  }
+
+  def toTsType(tm: MExpr, addNullability: Boolean = true): String = {
+    def args(tm: MExpr) = if (tm.args.isEmpty) ""
+    else
+      tm.args.map(arg => toTsType(arg, addNullability)).mkString("<", ", ", ">")
+    def f(tm: MExpr): String = {
+      tm.base match {
+        case MOptional =>
+          assert(tm.args.size == 1)
+          val arg = tm.args.head
+          arg.base match {
+            case MOptional => throw new AssertionError("nested optional?")
+            case m         => f(arg)
+          }
+        case e: MExtern => e.ts.typename + (if (e.ts.generic) args(tm) else "")
+        case o =>
+          val base = o match {
+            case p: MPrimitive => tsPrimitiveType(p)
+            case MString       => "string"
+            case MDate         => "Date"
+            case MBinary       => "Uint8Array"
+            case MOptional =>
+              throw new AssertionError(
+                "optional should have been special cased"
+              )
+            case MList      => "Array"
+            case MSet       => "Set"
+            case MMap       => "Map"
+            case d: MDef    => idJs.ty(d.name)
+            case e: MExtern => throw new AssertionError("unreachable")
+            case p: MParam  => idJs.typeParam(p.name)
+          }
+          base + args(tm)
+      }
+    }
+    f(tm) + (if (addNullability) nullityAnnotation(tm) else "")
+  }
+
+  case class TsSymbolRef(sym: String, module: String)
+  def references(m: Meta): Seq[TsSymbolRef] = m match {
+    case e: MExtern => List(TsSymbolRef(idJs.ty(e.name), e.ts.module))
+    case _          => List()
+  }
+  class TsRefs() {
+    var imports = scala.collection.mutable.Map[String, TreeSet[String]]()
+
+    def find(ty: TypeRef) { find(ty.resolved) }
+    def find(tm: MExpr) {
+      tm.args.foreach(find)
+      find(tm.base)
+    }
+    def find(m: Meta) = for (r <- references(m)) r match {
+      case TsSymbolRef(sym, module) => {
+        var syms = imports.getOrElseUpdate(module, TreeSet[String]())
+        syms += (sym)
+      }
+      case _ =>
+    }
+  }
+
+  private def generateTsConstants(
+      w: IndentWriter,
+      ident: Ident,
+      consts: Seq[Const]
+  ) = {
+    def writeJsConst(w: IndentWriter, ty: TypeRef, v: Any): Unit = v match {
+      case l: Long if (toTsType(removeOptional(ty.resolved)) == "bigint") =>
+        w.w(s"""BigInt("${l.toString}")""")
+      case l: Long      => w.w(l.toString)
+      case d: Double    => w.w(d.toString)
+      case b: Boolean   => w.w(if (b) "true" else "false")
+      case s: String    => w.w(s)
+      case e: EnumValue => w.w(s"${idJs.ty(ty.expr.ident)}.${idJs.enum(e)}")
+      case v: ConstRef  => w.w(s"${idJs.const(v)}")
+      case z: Any => { // Value is record
+        val recordMdef = ty.resolved.base.asInstanceOf[MDef]
+        val record = recordMdef.body.asInstanceOf[Record]
+        val vMap = z.asInstanceOf[Map[String, Any]]
+        w.w("").braced {
+          // Use exact sequence
+          val skipFirst = SkipFirst()
+          for (f <- record.fields) {
+            skipFirst { w.wl(",") }
+            w.w(s"${idJs.field(f.ident)}: ")
+            writeJsConst(w, f.ty, vMap.apply(f.ident.name))
+          }
+          w.wl
+        }
+      }
+    }
+    w.w(s"export namespace ${idJs.ty(ident)}").braced {
+      for (c <- consts) {
+        writeDoc(w, c.doc)
+        w.w(s"export const ${idJs.const(c.ident)} = ")
+        writeJsConst(w, c.ty, c.value)
+        w.wl(";")
+      }
+    }
+  }
+
+  // ------------------------------------------------------------------------
+  private def generateEnum(
+      origin: String,
+      ident: Ident,
+      doc: Doc,
+      e: Enum,
+      w: IndentWriter
+  ) {
+    w.wl
+    writeDoc(w, doc)
+    w.w(s"export enum ${idJs.ty(ident)}").braced {
+      writeEnumOptionNone(w, e, idJs.enum)
+      writeEnumOptions(
+        w,
+        e,
+        idJs.enum,
+        (o: Enum.Option, shift: Int) => s" = 1 << $shift,"
+      )
+      writeEnumOptionAll(w, e, idJs.enum)
+    }
+  }
+  private def generateRecord(
+      origin: String,
+      ident: Ident,
+      doc: Doc,
+      params: Seq[TypeParam],
+      r: Record,
+      w: IndentWriter
+  ) {
+    w.wl
+    writeDoc(w, doc)
+    w.w(s"export interface /*record*/ ${idJs.ty(ident)}").braced {
+      for (f <- r.fields) {
+        writeDoc(w, f.doc)
+        optionalBase(f.ty.resolved) match {
+          case Some(t) => w.wl(s"${idJs.field(f.ident)}?: ${toTsType(t)};")
+          case _ => w.wl(s"${idJs.field(f.ident)}: ${toTsType(f.ty.resolved)};")
+        }
+      }
+    }
+    if (!r.consts.isEmpty) {
+      generateTsConstants(w, ident, r.consts);
+    }
+  }
+  private def generateInterface(
+      origin: String,
+      ident: Ident,
+      doc: Doc,
+      typeParams: Seq[TypeParam],
+      i: Interface,
+      w: IndentWriter
+  ) {
+    w.wl
+    writeDoc(w, doc)
+    w.w(s"export interface ${idJs.ty(ident)}").braced {
+      for (m <- i.methods.filter(!_.static)) {
+        writeMethodDoc(w, m, idJs.local)
+        w.w(s"${idJs.method(m.ident)}(")
+        w.w(
+          m.params
+            .map(p => s"${idJs.local(p.ident)}: ${toTsType(p.ty.resolved)}")
+            .mkString(", ")
+        )
+        w.wl(s"): ${tsRetType(m)};")
+      }
+    }
+    if (!i.consts.isEmpty) {
+      generateTsConstants(w, ident, i.consts);
+    }
+    val staticMethods = i.methods.filter(m => m.static && m.lang.js)
+    if (!staticMethods.isEmpty) {
+      w.w(s"export interface ${idJs.ty(ident)}_statics").braced {
+        for (m <- staticMethods) {
+          writeMethodDoc(w, m, idJs.local)
+          w.w(s"${idJs.method(m.ident)}(")
+          w.w(
+            m.params
+              .map(p => s"${idJs.method(p.ident)}: ${toTsType(p.ty.resolved)}")
+              .mkString(", ")
+          )
+          w.wl(s"): ${tsRetType(m)};")
+        }
+      }
+    }
+  }
+  private def withWasmNamespace(name: String, sep: String = "_") =
+    spec.wasmNamespace match {
+      case Some(p) =>
+        p.replaceAll(
+          Pattern.quote("."),
+          Matcher.quoteReplacement(sep)
+        ) + sep + name
+      case None => name
+    }
+  // --------------------------------------------------------------------------
+  override def generate(idl: Seq[TypeDecl]) {
+    if (!spec.tsSupportFilesOutFolder.isEmpty) {
+      writeDjinniModuleFilesFile()
+    }
+    createFile(
+      spec.tsOutFolder.get,
+      spec.tsModule + ".ts",
+      (w: IndentWriter) => {
+        w.wl("// AUTOGENERATED FILE - DO NOT MODIFY!")
+        w.wl(
+          "// This file was generated by Djinni from " + spec.moduleName + ".djinni"
+        )
+        w.wl
+        val decls = idl.collect { case itd: InternTypeDecl => itd }
+
+        // find external references
+        val refs = new TsRefs()
+        for (td <- decls) td.body match {
+          case r: Record => {
+            r.fields.foreach(f => refs.find(f.ty))
+            r.consts.foreach(c => refs.find(c.ty))
+          }
+          case i: Interface => {
+            i.methods.foreach(m => {
+              m.params.foreach(p => refs.find(p.ty))
+              m.ret.foreach(refs.find)
+            })
+            i.consts.foreach(c => refs.find(c.ty))
+          }
+          case _ =>
+        }
+        // write external references
+        for ((module, syms) <- refs.imports) {
+          if (module != "") {
+            w.wl(s"""import { ${syms.mkString(", ")} } from "$module"""")
+          }
+        }
+
+        var interfacesWithStatics = new ListBuffer[String]()
+        for (td <- decls) td.body match {
+          case e: Enum => generateEnum(td.origin, td.ident, td.doc, e, w)
+          case r: Record =>
+            generateRecord(td.origin, td.ident, td.doc, td.params, r, w)
+          case i: Interface => {
+            generateInterface(td.origin, td.ident, td.doc, td.params, i, w)
+            if (i.methods.exists(m => m.static && m.lang.js)) {
+              interfacesWithStatics += idJs.ty(td.ident.name)
+            }
+          }
+          case _ =>
+        }
+        // add static factories
+        w.wl
+        if (!spec.wasmOmitNsAlias && !spec.wasmNamespace.isEmpty) {
+          val nsParts = spec.wasmNamespace.get.split("\\.")
+
+          for (i <- 0 until nsParts.length - 1) {
+            w.w(s"export interface ns_${nsParts(i)}").braced {
+              w.wl(s"${nsParts(i + 1)}: ns_${nsParts(i + 1)}")
+            }
+          }
+          w.w(s"export interface ns_${nsParts.last}").braced {
+            for (i <- interfacesWithStatics.toList) {
+              w.wl(i + ": " + i + "_statics;")
+            }
+          }
+          w.w(s"export interface ${idJs.ty(spec.tsModule)}_statics").braced {
+            for (i <- interfacesWithStatics.toList) {
+              w.wl(withWasmNamespace(i) + ": " + i + "_statics;")
+            }
+            w.wl
+            w.wl(s"${nsParts.head}: ns_${nsParts.head};")
+          }
+        } else {
+          w.w(s"export interface ${idJs.ty(spec.tsModule)}_statics").braced {
+            for (i <- interfacesWithStatics.toList) {
+              w.wl(withWasmNamespace(i) + ": " + i + "_statics;")
+            }
+          }
+        }
+      }
+    )
+  }
+  override def generateEnum(origin: String, ident: Ident, doc: Doc, e: Enum) {}
+  override def generateRecord(
+      origin: String,
+      ident: Ident,
+      doc: Doc,
+      params: Seq[TypeParam],
+      r: Record
+  ) {}
+  override def generateInterface(
+      origin: String,
+      ident: Ident,
+      doc: Doc,
+      typeParams: Seq[TypeParam],
+      i: Interface
+  ) {}
+
+  def writeDjinniModuleFile(f: IndentWriter => Unit, ext: String): Unit = {
+    if (!spec.skipGeneration) {
+      createFolder("TS Support lib", spec.tsSupportFilesOutFolder.get)
+      createFileOnce(
+        spec.tsSupportFilesOutFolder.get,
+        s"DjinniModule.$ext",
+        (w: IndentWriter) => {
+          w.wl("// AUTOGENERATED FILE - DO NOT MODIFY!")
+          w.wl("// This file was generated by Djinni")
+          w.wl
+          f(w)
+        }
+      )
+    }
+  }
+
+  def writeDjinniModuleFilesFile(): Unit = {
+    val tsContent = Source
+      .fromResource("ts/DjinniModule.ts")
+      .getLines
+      .mkString("\n")
+    writeDjinniModuleFile(
+      w => {
+        w.wl(tsContent)
+      },
+      "ts"
+    )
+    val jsContent = Source
+      .fromResource("ts/DjinniModule.js")
+      .getLines
+      .mkString("\n")
+    writeDjinniModuleFile(
+      w => {
+        w.wl(jsContent)
+      },
+      "js"
+    )
+  }
+}
diff --git a/src/main/scala/djinni/WasmGenerator.scala b/src/main/scala/djinni/WasmGenerator.scala
new file mode 100644
index 00000000..0e5591f9
--- /dev/null
+++ b/src/main/scala/djinni/WasmGenerator.scala
@@ -0,0 +1,697 @@
+/** Copyright 2021 Snap, Inc.
+  *
+  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+  * use this file except in compliance with the License. You may obtain a copy
+  * of the License at
+  *
+  * http://www.apache.org/licenses/LICENSE-2.0
+  *
+  * Unless required by applicable law or agreed to in writing, software
+  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+  * License for the specific language governing permissions and limitations
+  * under the License.
+  */
+
+package djinni
+
+import djinni.ast.Record.DerivingType
+import djinni.ast._
+import djinni.generatorTools._
+import djinni.meta._
+import djinni.writer.IndentWriter
+
+import scala.collection.mutable
+import java.util.regex.Pattern
+import java.util.regex.Matcher
+
+class WasmGenerator(spec: Spec) extends Generator(spec) {
+
+  val cppMarshal = new CppMarshal(spec)
+
+  private def wasmFilenameStyle(name: String): String = {
+    return spec.jniFileIdentStyle(name)
+  }
+  private def helperNamespace(): String = {
+    return spec.jniNamespace;
+  }
+
+  private def helperClass(name: String) = spec.jniClassIdentStyle(name)
+  private def helperClass(tm: MExpr): String =
+    helperName(tm) + helperTemplates(tm)
+  def helperName(tm: MExpr): String = tm.base match {
+    case d: MDef    => withNs(Some(helperNamespace()), helperClass(d.name))
+    case e: MExtern => e.wasm.translator
+    case o =>
+      withNs(
+        Some("djinni"),
+        o match {
+          case p: MPrimitive =>
+            p.idlName match {
+              case "i8"   => "I8"
+              case "i16"  => "I16"
+              case "i32"  => "I32"
+              case "i64"  => "I64"
+              case "f32"  => "F32"
+              case "f64"  => "F64"
+              case "bool" => "Bool"
+            }
+          case MOptional  => "Optional"
+          case MBinary    => "Binary"
+          case MString    => if (spec.cppUseWideStrings) "WString" else "String"
+          case MDate      => "Date"
+          case MList      => "List"
+          case MSet       => "Set"
+          case MMap       => "Map"
+          case d: MDef    => throw new AssertionError("unreachable")
+          case e: MExtern => throw new AssertionError("unreachable")
+          case p: MParam  => throw new AssertionError("not applicable")
+        }
+      )
+  }
+  private def helperTemplates(tm: MExpr): String = {
+    def f() = if (tm.args.isEmpty) ""
+    else tm.args.map(helperClass).mkString("<", ", ", ">")
+    tm.base match {
+      case MOptional =>
+        assert(tm.args.size == 1)
+        val argHelperClass = helperClass(tm.args.head)
+        s"<${spec.cppOptionalTemplate}, $argHelperClass>"
+      case MList | MSet =>
+        assert(tm.args.size == 1)
+        f
+      case MMap =>
+        assert(tm.args.size == 2)
+        f
+      case _ => f
+    }
+  }
+
+  def wasmType(tm: MExpr): String = tm.base match {
+    case p: MPrimitive => p.cName
+    case MString =>
+      if (spec.cppUseWideStrings) "std::wstring" else "std::string"
+    case d: MDef =>
+      d.defType match {
+        case DEnum => "int32_t"
+        case _     => "em::val"
+      }
+    case e: MExtern => e.wasm.typename
+    case _          => "em::val"
+  }
+  def wasmType(t: TypeRef): String = wasmType(t.resolved)
+
+  private def stubRetType(m: Interface.Method): String = {
+    return if (m.ret.isEmpty) "void" else wasmType(m.ret.get)
+  }
+  private def stubParamType(t: TypeRef): String = t.resolved.base match {
+    case p: MPrimitive => p.cName
+    case MString =>
+      if (spec.cppUseWideStrings) "const std::wstring&"
+      else "const std::string&"
+    case d: MDef =>
+      d.defType match {
+        case DEnum => "int32_t"
+        case _     => "const em::val&"
+      }
+    case e: MExtern =>
+      e.defType match {
+        case DEnum => e.wasm.typename
+        case _     => "const " + e.wasm.typename + "&"
+      }
+    case _ => "const em::val&"
+  }
+
+  private def stubParamName(name: String): String = s"w_${idCpp.local(name)}"
+
+  def jsClassNameAsCppType(jsClass: String): String = {
+    val classNameChars = jsClass.toList.map(c => s"'$c'")
+    s"""::djinni::JsClassName<${classNameChars.mkString(",")}>"""
+  }
+
+  def include(ident: String) = q(
+    spec.wasmIncludePrefix + wasmFilenameStyle(ident) + "." + spec.cppHeaderExt
+  )
+
+  def references(m: Meta, exclude: String = ""): Seq[SymbolReference] =
+    m match {
+      case d: MDef    => List(ImportRef(include(d.name)))
+      case e: MExtern => List(ImportRef(resolveExtWasmHdr(e.wasm.header)))
+      case _          => List()
+    }
+
+  def resolveExtWasmHdr(path: String) = {
+    path.replaceAll("\\$", spec.wasmBaseLibIncludePrefix);
+  }
+
+  class WasmRefs(name: String, cppPrefixOverride: Option[String] = None) {
+    var hpp = mutable.TreeSet[String]()
+    var cpp = mutable.TreeSet[String]()
+
+    val cppPrefix = cppPrefixOverride.getOrElse(spec.wasmIncludeCppPrefix)
+    hpp.add(
+      "#include " + q(
+        cppPrefix + spec.cppFileIdentStyle(name) + "." + spec.cppHeaderExt
+      )
+    )
+    hpp.add("#include " + q(spec.wasmBaseLibIncludePrefix + "djinni_wasm.hpp"))
+    spec.cppNnHeader match {
+      case Some(nnHdr) => hpp.add("#include " + nnHdr)
+      case _           =>
+    }
+
+    def find(ty: TypeRef) { find(ty.resolved) }
+    def find(tm: MExpr) {
+      tm.args.foreach(find)
+      find(tm.base)
+    }
+    def find(m: Meta) = for (r <- references(m, name)) r match {
+      case ImportRef(arg) => cpp.add("#include " + arg)
+      case _              =>
+    }
+  }
+
+  private def generateWasmConstants(
+      w: IndentWriter,
+      ident: Ident,
+      consts: Seq[Const]
+  ) {
+    val helper = helperClass(ident)
+    var dependentTypes = mutable.TreeSet[String]()
+    def writeJsConst(w: IndentWriter, ty: TypeRef, v: Any): Unit = v match {
+      case l: Long if wasmType(ty).equalsIgnoreCase("int64_t") =>
+        w.w(s"""BigInt("${l.toString}")""")
+      case l: Long    => w.w(l.toString)
+      case d: Double  => w.w(d.toString)
+      case b: Boolean => w.w(if (b) "true" else "false")
+      case s: String  => w.w(s)
+      case e: EnumValue => {
+        w.w(
+          s"Module.${withWasmNamespace(idJs.ty(ty.expr.ident))}.${idJs.enum(e)}"
+        )
+        dependentTypes.add(helperClass(ty.expr.ident))
+      }
+      case v: ConstRef => {
+        w.w(s"Module.${withWasmNamespace(idJs.ty(ident))}.${idJs.const(v)}")
+      }
+      case z: Map[_, _] => { // Value is record
+        val recordMdef = ty.resolved.base.asInstanceOf[MDef]
+        val record = recordMdef.body.asInstanceOf[Record]
+        val vMap = z.asInstanceOf[Map[String, Any]]
+        w.w("").braced {
+          // Use exact sequence
+          val skipFirst = SkipFirst()
+          for (f <- record.fields) {
+            skipFirst { w.wl(",") }
+            w.w(s"${idJs.field(f.ident)}: ")
+            writeJsConst(w, f.ty, vMap.apply(f.ident.name))
+          }
+          w.wl
+        }
+      }
+    }
+    w.wl
+    val fullyQualifiedName = withWasmNamespace(idJs.ty(ident))
+    w.w(s"namespace").braced {
+      w.wl(
+        s"EM_JS(void, djinni_init_${withCppNamespace(ident.name)}_consts, (), {"
+      ).nested {
+        w.w(s"if (!('${fullyQualifiedName}' in Module))").braced {
+          w.wl(s"Module.${fullyQualifiedName} = {};")
+        }
+        for (c <- consts) {
+          w.w(s"Module.${fullyQualifiedName}.${idJs.const(c.ident)} = ")
+          writeJsConst(w, c.ty, c.value)
+          w.wl(";")
+        }
+      }
+      w.wl("})")
+    }
+    w.w(s"void $helper::staticInitializeConstants()").braced {
+      w.wl("static std::once_flag initOnce;")
+      w.wl(s"std::call_once(initOnce, [] {")
+      w.wl(s"    djinni_init_${withCppNamespace(ident.name)}_consts();")
+      if (!spec.wasmOmitNsAlias && !spec.wasmNamespace.isEmpty) {
+        w.wl(
+          s"""    ::djinni::djinni_register_name_in_ns("${fullyQualifiedName}", "${spec.wasmNamespace.get}.${idJs
+              .ty(ident)}");"""
+        )
+      }
+      w.wl(s"});")
+    }
+    w.wl
+    w.w(s"EMSCRIPTEN_BINDINGS(${withCppNamespace(ident.name)}_consts)").braced {
+      for (d <- dependentTypes) {
+        if (d != helper)
+          w.wl(s"$d::staticInitializeConstants();");
+      }
+      w.wl(s"$helper::staticInitializeConstants();");
+    }
+  }
+
+  // ------------------------------------------------------------------------------
+
+  override def generateEnum(origin: String, ident: Ident, doc: Doc, e: Enum) {
+    val refs = new WasmRefs(ident.name)
+    refs.cpp.add("#include <mutex>")
+    val cls = cppMarshal.fqTypename(ident, e)
+    val helper = helperClass(ident)
+    val fullyQualifiedName = withWasmNamespace(idJs.ty(ident))
+    writeHppFileGeneric(
+      spec.wasmOutFolder.get,
+      helperNamespace(),
+      wasmFilenameStyle
+    )(
+      ident.name,
+      origin,
+      refs.hpp,
+      Nil,
+      (w => {
+        w.w(s"struct $helper: ::djinni::WasmEnum<$cls>").bracedSemi {
+          if (!spec.wasmOmitConstants) {
+            w.wl("static void staticInitializeConstants();");
+          }
+        }
+      }),
+      (w => {})
+    )
+    if (!spec.wasmOmitConstants) {
+      writeCppFileGeneric(
+        spec.wasmOutFolder.get,
+        helperNamespace(),
+        wasmFilenameStyle,
+        spec.wasmIncludePrefix
+      )(
+        ident.name,
+        origin,
+        refs.cpp,
+        (w => {
+          w.w(s"namespace").braced {
+            w.wl(
+              s"EM_JS(void, djinni_init_${withCppNamespace(ident.name)}_consts, (), {"
+            ).nested {
+              w.w(s"Module.${fullyQualifiedName} = ").braced {
+                writeEnumOptionNone(w, e, idJs.enum, () => ": 0,")
+                writeEnumOptions(
+                  w,
+                  e,
+                  idJs.enum,
+                  (o: Enum.Option, shift: Int) => s": 1 << $shift,",
+                  (ordinal: Int) => s": $ordinal,"
+                )
+                writeEnumOptionAll(
+                  w,
+                  e,
+                  idJs.enum,
+                  (ordinalsAndNames: Seq[Tuple2[Int, String]]) =>
+                    s""": ${ordinalsAndNames
+                        .map(e => e._1)
+                        .fold("0")((acc, o) => acc + s" | (1 << $o)")},"""
+                )
+              }
+            }
+            w.wl("})")
+          }
+          w.wl
+          w.w(s"void $helper::staticInitializeConstants()").braced {
+            w.wl("static std::once_flag initOnce;")
+            w.wl(s"std::call_once(initOnce, [] {")
+            w.wl(s"    djinni_init_${withCppNamespace(ident.name)}_consts();")
+            if (!spec.wasmOmitNsAlias && !spec.wasmNamespace.isEmpty) {
+              w.wl(
+                s"""    ::djinni::djinni_register_name_in_ns("${fullyQualifiedName}", "${spec.wasmNamespace.get}.${idJs
+                    .ty(ident)}");"""
+              )
+            }
+            w.wl(s"});")
+          }
+          w.wl
+          w.w(s"EMSCRIPTEN_BINDINGS(${withCppNamespace(ident.name)})").braced {
+            w.wl(s"$helper::staticInitializeConstants();")
+          }
+        })
+      )
+    }
+  }
+
+  override def generateInterface(
+      origin: String,
+      ident: Ident,
+      doc: Doc,
+      typeParams: Seq[TypeParam],
+      i: Interface
+  ) {
+    val refs = new WasmRefs(ident.name)
+    i.consts.foreach(c => refs.find(c.ty))
+    i.methods.foreach(m => {
+      m.params.foreach(p => refs.find(p.ty))
+      m.ret.foreach(refs.find)
+    })
+
+    val cls = withNs(Some(spec.cppNamespace), idCpp.ty(ident))
+    val helper = helperClass(ident)
+
+    writeHppFileGeneric(
+      spec.wasmOutFolder.get,
+      helperNamespace(),
+      wasmFilenameStyle
+    )(
+      ident.name,
+      origin,
+      refs.hpp,
+      Nil,
+      (w => {
+        w.w(s"struct $helper : ::djinni::JsInterface<$cls, $helper>").bracedSemi {
+          // types
+          w.wl(s"using CppType = std::shared_ptr<$cls>;")
+          w.wl(s"using CppOptType = std::shared_ptr<$cls>;")
+          w.wl("using JsType = em::val;")
+          w.wl(s"using Boxed = $helper;")
+          w.wl
+          // mashalling
+          w.wl("static CppType toCpp(JsType j) { return _fromJs(j); }")
+          w.wl(
+            "static JsType fromCppOpt(const CppOptType& c) { return {_toJs(c)}; }"
+          )
+          w.w("static JsType fromCpp(const CppType& c)").braced {
+            if (spec.cppNnType.isEmpty) {
+              w.wl(s"""::djinni::checkForNull(c.get(), "$helper::fromCpp");""")
+            }
+            w.wl("return fromCppOpt(c);")
+          }
+          w.wl
+          // method list
+          if (i.ext.cpp) {
+            w.wl("static em::val cppProxyMethods();")
+          }
+          w.wl
+          // stubs
+          if (i.ext.cpp) {
+            for (m <- i.methods.filter(m => !m.static || m.lang.js)) {
+              val selfRef =
+                if (m.static) ""
+                else if (m.params.isEmpty) "const CppType& self"
+                else "const CppType& self, "
+              w.w(
+                s"static ${stubRetType(m)} ${idCpp.method(m.ident)}(${selfRef}"
+              )
+              w.w(
+                m.params
+                  .map(p => {
+                    s"${stubParamType(p.ty)} ${stubParamName(idCpp.local(p.ident))}"
+                  })
+                  .mkString(",")
+              )
+              w.wl(");")
+            }
+            w.wl
+          }
+          // js proxy
+          if (i.ext.js) {
+            w.w(
+              s"struct JsProxy: ::djinni::JsProxyBase, $cls, ::djinni::InstanceTracker<JsProxy>"
+            ).bracedSemi {
+              w.wl("JsProxy(const em::val& v) : JsProxyBase(v) {}")
+              for (m <- i.methods) {
+                if (!m.static) {
+                  w.w(
+                    s"${cppMarshal.fqReturnType(m.ret)} ${idCpp.method(m.ident)}("
+                  )
+                  w.w(
+                    m.params
+                      .map(p => {
+                        s"${cppMarshal.fqParamType(p.ty)} ${idCpp.local(p.ident)}"
+                      })
+                      .mkString(",")
+                  )
+                  val constModifier = if (m.const) " const" else ""
+                  w.wl(s")$constModifier override;")
+                }
+              }
+            }
+          }
+          // init consts
+          if (!spec.wasmOmitConstants && !i.consts.isEmpty) {
+            w.wl("static void staticInitializeConstants();");
+          }
+        }
+      }),
+      (w => {})
+    )
+
+    writeCppFileGeneric(
+      spec.wasmOutFolder.get,
+      helperNamespace(),
+      wasmFilenameStyle,
+      spec.wasmIncludePrefix
+    )(
+      ident.name,
+      origin,
+      refs.cpp,
+      (w => {
+        // method list
+        if (i.ext.cpp) {
+          w.w(s"em::val $helper::cppProxyMethods()").braced {
+            w.w(
+              "static const em::val methods = em::val::array(std::vector<std::string>"
+            ).bracedEnd(");") {
+              for (m <- i.methods) {
+                if (!m.static) {
+                  w.wl(s""""${idJs.method(m.ident)}",""")
+                }
+              }
+            }
+            w.wl("return methods;")
+          }
+        }
+        w.wl
+        // stub methods
+        if (i.ext.cpp) {
+          for (m <- i.methods.filter(m => !m.static || m.lang.js)) {
+            val selfRef =
+              if (m.static) ""
+              else if (m.params.isEmpty) "const CppType& self"
+              else "const CppType& self, "
+            w.w(
+              s"${stubRetType(m)} $helper::${idCpp.method(m.ident)}(${selfRef}"
+            )
+            w.w(
+              m.params
+                .map(p => {
+                  s"${stubParamType(p.ty)} ${stubParamName(p.ident)}"
+                })
+                .mkString(",")
+            )
+            w.w(")").braced {
+              w.w("try").braced {
+                if (!m.ret.isEmpty) w.w("auto r = ")
+                if (m.static) w.w(s"$cls::") else w.w("self->")
+                writeAlignedCall(
+                  w,
+                  s"""${idCpp.method(m.ident)}(""",
+                  m.params,
+                  ")",
+                  p => {
+                    s"${helperClass(p.ty.resolved)}::toCpp(${stubParamName(p.ident)})"
+                  }
+                )
+                w.wl(";")
+                m.ret.fold(())(r =>
+                  w.wl(s"return ${helperClass(r.resolved)}::fromCpp(r);")
+                )
+              }
+              w.w("catch(const std::exception& e)").braced {
+                val helper =
+                  if (!m.ret.isEmpty) helperClass(m.ret.get.resolved)
+                  else "void"
+                w.wl(
+                  s"return ::djinni::ExceptionHandlingTraits<${helper}>::handleNativeException(e);"
+                );
+              }
+            }
+          }
+          w.wl
+        }
+        // js proxy methods
+        if (i.ext.js) {
+          for (m <- i.methods) {
+            if (!m.static) {
+              val constModifier = if (m.const) " const" else ""
+              w.w(
+                s"${cppMarshal.fqReturnType(m.ret)} ${helper}::JsProxy::${idCpp
+                    .method(m.ident)}("
+              )
+              w.w(
+                m.params
+                  .map(p => {
+                    s"${cppMarshal.fqParamType(p.ty)} ${idCpp.local(p.ident)}"
+                  })
+                  .mkString(",")
+              )
+              w.w(s")$constModifier").braced {
+                val methodName =
+                  q(idJs.method(m.ident.name)) + (if (m.params.isEmpty) ""
+                                                  else ", ")
+                writeAlignedCall(
+                  w,
+                  s"auto ret = callMethod($methodName",
+                  m.params,
+                  s")",
+                  p => {
+                    s"${helperClass(p.ty.resolved)}::fromCpp(${idCpp.local(p.ident)})"
+                  }
+                )
+                w.wl(";")
+                w.wl("checkError(ret);")
+                stubRetType(m) match {
+                  case "void" =>
+                  case "em::val" =>
+                    w.wl(
+                      s"return ${helperClass(m.ret.get.resolved)}::toCpp(ret);"
+                    )
+                  case _ =>
+                    w.wl(
+                      s"return ${helperClass(m.ret.get.resolved)}::toCpp(ret.as<${stubRetType(m)}>());"
+                    )
+                }
+              }
+              w.wl
+            }
+          }
+        }
+
+        val fullyQualifiedName = withCppNamespace(ident.name)
+        val fullyQualifiedJsName = withWasmNamespace(idJs.ty(ident.name))
+
+        // embind
+        w.w(s"EMSCRIPTEN_BINDINGS(${fullyQualifiedName})").braced {
+          val classRegister =
+            if (!spec.wasmOmitNsAlias && !spec.wasmNamespace.isEmpty) {
+              s"""::djinni::DjinniClass_<$cls>("${fullyQualifiedJsName}", "${spec.wasmNamespace.get}.${idJs
+                  .ty(ident.name)}")"""
+            } else {
+              s"""em::class_<$cls>("${fullyQualifiedJsName}")"""
+            }
+
+          w.wl(classRegister).nested {
+            w.wl(
+              s""".smart_ptr<std::shared_ptr<$cls>>("${fullyQualifiedJsName}")"""
+            )
+            w.wl(s""".function("${idJs
+                .method("native_destroy")}", &$helper::nativeDestroy)""")
+            if (i.ext.cpp) {
+              for (m <- i.methods.filter(m => !m.static || m.lang.js)) {
+                val funcType = if (m.static) "class_function" else "function"
+                w.wl(s""".$funcType("${idJs.method(
+                    m.ident.name
+                  )}", $helper::${idCpp.method(m.ident)})""")
+              }
+            }
+            w.wl(";")
+          }
+        }
+        // constants
+        if (!spec.wasmOmitConstants && !i.consts.isEmpty) {
+          generateWasmConstants(w, ident, i.consts);
+        }
+      })
+    )
+  }
+
+  def withWasmNamespace(name: String, sep: String = "_") =
+    spec.wasmNamespace match {
+      case Some(p) =>
+        p.replaceAll(
+          Pattern.quote("."),
+          Matcher.quoteReplacement(sep)
+        ) + sep + name
+      case None => name
+    }
+
+  def withCppNamespace(name: String, sep: String = "_") = {
+    spec.cppNamespace.replaceAll(
+      "::",
+      Matcher.quoteReplacement(sep)
+    ) + sep + name
+  }
+
+  override def generateRecord(
+      origin: String,
+      ident: Ident,
+      doc: Doc,
+      params: Seq[TypeParam],
+      r: Record
+  ) {
+    val refs = new WasmRefs(ident.name)
+    r.fields.foreach(f => refs.find(f.ty))
+    r.consts.foreach(c => refs.find(c.ty))
+
+    val cls = withNs(Some(spec.cppNamespace), idCpp.ty(ident.name))
+    val helper = helperClass(ident)
+
+    writeHppFileGeneric(
+      spec.wasmOutFolder.get,
+      helperNamespace(),
+      wasmFilenameStyle
+    )(
+      ident.name,
+      origin,
+      refs.hpp,
+      Nil,
+      (w => {
+        w.wl(s"struct $helper").bracedSemi {
+          w.wl(s"using CppType = $cls;")
+          w.wl("using JsType = em::val;")
+          w.wl(s"using Boxed = $helper;")
+          w.wl
+          w.wl("static CppType toCpp(const JsType& j);")
+          w.wl("static JsType fromCpp(const CppType& c);")
+          // init consts
+          if (!spec.wasmOmitConstants && !r.consts.isEmpty) {
+            w.wl("static void staticInitializeConstants();");
+          }
+        }
+      }),
+      (w => {})
+    )
+
+    writeCppFileGeneric(
+      spec.wasmOutFolder.get,
+      helperNamespace(),
+      wasmFilenameStyle,
+      spec.wasmIncludePrefix
+    )(
+      ident.name,
+      origin,
+      refs.cpp,
+      (w => {
+        w.w(s"auto $helper::toCpp(const JsType& j) -> CppType").braced {
+          writeAlignedCall(
+            w,
+            "return {",
+            r.fields,
+            "}",
+            f => {
+              s"""${helperClass(f.ty.resolved)}::Boxed::toCpp(j["${idJs
+                  .field(f.ident.name)}"])"""
+            }
+          )
+          w.wl(";")
+        }
+        w.w(s"auto $helper::fromCpp(const CppType& c) -> JsType").braced {
+          w.wl("em::val js = em::val::object();")
+          for (f <- r.fields) {
+            w.wl(s"""js.set("${idJs.field(f.ident.name)}", ${helperClass(
+                f.ty.resolved
+              )}::Boxed::fromCpp(c.${idCpp.field(f.ident)}));""")
+          }
+          w.wl("return js;")
+        }
+        // constants
+        if (!spec.wasmOmitConstants && !r.consts.isEmpty) {
+          generateWasmConstants(w, ident, r.consts);
+        }
+      })
+    )
+  }
+}
diff --git a/src/main/scala/djinni/YamlGenerator.scala b/src/main/scala/djinni/YamlGenerator.scala
index 187f19b8..8a172019 100644
--- a/src/main/scala/djinni/YamlGenerator.scala
+++ b/src/main/scala/djinni/YamlGenerator.scala
@@ -17,6 +17,8 @@ class YamlGenerator(spec: Spec) extends Generator(spec) {
   val javaMarshal = new JavaMarshal(spec)
   val jniMarshal = new JNIMarshal(spec)
   val cppCliMarshal = new CppCliMarshal(spec)
+  val wasmMarshal = new WasmGenerator(spec)
+  val tsMarshal = new TsGenerator(spec)
 
   case class QuotedString(
       str: String
@@ -76,6 +78,8 @@ class YamlGenerator(spec: Spec) extends Generator(spec) {
     w.wl("java:").nested { write(w, java(td)) }
     w.wl("jni:").nested { write(w, jni(td)) }
     w.wl("cs:").nested { write(w, cs(td)) }
+    w.wl("wasm:").nested { write(w, wasm(td)) }
+    w.wl("ts:").nested { write(w, ts(td)) }
   }
 
   private def write(w: IndentWriter, m: Map[String, Any]): Unit = {
@@ -122,7 +126,12 @@ class YamlGenerator(spec: Spec) extends Generator(spec) {
     def ext(e: Ext): String =
       (if (e.cpp) " +c" else "") + (if (e.objc) " +o" else "") + (if (e.java)
                                                                     " +j"
-                                                                  else "")
+                                                                  else
+                                                                    "") + (if (
+                                                                             e.js
+                                                                           ) " +w"
+                                                                           else
+                                                                             "")
     def deriving(r: Record) = {
       if (r.derivingTypes.isEmpty) {
         ""
@@ -188,6 +197,18 @@ class YamlGenerator(spec: Spec) extends Generator(spec) {
     "reference" -> cppCliMarshal.isReference(td)
   )
 
+  private def wasm(td: TypeDecl) = Map[String, Any](
+    "translator" -> QuotedString(wasmMarshal.helperName(mexpr(td))),
+    "header" -> QuotedString(wasmMarshal.include(td.ident)),
+    "typename" -> wasmMarshal.wasmType(mexpr(td))
+  )
+
+  private def ts(td: TypeDecl) = Map[String, Any](
+    "typename" -> tsMarshal.toTsType(mexpr(td), /*addNullability*/ false),
+    "module" -> QuotedString("./" + spec.tsModule)
+    // , "generic" -> false
+  )
+
   // TODO: there has to be a way to do all this without the MExpr/Meta conversions?
   private def mexpr(td: TypeDecl) = MExpr(meta(td), List())
 
@@ -251,7 +272,9 @@ object YamlGenerator {
       objcppOutRequired: Boolean,
       javaOutRequired: Boolean,
       jniOutRequired: Boolean,
-      cppCliOutRequired: Boolean
+      cppCliOutRequired: Boolean,
+      wasmOutRequired: Boolean,
+      tsOutRequired: Boolean
   ): MExtern = MExtern(
     td.ident.name.stripPrefix(
       td.properties("prefix").toString
@@ -369,6 +392,16 @@ object YamlGenerator {
         "generic",
         _.asInstanceOf[Boolean]
       ) orElse Option.apply[Boolean](false)
+    ),
+    MExtern.Wasm(
+      getOptionalField(td, "wasm", "typename"),
+      getOptionalField(td, "wasm", "translator"),
+      getOptionalField(td, "wasm", "header")
+    ),
+    MExtern.Ts(
+      getOptionalField(td, "ts", "typename"),
+      getOptionalField(td, "ts", "module"),
+      getOptionalField(td, "ts", "generic", false)
     )
   )
 
@@ -377,6 +410,33 @@ object YamlGenerator {
       m.asScala.collect { case (k: String, v: Any) => (k, v) }
     }
   }
+
+  private def getOptionalField[T](
+      td: ExternTypeDecl,
+      key: String,
+      subKey: String,
+      defVal: T
+  ) = {
+    if ((nested(td, key) getOrElse (Map[String, Any]())) contains subKey)
+      (nested(td, key) getOrElse (Map[String, Any]()))(subKey).asInstanceOf[T]
+    else defVal
+  }
+
+  private def getOptionalField(
+      td: ExternTypeDecl,
+      key: String,
+      subKey: String
+  ) = {
+    try {
+      (nested(td, key) getOrElse (Map[String, Any]()))(subKey).toString
+    } catch {
+      case e: java.util.NoSuchElementException => {
+        println(s"Warning: in ${td.origin}, missing field $key/$subKey")
+        "[unspecified]"
+      }
+    }
+  }
+
   private def nested[T](
       td: ExternTypeDecl,
       isRequired: Boolean,
@@ -389,7 +449,10 @@ object YamlGenerator {
       .flatten
       .map(v => convert(v)) match {
       case None if isRequired =>
-        throw Error(td.ident.loc, s"missing '$lang' definitions").toException
+        throw Error(
+          td.ident.loc,
+          s"missing requried: $isRequired '$lang' definitions for $td, $lang, $key, $convert"
+        ).toException
       case other => other
     }
   }
diff --git a/src/main/scala/djinni/ast/ast.scala b/src/main/scala/djinni/ast/ast.scala
index f5ae3c3e..57b66f68 100644
--- a/src/main/scala/djinni/ast/ast.scala
+++ b/src/main/scala/djinni/ast/ast.scala
@@ -64,10 +64,11 @@ case class Ext(
     cpp: Boolean,
     objc: Boolean,
     py: Boolean,
-    cppcli: Boolean
+    cppcli: Boolean,
+    js: Boolean
 ) {
   def any(): Boolean = {
-    java || cpp || objc || py || cppcli
+    java || cpp || objc || py || cppcli || js
   }
 }
 
@@ -119,7 +120,8 @@ object Interface {
       ret: Option[TypeRef],
       doc: Doc,
       static: Boolean,
-      const: Boolean
+      const: Boolean,
+      lang: Ext
   )
 }
 
diff --git a/src/main/scala/djinni/generator.scala b/src/main/scala/djinni/generator.scala
index eed2053f..84e7688a 100644
--- a/src/main/scala/djinni/generator.scala
+++ b/src/main/scala/djinni/generator.scala
@@ -102,7 +102,19 @@ package object generatorTools {
       cWrapperIncludePrefix: String,
       cWrapperIncludeCppPrefix: String,
       pyImportPrefix: String,
-      cppJsonSerialization: Option[String]
+      cppJsonSerialization: Option[String],
+      wasmOutFolder: Option[File],
+      wasmIncludePrefix: String,
+      wasmIncludeCppPrefix: String,
+      wasmBaseLibIncludePrefix: String,
+      wasmOmitConstants: Boolean,
+      wasmNamespace: Option[String],
+      wasmOmitNsAlias: Boolean,
+      jsIdentStyle: JsIdentStyle,
+      tsOutFolder: Option[File],
+      tsModule: String,
+      tsSupportFilesOutFolder: Option[File],
+      moduleName: String
   )
 
   def preComma(s: String): String = {
@@ -168,6 +180,15 @@ package object generatorTools {
       const: IdentConverter,
       file: IdentConverter
   )
+  case class JsIdentStyle(
+      ty: IdentConverter,
+      typeParam: IdentConverter,
+      method: IdentConverter,
+      field: IdentConverter,
+      local: IdentConverter,
+      enum: IdentConverter,
+      const: IdentConverter
+  )
 
   object IdentStyle {
     val camelUpper: String => String = (s: String) =>
@@ -235,6 +256,16 @@ package object generatorTools {
       file = camelUpper
     )
 
+    val jsDefault = JsIdentStyle(
+      ty = camelUpper,
+      typeParam = camelUpper,
+      method = camelLower,
+      field = camelLower,
+      local = camelLower,
+      enum = underCaps,
+      const = underCaps
+    )
+
     val styles: Map[String, String => String] = Map(
       "FooBar" -> camelUpper,
       "fooBar" -> camelLower,
@@ -403,6 +434,18 @@ package object generatorTools {
         }
         new CffiGenerator(spec).generate(idl)
       }
+      if (spec.wasmOutFolder.isDefined) {
+        if (!spec.skipGeneration) {
+          createFolder("WASM", spec.wasmOutFolder.get)
+        }
+        new WasmGenerator(spec).generate(idl)
+      }
+      if (spec.tsOutFolder.isDefined) {
+        if (!spec.skipGeneration) {
+          createFolder("TypeScript", spec.tsOutFolder.get)
+        }
+        new TsGenerator(spec).generate(idl)
+      }
       None
     } catch {
       case GenerateException(message) => Some(message)
@@ -530,6 +573,7 @@ abstract class Generator(spec: Spec) {
   val idObjc = spec.objcIdentStyle
   val idPython = spec.pyIdentStyle
   val idCs = spec.cppCliIdentStyle
+  val idJs = spec.jsIdentStyle
 
   def wrapNamespace(
       w: IndentWriter,
@@ -716,26 +760,29 @@ abstract class Generator(spec: Spec) {
   def writeEnumOptionNone(
       w: IndentWriter,
       e: Enum,
-      ident: IdentConverter
+      ident: IdentConverter,
+      noneFlagWriter: () => String = () => " = 0,"
   ): Unit = {
-    for (
-      o <- e.options.find(_.specialFlag.contains(Enum.SpecialFlag.NoFlags))
-    ) {
+    for (o <- e.options.find(_.specialFlag == Some(Enum.SpecialFlag.NoFlags))) {
       writeDoc(w, o.doc)
-      w.wl(ident(o.ident.name) + " = 0,")
+      w.wl(ident(o.ident.name) + noneFlagWriter())
     }
   }
 
   def writeEnumOptions(
       w: IndentWriter,
       e: Enum,
-      ident: IdentConverter
+      ident: IdentConverter,
+      flagWriter: (Enum.Option, Int) => String = (o: Enum.Option, shift: Int) =>
+        s" = 1u << $shift,",
+      ordinalWriter: (Int) => String = (ordinal: Int) => ","
   ): Unit = {
     var shift = 0
     for (o <- normalEnumOptions(e)) {
       writeDoc(w, o.doc)
       w.wl(
-        ident(o.ident.name) + (if (e.flags) s" = 1u << $shift" else "") + ","
+        ident(o.ident.name) + (if (e.flags) flagWriter(o, shift)
+                               else ordinalWriter(shift))
       )
       shift += 1
     }
@@ -744,19 +791,20 @@ abstract class Generator(spec: Spec) {
   def writeEnumOptionAll(
       w: IndentWriter,
       e: Enum,
-      ident: IdentConverter
+      ident: IdentConverter,
+      allFlagWriter: (Seq[Tuple2[Int, String]]) => String =
+        (ordinalsAndNames: Seq[Tuple2[Int, String]]) =>
+          s""" = ${ordinalsAndNames
+              .map(e => e._2)
+              .fold("0")((acc, o) => acc + " | " + o)},"""
   ): Unit = {
     for (
       o <- e.options.find(_.specialFlag.contains(Enum.SpecialFlag.AllFlags))
     ) {
       writeDoc(w, o.doc)
-      w.w(ident(o.ident.name) + " = ")
-      w.w(
-        normalEnumOptions(e)
-          .map(o => ident(o.ident.name))
-          .fold("0")((acc, o) => acc + " | " + o)
-      )
-      w.wl(",")
+      val ordinalsAndNames = normalEnumOptions(e).zipWithIndex
+        .map { case (o, i) => Tuple2(i, ident(o.ident.name)) }
+      w.wl(ident(o.ident.name) + allFlagWriter(ordinalsAndNames))
     }
   }
 
diff --git a/src/main/scala/djinni/meta.scala b/src/main/scala/djinni/meta.scala
index b8752d5c..ce17f84c 100644
--- a/src/main/scala/djinni/meta.scala
+++ b/src/main/scala/djinni/meta.scala
@@ -44,7 +44,9 @@ package object meta {
       objcpp: MExtern.Objcpp,
       java: MExtern.Java,
       jni: MExtern.Jni,
-      cs: MExtern.Cs
+      cs: MExtern.Cs,
+      wasm: MExtern.Wasm,
+      ts: MExtern.Ts
   ) extends Meta
   object MExtern {
     // These hold the information marshals need to interface with existing types correctly
@@ -112,6 +114,16 @@ package object meta {
           Boolean
         ] // Set to false to exclude type arguments from the C++/CLI typename. This is false by default. Useful if template arguments are only used in C++.
     )
+    case class Wasm(
+        typename: String, // The Emscripten type to use (e.g. em::val, int32_t)
+        translator: String, // C++ typename containing toCpp/fromCpp methods
+        header: String // Where to find the translator class
+    )
+    case class Ts(
+        typename: String, // The TypeScript type
+        module: String, // The module to import for the type
+        generic: Boolean
+    )
   }
 
   abstract sealed class MOpaque extends Meta { val idlName: String }
@@ -270,6 +282,10 @@ package object meta {
     }
   }
 
+  def isOptional(ty: MExpr): Boolean = {
+    ty.base == MOptional && ty.args.length == 1
+  }
+
   def isOptionalInterface(ty: MExpr): Boolean = {
     ty.base == MOptional && ty.args.length == 1 && isInterface(ty.args.head)
   }
diff --git a/src/main/scala/djinni/parser.scala b/src/main/scala/djinni/parser.scala
index d7fc5449..73d63338 100644
--- a/src/main/scala/djinni/parser.scala
+++ b/src/main/scala/djinni/parser.scala
@@ -94,10 +94,34 @@ case class Parser(includePaths: List[String]) {
     def ext(default: Ext): Parser[Ext] =
       (rep1("+" ~> ident) >> checkExts) | success(default)
     def extRecord: Parser[Ext] = ext(
-      Ext(java = false, cpp = false, objc = false, py = false, cppcli = false)
+      Ext(
+        java = false,
+        cpp = false,
+        objc = false,
+        py = false,
+        cppcli = false,
+        js = false
+      )
     )
     def extInterface: Parser[Ext] = ext(
-      Ext(java = true, cpp = true, objc = true, py = true, cppcli = true)
+      Ext(
+        java = true,
+        cpp = true,
+        objc = true,
+        py = true,
+        cppcli = true,
+        js = true
+      )
+    )
+    def supportLang: Parser[Ext] = ext(
+      Ext(
+        java = true,
+        cpp = true,
+        objc = true,
+        py = true,
+        cppcli = true,
+        js = true
+      )
     )
 
     def checkExts(parts: List[Ident]): Parser[Ext] = {
@@ -106,6 +130,7 @@ case class Parser(includePaths: List[String]) {
       var foundObjc = false
       var foundPy = false
       var foundCs = false
+      var foundJavascript = false
 
       for (part <- parts)
         part.name match {
@@ -129,9 +154,15 @@ case class Parser(includePaths: List[String]) {
             if (foundCs) return err("Found multiple \"s\" modifiers.")
             foundCs = true
           }
+          case "w" => {
+            if (foundJavascript) return err("Found multiple \"w\" modifiers.")
+            foundJavascript = true
+          }
           case _ => return err("Invalid modifier \"" + part.name + "\"")
         }
-      success(Ext(foundJava, foundCpp, foundObjc, foundPy, foundCs))
+      success(
+        Ext(foundJava, foundCpp, foundObjc, foundPy, foundCs, foundJavascript)
+      )
     }
 
     def typeDef: Parser[TypeDef] = record | enum | flags | interface
@@ -224,10 +255,28 @@ case class Parser(includePaths: List[String]) {
     def method: Parser[Interface.Method] =
       doc ~ staticLabel ~ constLabel ~ ident ~ parens(
         repsepend(field, ",")
-      ) ~ opt(ret) ^^ {
-        case doc ~ staticLabel ~ constLabel ~ ident ~ params ~ ret =>
-          Interface.Method(ident, params, ret, doc, staticLabel, constLabel)
+      ) ~ opt(ret) ~ supportLang ^^ {
+        case doc ~ staticLabel ~ constLabel ~ ident ~ params ~ ret ~ ext =>
+          Interface.Method(
+            ident,
+            params,
+            ret,
+            doc,
+            staticLabel,
+            constLabel,
+            ext
+          )
       }
+    // def method: Parser[Interface.Method] =
+    //   doc ~ staticLabel ~ constLabel ~ ident ~ parens(repsepend(field, ",")) ~ opt(ret) ~ supportLang ^^ {
+    //   case doc ~ staticLabel ~ constLabel ~ ident ~ params ~ ret ~ ext => {
+    //     ret match {
+    //       case Some(r) if (r.expr.ident.name == "void") => Interface.Method(ident, params, None, doc, staticLabel, constLabel, ext)
+    //       case _ => Interface.Method(ident, params, ret, doc, staticLabel, constLabel, ext)
+    //     }
+    //   }
+    // }
+
     def ret: Parser[TypeRef] = ":" ~> typeRef
 
     def boolValue: Parser[Boolean] = "([Tt]rue)|([Ff]alse)".r ^^ { s: String =>
diff --git a/src/main/scala/djinni/resolver.scala b/src/main/scala/djinni/resolver.scala
index 6eac5aad..4ddba2aa 100644
--- a/src/main/scala/djinni/resolver.scala
+++ b/src/main/scala/djinni/resolver.scala
@@ -36,7 +36,9 @@ package object resolver {
       objcppOutRequired: Boolean,
       javaOutRequired: Boolean,
       jniOutRequired: Boolean,
-      cppCliOutRequired: Boolean
+      cppCliOutRequired: Boolean,
+      wasmOutRequired: Boolean,
+      tsOutRequired: Boolean
   ): Option[Error] = {
 
     try {
@@ -77,7 +79,9 @@ package object resolver {
                 objcppOutRequired = objcppOutRequired,
                 javaOutRequired = javaOutRequired,
                 jniOutRequired = jniOutRequired,
-                cppCliOutRequired = cppCliOutRequired
+                cppCliOutRequired = cppCliOutRequired,
+                wasmOutRequired = wasmOutRequired,
+                tsOutRequired = tsOutRequired
               )
           }
         )