diff --git a/README.md b/README.md
index 4fc34006..61fb8479 100644
--- a/README.md
+++ b/README.md
@@ -18,10 +18,10 @@ Notable features of this project includes:
 * Managing arguments between JS/Rust (strings, numbers, classes, etc)
 * Importing JS functions with richer types (strings)
 * Receiving arbitrary JS objects in Rust, passing them through to JS
+* Generates Typescript for now instead of JS (although that may come later)
 
 Planned features include:
 
-* An optional flag to generate Typescript bindings
 * Field setters/getters in JS through Rust functions
 * ... and more coming soon!
 
@@ -104,19 +104,26 @@ set of JS bindings as well. Let's invoke it!
 
 ```
 $ wasm-bindgen target/wasm32-unknown-unknown/release/js_hello_world.wasm \
-  --output-js hello.js \
+  --output-ts hello.ts \
   --output-wasm hello.wasm
 ```
 
-This'll create a `hello.js` which binds the functions described in
-`js_hello_world.wasm`, and the `hello.wasm` will be a little smaller than the
-input `js_hello_world.wasm`, but it's otherwise equivalent. Note that `hello.js`
-isn't very pretty so to read it you'll probably want to run it through a JS
-formatter.
+This'll create a `hello.ts` (a TypeScript file) which binds the functions
+described in `js_hello_world.wasm`, and the `hello.wasm` will be a little
+smaller than the input `js_hello_world.wasm`, but it's otherwise equivalent.
+Note that `hello.ts` isn't very pretty so to read it you'll probably want to run
+it through a formatter.
 
-Note that `hello.js` uses ES6 modules for easier integration into transpilers
-like webpack/rollup/babel/etc. We're going to test out the syntax in the browser
-with `index.html` below, but your browser may not natively support ES6 modules
+Typically you'll be feeding this typescript into a larger build system, and
+often you'll be using this with your own typescript project as well. For now
+though we'll just want the JS output, so let's convert it real quick:
+
+```
+$ npm install typescript @types/webassembly-js-api @types/text-encoding
+$ ./node_modules/typescript/bin/tsc hello.ts --lib es6 -m es2015
+```
+
+Below we'll be using ES6 modules, but your browser may not support them natively
 just yet. To see more information about this, you can browse
 [online](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import).
 
@@ -285,12 +292,10 @@ this][bindings]. You can view them in action like so:
         .then(resp => resp.arrayBuffer())
         .then(bytes => {
           return instantiate(bytes, {
-            env: {
-              bar_on_reset(s, token) {
-                console.log(token);
-                console.log(`this instance of bar was reset to ${s}`);
-              },
-            }
+            bar_on_reset(s, token) {
+              console.log(token);
+              console.log(`this instance of bar was reset to ${s}`);
+            },
           });
         })
         .then(mod => {
diff --git a/crates/test-support/src/lib.rs b/crates/test-support/src/lib.rs
index 26346524..a21f94a8 100644
--- a/crates/test-support/src/lib.rs
+++ b/crates/test-support/src/lib.rs
@@ -46,20 +46,22 @@ pub fn project() -> Project {
 
             ("Cargo.lock".to_string(), lockfile),
 
-            ("run.js".to_string(), r#"
-                var fs = require("fs");
-                var out = require("./out.compat");
-                var test = require("./test.compat");
-                var wasm = fs.readFileSync("out.wasm");
-                var process = require("process");
+            ("run.ts".to_string(), r#"
+                import * as fs from "fs";
+                import * as process from "process";
 
-                out.instantiate(wasm, test.imports).then(m => {
-                    test.test(m);
-                    if (m.assertHeapAndStackEmpty)
-                        m.assertHeapAndStackEmpty();
-                }).catch(function(error) {
-                    console.error(error);
-                    process.exit(1);
+                import { instantiate } from "./out";
+                import * as test from "./test";
+
+                var wasm = fs.readFileSync("out.wasm");
+
+                instantiate(wasm, test.imports).then(m => {
+                  test.test(m);
+                  if (m.assertHeapAndStackEmpty)
+                    m.assertHeapAndStackEmpty();
+                }).catch(error => {
+                  console.error(error);
+                  process.exit(1);
                 });
             "#.to_string()),
         ],
@@ -78,7 +80,7 @@ pub fn root() -> PathBuf {
     return me
 }
 
-fn babel() -> PathBuf {
+fn typescript() -> PathBuf {
     static INIT: Once = ONCE_INIT;
 
     let mut me = env::current_exe().unwrap();
@@ -86,7 +88,7 @@ fn babel() -> PathBuf {
     me.pop(); // chop off `deps`
     me.pop(); // chop off `debug` / `release`
     let install_dir = me.clone();
-    me.push("node_modules/babel-cli/bin/babel.js");
+    me.push("node_modules/typescript/bin/tsc");
 
     INIT.call_once(|| {
         if !me.exists() {
@@ -99,8 +101,9 @@ fn babel() -> PathBuf {
             };
             run(npm
                 .arg("install")
-                .arg("babel-cli")
-                .arg("babel-preset-env")
+                .arg("typescript")
+                .arg("@types/node")
+                .arg("@types/webassembly-js-api")
                 .current_dir(&install_dir), "npm");
             assert!(me.exists());
         }
@@ -151,20 +154,21 @@ impl Project {
             .debug(true)
             .generate()
             .expect("failed to run bindgen");
-        obj.write_js_to(root.join("out.js")).expect("failed to write js");
+        obj.write_ts_to(root.join("out.ts")).expect("failed to write ts");
         obj.write_wasm_to(root.join("out.wasm")).expect("failed to write wasm");
 
         let mut cmd = Command::new("node");
-        cmd.arg(babel())
-            .arg(root.join("out.js"))
-            .arg("--presets").arg("env")
-            .arg("--out-file").arg(root.join("out.compat.js"));
-        run(&mut cmd, "node");
-        let mut cmd = Command::new("node");
-        cmd.arg(babel())
-            .arg(root.join("test.js"))
-            .arg("--presets").arg("env")
-            .arg("--out-file").arg(root.join("test.compat.js"));
+        cmd.arg(typescript())
+            .current_dir(&target_dir)
+            .arg(root.join("run.ts"))
+            .arg("--strict")
+            .arg("--noImplicitAny")
+            .arg("--strictNullChecks")
+            .arg("--strictFunctionTypes")
+            .arg("--noUnusedLocals")
+            .arg("--noImplicitReturns")
+            .arg("--lib")
+            .arg("es6");
         run(&mut cmd, "node");
 
         let mut cmd = Command::new("node");
diff --git a/crates/wasm-bindgen-cli-support/src/js.rs b/crates/wasm-bindgen-cli-support/src/js.rs
deleted file mode 100644
index b4fb7e1e..00000000
--- a/crates/wasm-bindgen-cli-support/src/js.rs
+++ /dev/null
@@ -1,486 +0,0 @@
-use shared;
-
-#[derive(Default)]
-pub struct Js {
-    expose_global_memory: bool,
-    expose_global_exports: bool,
-    expose_get_string_from_wasm: bool,
-    expose_pass_string_to_wasm: bool,
-    expose_assert_num: bool,
-    expose_assert_class: bool,
-    expose_token: bool,
-    expose_objects: bool,
-    exports: Vec<(String, String)>,
-    classes: Vec<String>,
-    imports: Vec<String>,
-    pub nodejs: bool,
-    pub debug: bool,
-}
-
-impl Js {
-
-    pub fn generate_program(&mut self, program: &shared::Program) {
-        for f in program.free_functions.iter() {
-            self.generate_free_function(f);
-        }
-        for s in program.structs.iter() {
-            self.generate_struct(s);
-        }
-        for s in program.imports.iter() {
-            self.generate_import(s);
-        }
-    }
-
-    pub fn generate_free_function(&mut self, func: &shared::Function) {
-        let ret = self.generate_function(&format!("function {}", func.name),
-                                         &func.name,
-                                         false,
-                                         &func.arguments,
-                                         func.ret.as_ref());
-
-        self.exports.push((func.name.clone(), ret));
-    }
-
-    pub fn generate_struct(&mut self, s: &shared::Struct) {
-        let mut dst = String::new();
-        self.expose_token = true;
-        self.expose_global_exports = true;
-        dst.push_str(&format!("
-            class {} {{
-                constructor(ptr, sym) {{
-                    _checkToken(sym);
-                    this.__wasmPtr = ptr;
-                }}
-
-                free() {{
-                    const ptr = this.__wasmPtr;
-                    this.__wasmPtr = 0;
-                    exports.{}(ptr);
-                }}
-        ", s.name, s.free_function()));
-
-        for function in s.functions.iter() {
-            let f = self.generate_function(
-                &format!("static {}", function.name),
-                &function.struct_function_export_name(&s.name),
-                false,
-                &function.arguments,
-                function.ret.as_ref(),
-            );
-            dst.push_str(&f);
-            dst.push_str("\n");
-        }
-        for method in s.methods.iter() {
-            let f = self.generate_function(
-                &format!("{}", method.function.name),
-                &method.function.struct_function_export_name(&s.name),
-                true,
-                &method.function.arguments,
-                method.function.ret.as_ref(),
-            );
-            dst.push_str(&f);
-            dst.push_str("\n");
-        }
-        dst.push_str("}\n");
-        self.classes.push(dst);
-        self.exports.push((s.name.clone(), s.name.clone()));
-    }
-
-    fn generate_function(&mut self,
-                         name: &str,
-                         wasm_name: &str,
-                         is_method: bool,
-                         arguments: &[shared::Type],
-                         ret: Option<&shared::Type>) -> String {
-        let mut dst = format!("{}(", name);
-        let mut passed_args = String::new();
-        let mut arg_conversions = String::new();
-        let mut destructors = String::new();
-
-        if is_method {
-            passed_args.push_str("this.__wasmPtr");
-        }
-
-        for (i, arg) in arguments.iter().enumerate() {
-            let name = format!("arg{}", i);
-            if i > 0 {
-                dst.push_str(", ");
-            }
-            dst.push_str(&name);
-
-            let mut pass = |arg: &str| {
-                if passed_args.len() > 0 {
-                    passed_args.push_str(", ");
-                }
-                passed_args.push_str(arg);
-            };
-            match *arg {
-                shared::Type::Number => {
-                    self.expose_assert_num = true;
-                    arg_conversions.push_str(&format!("_assertNum({});\n", name));
-                    pass(&name)
-                }
-                shared::Type::BorrowedStr |
-                shared::Type::String => {
-                    self.expose_global_exports = true;
-                    self.expose_pass_string_to_wasm = true;
-                    arg_conversions.push_str(&format!("\
-                        const [ptr{i}, len{i}] = passStringToWasm({arg});
-                    ", i = i, arg = name));
-                    pass(&format!("ptr{}", i));
-                    pass(&format!("len{}", i));
-                    if let shared::Type::BorrowedStr = *arg {
-                        destructors.push_str(&format!("\n\
-                            exports.__wbindgen_free(ptr{i}, len{i});\n\
-                        ", i = i));
-                    }
-                }
-                shared::Type::ByRef(ref s) |
-                shared::Type::ByMutRef(ref s) => {
-                    self.expose_assert_class = true;
-                    arg_conversions.push_str(&format!("\
-                        const ptr{i} = _assertClass({arg}, {struct_});
-                    ", i = i, arg = name, struct_ = s));
-                    pass(&format!("ptr{}", i));
-                }
-                shared::Type::ByValue(ref s) => {
-                    self.expose_assert_class = true;
-                    arg_conversions.push_str(&format!("\
-                        const ptr{i} = _assertClass({arg}, {struct_});
-                        {arg}.__wasmPtr = 0;
-                    ", i = i, arg = name, struct_ = s));
-                    pass(&format!("ptr{}", i));
-                }
-                shared::Type::JsObject => {
-                    self.expose_objects = true;
-                    arg_conversions.push_str(&format!("\
-                        const idx{i} = addHeapObject({arg});
-                    ", i = i, arg = name));
-                    pass(&format!("idx{}", i));
-                }
-                shared::Type::JsObjectRef => {
-                    self.expose_objects = true;
-                    arg_conversions.push_str(&format!("\
-                        const idx{i} = addBorrowedObject({arg});
-                    ", i = i, arg = name));
-                    destructors.push_str("popBorrowedObject();\n");
-                    pass(&format!("idx{}", i));
-                }
-            }
-        }
-        let convert_ret = match ret {
-            None |
-            Some(&shared::Type::Number) => format!("return ret;"),
-            Some(&shared::Type::JsObject) => {
-                self.expose_objects = true;
-                format!("return takeObject(ret);")
-            }
-            Some(&shared::Type::JsObjectRef) |
-            Some(&shared::Type::BorrowedStr) |
-            Some(&shared::Type::ByMutRef(_)) |
-            Some(&shared::Type::ByRef(_)) => panic!(),
-            Some(&shared::Type::ByValue(ref name)) => {
-                format!("\
-                    return new {name}(ret, token);
-                ", name = name)
-            }
-            Some(&shared::Type::String) => {
-                self.expose_get_string_from_wasm = true;
-                self.expose_global_exports = true;
-                format!("
-                    const ptr = exports.__wbindgen_boxed_str_ptr(ret);
-                    const len = exports.__wbindgen_boxed_str_len(ret);
-                    const realRet = getStringFromWasm(ptr, len);
-                    exports.__wbindgen_boxed_str_free(ret);
-                    return realRet;
-                ")
-            }
-        };
-        dst.push_str(") {\n        ");
-        dst.push_str(&arg_conversions);
-        self.expose_global_exports = true;
-        if destructors.len() == 0 {
-            dst.push_str(&format!("\
-                const ret = exports.{f}({passed});
-                {convert_ret}
-            ", f = wasm_name, passed = passed_args, convert_ret = convert_ret));
-        } else {
-            dst.push_str(&format!("\
-                try {{
-                    const ret = exports.{f}({passed});
-                    {convert_ret}
-                }} finally {{
-                    {destructors}
-                }}
-            ", f = wasm_name, passed = passed_args, destructors = destructors,
-                convert_ret = convert_ret));
-        }
-        dst.push_str("}");
-        return dst
-    }
-
-    pub fn generate_import(&mut self, import: &shared::Function) {
-        let mut dst = String::new();
-
-        dst.push_str(&format!("const {0} = imports.env.{0};\n", import.name));
-        dst.push_str(&format!("imports.env.{0} = function {0}_shim(", import.name));
-
-        let mut invocation = String::new();
-        for (i, arg) in import.arguments.iter().enumerate() {
-            if invocation.len() > 0 {
-                invocation.push_str(", ");
-            }
-            if i > 0 {
-                dst.push_str(", ");
-            }
-            match *arg {
-                shared::Type::Number => {
-                    invocation.push_str(&format!("arg{}", i));
-                    dst.push_str(&format!("arg{}", i));
-                }
-                shared::Type::BorrowedStr => {
-                    self.expose_get_string_from_wasm = true;
-                    invocation.push_str(&format!("getStringFromWasm(ptr{0}, len{0})", i));
-                    dst.push_str(&format!("ptr{0}, len{0}", i));
-                }
-                shared::Type::JsObject => {
-                    self.expose_objects = true;
-                    invocation.push_str(&format!("takeObject(arg{})", i));
-                    dst.push_str(&format!("arg{}", i));
-                }
-                shared::Type::JsObjectRef => {
-                    self.expose_objects = true;
-                    invocation.push_str(&format!("getObject(arg{})", i));
-                    dst.push_str(&format!("arg{}", i));
-                }
-                shared::Type::String |
-                shared::Type::ByRef(_) |
-                shared::Type::ByMutRef(_) |
-                shared::Type::ByValue(_) => {
-                    panic!("unsupported type in import");
-                }
-            }
-        }
-        dst.push_str(") {\n");
-        dst.push_str(&format!("return {}({});\n}}", import.name, invocation));
-
-        self.imports.push(dst);
-    }
-
-    pub fn to_string(&mut self) -> String {
-        let mut globals = String::new();
-        let mut real_globals = String::new();
-        if self.expose_global_memory ||
-            self.expose_pass_string_to_wasm ||
-            self.expose_get_string_from_wasm
-        {
-            globals.push_str("const memory = obj.instance.exports.memory;\n");
-        }
-        if self.expose_global_exports ||
-            self.expose_pass_string_to_wasm ||
-            self.expose_get_string_from_wasm
-        {
-            globals.push_str("const exports = obj.instance.exports;\n");
-        }
-        if self.expose_token {
-            globals.push_str("\
-                const token = Symbol('foo');
-                function _checkToken(sym) {
-                    if (token !== sym)
-                        throw new Error('cannot invoke `new` directly');
-                }
-            ");
-        }
-        if self.expose_assert_num {
-            globals.push_str("\
-                function _assertNum(n) {
-                    if (typeof(n) !== 'number')
-                        throw new Error('expected a number argument');
-                }
-            ");
-        }
-        if self.expose_pass_string_to_wasm {
-            if self.nodejs {
-                globals.push_str("
-                    function passStringToWasm(arg) {
-                        if (typeof(arg) !== 'string')
-                            throw new Error('expected a string argument');
-                        const buf = Buffer.from(arg);
-                        const len = buf.length;
-                        const ptr = exports.__wbindgen_malloc(len);
-                        let array = new Uint8Array(memory.buffer);
-                        buf.copy(array, ptr);
-                        return [ptr, len];
-                    }
-                ");
-            } else {
-                globals.push_str("
-                    function passStringToWasm(arg) {
-                        if (typeof(arg) !== 'string')
-                            throw new Error('expected a string argument');
-                        const buf = new TextEncoder('utf-8').encode(arg);
-                        const len = buf.length;
-                        const ptr = exports.__wbindgen_malloc(len);
-                        let array = new Uint8Array(memory.buffer);
-                        array.set(buf, ptr);
-                        return [ptr, len];
-                    }
-                ");
-            }
-        }
-        if self.expose_get_string_from_wasm {
-            real_globals.push_str("let getStringFromWasm = null;\n");
-            if self.nodejs {
-                globals.push_str("
-                    getStringFromWasm = function getStringFromWasm(ptr, len) {
-                        const mem = new Uint8Array(memory.buffer);
-                        const buf = Buffer.from(mem.slice(ptr, ptr + len));
-                        const ret = buf.toString();
-                        return ret;
-                    }
-                ");
-            } else {
-                globals.push_str("
-                    getStringFromWasm = function getStringFromWasm(ptr, len) {
-                        const mem = new Uint8Array(memory.buffer);
-                        const slice = mem.slice(ptr, ptr + len);
-                        const ret = new TextDecoder('utf-8').decode(slice);
-                        return ret;
-                    }
-                ");
-            }
-        }
-        if self.expose_assert_class {
-            globals.push_str("
-                function _assertClass(instance, klass) {
-                    if (!(instance instanceof klass))
-                        throw new Error(`expected instance of ${klass.name}`);
-                    return instance.__wasmPtr;
-                }
-            ");
-        }
-
-
-        if self.expose_objects {
-            real_globals.push_str("
-                let stack = [];
-                let slab = [];
-                let slab_next = 0;
-
-                function addHeapObject(obj) {
-                    if (slab_next == slab.length) {
-                        slab.push(slab.length + 1);
-                    }
-                    const idx = slab_next;
-                    slab_next = slab[idx];
-                    slab[idx] = { obj, cnt: 1 };
-                    return idx << 1;
-                }
-
-                function addBorrowedObject(obj) {
-                    stack.push(obj);
-                    return ((stack.length - 1) << 1) | 1;
-                }
-
-                function popBorrowedObject() {
-                    stack.pop();
-                }
-
-                function getObject(idx) {
-                    if (idx & 1 == 1) {
-                        return stack[idx >> 1];
-                    } else {
-                        return slab[idx >> 1].obj;
-                    }
-                }
-
-                function takeObject(idx) {
-                    const ret = getObject(idx);
-                    dropRef(idx);
-                    return ret;
-                }
-
-                function cloneRef(idx) {
-                    // If this object is on the stack promote it to the heap.
-                    if (idx & 1 == 1) {
-                        return addHeapObject(getObject(idx));
-                    }
-
-                    // Otherwise if the object is on the heap just bump the
-                    // refcount and move on
-                    slab[idx >> 1].cnt += 1;
-                    return idx;
-                }
-
-                function dropRef(idx) {
-                    if (idx & 1 == 1)
-                        throw new Error('cannot drop ref of stack objects');
-
-                    // Decrement our refcount, but if it's still larger than one
-                    // keep going
-                    let obj = slab[idx >> 1];
-                    obj.cnt -= 1;
-                    if (obj.cnt > 0)
-                        return;
-
-                    // If we hit 0 then free up our space in the slab
-                    slab[idx >> 1] = slab_next;
-                    slab_next = idx >> 1;
-                }
-            ");
-
-            if self.debug {
-                self.exports.push(
-                    (
-                        "assertHeapAndStackEmpty".to_string(),
-                        "function() {
-                            if (stack.length > 0)
-                                throw new Error('stack is not empty');
-                            for (let i = 0; i < slab.length; i++) {
-                                if (typeof(slab[i]) !== 'number')
-                                    throw new Error('slab is not empty');
-                            }
-                        }".to_string(),
-                    )
-                );
-            }
-        }
-
-        let mut exports = String::new();
-        for class in self.classes.iter() {
-            exports.push_str(class);
-            exports.push_str("\n");
-        }
-        for &(ref name, ref body) in self.exports.iter() {
-            exports.push_str("obj.");
-            exports.push_str(name);
-            exports.push_str(" = ");
-            exports.push_str(body);
-            exports.push_str(";\n");
-        }
-        let mut imports = String::new();
-        for import in self.imports.iter() {
-            imports.push_str(import);
-            imports.push_str("\n");
-        }
-
-        if self.expose_objects {
-            imports.push_str("
-                imports.env.__wasm_bindgen_object_clone_ref = cloneRef;
-                imports.env.__wasm_bindgen_object_drop_ref = dropRef;
-            ");
-        }
-        format!("
-            {}
-            function xform(obj) {{
-                {}
-                {}
-                return obj;
-            }}
-            export function instantiate(bytes, imports) {{
-                {}
-                return WebAssembly.instantiate(bytes, imports).then(xform);
-            }}
-        ", real_globals, globals, exports, imports)
-    }
-}
diff --git a/crates/wasm-bindgen-cli-support/src/lib.rs b/crates/wasm-bindgen-cli-support/src/lib.rs
index 2e1c837d..db40f208 100644
--- a/crates/wasm-bindgen-cli-support/src/lib.rs
+++ b/crates/wasm-bindgen-cli-support/src/lib.rs
@@ -11,7 +11,7 @@ use std::io::Write;
 use failure::{Error, ResultExt};
 use parity_wasm::elements::*;
 
-mod js;
+mod ts;
 
 pub struct Bindgen {
     path: Option<PathBuf>,
@@ -69,16 +69,16 @@ impl Bindgen {
 }
 
 impl Object {
-    pub fn write_js_to<P: AsRef<Path>>(&self, path: P) -> Result<(), Error> {
-        self._write_js_to(path.as_ref())
+    pub fn write_ts_to<P: AsRef<Path>>(&self, path: P) -> Result<(), Error> {
+        self._write_ts_to(path.as_ref())
     }
 
-    fn _write_js_to(&self, path: &Path) -> Result<(), Error> {
-        let js = self.generate_js();
+    fn _write_ts_to(&self, path: &Path) -> Result<(), Error> {
+        let ts = self.generate_ts();
         let mut f = File::create(path).with_context(|_| {
             format!("failed to create file at {:?}", path)
         })?;
-        f.write_all(js.as_bytes()).with_context(|_| {
+        f.write_all(ts.as_bytes()).with_context(|_| {
             format!("failed to write file at {:?}", path)
         })?;
         Ok(())
@@ -95,12 +95,12 @@ impl Object {
         Ok(())
     }
 
-    pub fn generate_js(&self) -> String {
-        let mut js = js::Js::default();
-        js.nodejs = self.nodejs;
-        js.debug = self.debug;
-        js.generate_program(&self.program);
-        js.to_string()
+    pub fn generate_ts(&self) -> String {
+        let mut ts = ts::Js::default();
+        ts.nodejs = self.nodejs;
+        ts.debug = self.debug;
+        ts.generate_program(&self.program, &self.module);
+        ts.to_string(&self.module)
     }
 }
 
diff --git a/crates/wasm-bindgen-cli-support/src/ts.rs b/crates/wasm-bindgen-cli-support/src/ts.rs
new file mode 100644
index 00000000..385b57d2
--- /dev/null
+++ b/crates/wasm-bindgen-cli-support/src/ts.rs
@@ -0,0 +1,795 @@
+use std::collections::HashSet;
+
+use shared;
+use parity_wasm::elements::*;
+
+#[derive(Default)]
+pub struct Js {
+    globals: String,
+    exposed_globals: HashSet<&'static str>,
+    exports: Vec<(String, String, String)>,
+    classes: Vec<String>,
+    imports: Vec<(String, String, String)>,
+    pub nodejs: bool,
+    pub debug: bool,
+}
+
+impl Js {
+
+    pub fn generate_program(&mut self,
+                            program: &shared::Program,
+                            _wasm: &Module) {
+        for f in program.free_functions.iter() {
+            self.generate_free_function(f);
+        }
+        for s in program.structs.iter() {
+            self.generate_struct(s);
+        }
+        for s in program.imports.iter() {
+            self.generate_import(s);
+        }
+    }
+
+    pub fn generate_free_function(&mut self, func: &shared::Function) {
+        let (js, ts) = self.generate_function("function",
+                                              &func.name,
+                                              &func.name,
+                                              false,
+                                              &func.arguments,
+                                              func.ret.as_ref());
+
+        self.exports.push((func.name.clone(), js, ts));
+    }
+
+    pub fn generate_struct(&mut self, s: &shared::Struct) {
+        let mut dst = String::new();
+        self.expose_check_token();
+        self.expose_wasm_exports();
+        dst.push_str(&format!("
+            export class {} {{
+                constructor(public __wasmPtr: number, sym: Symbol) {{
+                    _checkToken(sym);
+                }}
+
+                free(): void {{
+                    const ptr = this.__wasmPtr;
+                    this.__wasmPtr = 0;
+                    wasm_exports.{}(ptr);
+                }}
+        ", s.name, s.free_function()));
+
+        for function in s.functions.iter() {
+            let (js, _ts) = self.generate_function(
+                "static",
+                &function.name,
+                &function.struct_function_export_name(&s.name),
+                false,
+                &function.arguments,
+                function.ret.as_ref(),
+            );
+            dst.push_str(&js);
+            dst.push_str("\n");
+        }
+        for method in s.methods.iter() {
+            let (js, _ts) = self.generate_function(
+                "",
+                &method.function.name,
+                &method.function.struct_function_export_name(&s.name),
+                true,
+                &method.function.arguments,
+                method.function.ret.as_ref(),
+            );
+            dst.push_str(&js);
+            dst.push_str("\n");
+        }
+        dst.push_str("}\n");
+        self.classes.push(dst);
+
+        let ts_export = format!("{0}: typeof {0};", s.name);
+        self.exports.push((s.name.clone(), s.name.clone(), ts_export));
+    }
+
+    fn generate_function(&mut self,
+                         prefix: &str,
+                         name: &str,
+                         wasm_name: &str,
+                         is_method: bool,
+                         arguments: &[shared::Type],
+                         ret: Option<&shared::Type>) -> (String, String) {
+        let mut dst = format!("{}(", name);
+        let mut passed_args = String::new();
+        let mut arg_conversions = String::new();
+        let mut destructors = String::new();
+
+        if is_method {
+            passed_args.push_str("this.__wasmPtr");
+        }
+
+        for (i, arg) in arguments.iter().enumerate() {
+            let name = format!("arg{}", i);
+            if i > 0 {
+                dst.push_str(", ");
+            }
+            dst.push_str(&name);
+            dst.push_str(": ");
+
+            let mut pass = |arg: &str| {
+                if passed_args.len() > 0 {
+                    passed_args.push_str(", ");
+                }
+                passed_args.push_str(arg);
+            };
+            match *arg {
+                shared::Type::Number => {
+                    dst.push_str("number");
+                    self.expose_assert_num();
+                    arg_conversions.push_str(&format!("_assertNum({});\n", name));
+                    pass(&name)
+                }
+                shared::Type::BorrowedStr |
+                shared::Type::String => {
+                    dst.push_str("string");
+                    self.expose_pass_string_to_wasm();
+                    arg_conversions.push_str(&format!("\
+                        const [ptr{i}, len{i}] = passStringToWasm({arg});
+                    ", i = i, arg = name));
+                    pass(&format!("ptr{}", i));
+                    pass(&format!("len{}", i));
+                    if let shared::Type::BorrowedStr = *arg {
+                        self.expose_wasm_exports();
+                        destructors.push_str(&format!("\n\
+                            wasm_exports.__wbindgen_free(ptr{i}, len{i});\n\
+                        ", i = i));
+                    }
+                }
+                shared::Type::ByRef(ref s) |
+                shared::Type::ByMutRef(ref s) => {
+                    dst.push_str(s);
+                    self.expose_assert_class();
+                    arg_conversions.push_str(&format!("\
+                        const ptr{i} = _assertClass({arg}, {struct_});
+                    ", i = i, arg = name, struct_ = s));
+                    pass(&format!("ptr{}", i));
+                }
+                shared::Type::ByValue(ref s) => {
+                    dst.push_str(s);
+                    self.expose_assert_class();
+                    arg_conversions.push_str(&format!("\
+                        const ptr{i} = _assertClass({arg}, {struct_});
+                        {arg}.__wasmPtr = 0;
+                    ", i = i, arg = name, struct_ = s));
+                    pass(&format!("ptr{}", i));
+                }
+                shared::Type::JsObject => {
+                    dst.push_str("any");
+                    self.expose_add_heap_object();
+                    arg_conversions.push_str(&format!("\
+                        const idx{i} = addHeapObject({arg});
+                    ", i = i, arg = name));
+                    pass(&format!("idx{}", i));
+                }
+                shared::Type::JsObjectRef => {
+                    dst.push_str("any");
+                    self.expose_borrowed_objects();
+                    arg_conversions.push_str(&format!("\
+                        const idx{i} = addBorrowedObject({arg});
+                    ", i = i, arg = name));
+                    destructors.push_str("popBorrowedObject();\n");
+                    pass(&format!("idx{}", i));
+                }
+            }
+        }
+        dst.push_str("): ");
+        let convert_ret = match ret {
+            None => {
+                dst.push_str("void");
+                format!("return ret;")
+            }
+            Some(&shared::Type::Number) => {
+                dst.push_str("number");
+                format!("return ret;")
+            }
+            Some(&shared::Type::JsObject) => {
+                dst.push_str("any");
+                self.expose_take_object();
+                format!("return takeObject(ret);")
+            }
+            Some(&shared::Type::JsObjectRef) |
+            Some(&shared::Type::BorrowedStr) |
+            Some(&shared::Type::ByMutRef(_)) |
+            Some(&shared::Type::ByRef(_)) => panic!(),
+            Some(&shared::Type::ByValue(ref name)) => {
+                dst.push_str(name);
+                format!("\
+                    return new {name}(ret, token);
+                ", name = name)
+            }
+            Some(&shared::Type::String) => {
+                dst.push_str("string");
+                self.expose_get_string_from_wasm();
+                self.expose_wasm_exports();
+                format!("
+                    const ptr = wasm_exports.__wbindgen_boxed_str_ptr(ret);
+                    const len = wasm_exports.__wbindgen_boxed_str_len(ret);
+                    const realRet = getStringFromWasm(ptr, len);
+                    wasm_exports.__wbindgen_boxed_str_free(ret);
+                    return realRet;
+                ")
+            }
+        };
+        let mut dst_ts = dst.clone();
+        dst_ts.push_str(";");
+        dst.push_str(" {\n        ");
+        dst.push_str(&arg_conversions);
+        self.expose_wasm_exports();
+        if destructors.len() == 0 {
+            dst.push_str(&format!("\
+                const ret = wasm_exports.{f}({passed});
+                {convert_ret}
+            ", f = wasm_name, passed = passed_args, convert_ret = convert_ret));
+        } else {
+            dst.push_str(&format!("\
+                try {{
+                    const ret = wasm_exports.{f}({passed});
+                    {convert_ret}
+                }} finally {{
+                    {destructors}
+                }}
+            ", f = wasm_name, passed = passed_args, destructors = destructors,
+                convert_ret = convert_ret));
+        }
+        dst.push_str("}");
+        (format!("{} {}", prefix, dst), dst_ts)
+    }
+
+    pub fn generate_import(&mut self, import: &shared::Function) {
+        let mut dst = String::new();
+        let mut ts_dst = String::new();
+
+        dst.push_str(&format!("function {0}_shim(", import.name));
+
+        ts_dst.push_str(&import.name);
+        ts_dst.push_str("(");
+
+        let mut invocation = String::new();
+        for (i, arg) in import.arguments.iter().enumerate() {
+            if invocation.len() > 0 {
+                invocation.push_str(", ");
+            }
+            if i > 0 {
+                dst.push_str(", ");
+                ts_dst.push_str(", ");
+            }
+            ts_dst.push_str(&format!("arg{}: ", i));
+            match *arg {
+                shared::Type::Number => {
+                    ts_dst.push_str("number");
+                    invocation.push_str(&format!("arg{}", i));
+                    dst.push_str(&format!("arg{}: number", i));
+                }
+                shared::Type::BorrowedStr => {
+                    ts_dst.push_str("string");
+                    self.expose_get_string_from_wasm();
+                    invocation.push_str(&format!("getStringFromWasm(ptr{0}, len{0})", i));
+                    dst.push_str(&format!("ptr{0}: number, len{0}: number", i));
+                }
+                shared::Type::JsObject => {
+                    ts_dst.push_str("any");
+                    self.expose_take_object();
+                    invocation.push_str(&format!("takeObject(arg{})", i));
+                    dst.push_str(&format!("arg{}: number", i));
+                }
+                shared::Type::JsObjectRef => {
+                    ts_dst.push_str("any");
+                    self.expose_get_object();
+                    invocation.push_str(&format!("getObject(arg{})", i));
+                    dst.push_str(&format!("arg{}: number", i));
+                }
+                shared::Type::String |
+                shared::Type::ByRef(_) |
+                shared::Type::ByMutRef(_) |
+                shared::Type::ByValue(_) => {
+                    panic!("unsupported type in import");
+                }
+            }
+        }
+        ts_dst.push_str("): ");
+        dst.push_str("): ");
+        match import.ret {
+            Some(shared::Type::Number) => {
+                ts_dst.push_str("number");
+                dst.push_str("number");
+            }
+            None => {
+                ts_dst.push_str("void");
+                dst.push_str("void");
+            }
+            _ => unimplemented!(),
+        }
+        ts_dst.push_str("\n");
+        dst.push_str(" {\n");
+        dst.push_str(&format!("return imports.{}({});\n}}", import.name, invocation));
+
+        self.imports.push((import.name.clone(), dst, ts_dst));
+    }
+
+    pub fn to_string(&mut self, m: &Module) -> String {
+        if self.debug {
+            self.expose_global_slab();
+            self.expose_global_stack();
+            self.exports.push(
+                (
+                    "assertHeapAndStackEmpty".to_string(),
+                    "function(): void {
+                        if (stack.length > 0)
+                            throw new Error('stack is not empty');
+                        for (let i = 0; i < slab.length; i++) {
+                            if (typeof(slab[i]) !== 'number')
+                                throw new Error('slab is not empty');
+                        }
+                    }".to_string(),
+                    "assertHeapAndStackEmpty(): void;\n".to_string(),
+                )
+            );
+        }
+
+        for class in self.classes.iter() {
+            self.globals.push_str(class);
+            self.globals.push_str("\n");
+        }
+        let mut typescript_exports = String::new();
+        let mut exports = format!("\
+            {{
+                module,
+                instance,
+        ");
+        for &(ref name, ref body, ref ts_export) in self.exports.iter() {
+            exports.push_str(name);
+            exports.push_str(": ");
+            exports.push_str(body);
+            exports.push_str(",\n");
+            typescript_exports.push_str(ts_export);
+            typescript_exports.push_str("\n");
+        }
+        exports.push_str("}");
+        let mut imports = String::new();
+        let mut typescript_imports = String::new();
+        for &(ref import, ref val, ref ts_import) in self.imports.iter() {
+            imports.push_str(import);
+            imports.push_str(":");
+            imports.push_str(val);
+            imports.push_str(",\n");
+            typescript_imports.push_str(ts_import);
+            typescript_imports.push_str("\n");
+        }
+
+        if self.wasm_import_needed("__wasm_bindgen_object_clone_ref", m) {
+            self.expose_add_heap_object();
+            self.expose_get_object();
+            imports.push_str("
+                __wasm_bindgen_object_clone_ref: function(idx: number): number {
+                    // If this object is on the stack promote it to the heap.
+                    if ((idx & 1) === 1) {
+                        return addHeapObject(getObject(idx));
+                    }
+
+                    // Otherwise if the object is on the heap just bump the
+                    // refcount and move on
+                    const val = slab[idx >> 1];
+                    if (typeof(val) === 'number')
+                        throw new Error('corrupt slab');
+                    val.cnt += 1;
+                    return idx;
+                },
+            ");
+        }
+
+        if self.wasm_import_needed("__wasm_bindgen_object_drop_ref", m) {
+            self.expose_drop_ref();
+            imports.push_str("__wasm_bindgen_object_drop_ref: dropRef,\n");
+        }
+
+        let mut writes = String::new();
+        if self.exposed_globals.contains(&"memory") {
+            writes.push_str("memory = exports.memory;\n");
+        }
+        if self.exposed_globals.contains(&"wasm_exports") {
+            writes.push_str("wasm_exports = exports;\n");
+        }
+        format!("
+            {globals}
+
+            interface WasmImportsTop {{
+                env: WasmImports,
+            }}
+
+            interface WasmImports {{
+                {wasm_imports}
+            }}
+
+            interface WasmExports {{
+                {wasm_exports}
+            }}
+
+            export interface Imports {{
+                {typescript_imports}
+            }}
+
+            export interface Exports {{
+                module: WebAssembly.Module;
+                instance: WebAssembly.Module;
+                {typescript_exports}
+            }}
+
+            function xform(obj: WebAssembly.ResultObject): Exports {{
+                let {{ module, instance }} = obj;
+                let {{ exports }} = instance;
+                {writes}
+                return {exports};
+            }}
+            export function instantiate(bytes: any, imports: Imports): Promise<Exports> {{
+                let wasm_imports: WasmImportsTop = {{
+                    env: {{
+                        {imports}
+                    }},
+                }};
+                return WebAssembly.instantiate(bytes, wasm_imports).then(xform);
+            }}
+        ",
+            globals = self.globals,
+            exports = exports,
+            imports = imports,
+            writes = writes,
+            typescript_imports = typescript_imports,
+            typescript_exports = typescript_exports,
+            wasm_imports = self.typescript_wasm_imports(m),
+            wasm_exports = self.typescript_wasm_exports(m),
+        )
+    }
+
+    fn wasm_import_needed(&self, name: &str, m: &Module) -> bool {
+        let imports = match m.import_section() {
+            Some(s) => s,
+            None => return false,
+        };
+
+        imports.entries().iter().any(|i| {
+            i.module() == "env" && i.field() == name
+        })
+    }
+
+    fn typescript_wasm_imports(&self, m: &Module) -> String {
+        let imports = match m.import_section() {
+            Some(s) => s,
+            None => return String::new(),
+        };
+        let types = match m.type_section() {
+            Some(s) => s,
+            None => return String::new(),
+        };
+
+        let mut ts = String::new();
+        for import in imports.entries() {
+            assert_eq!(import.module(), "env");
+
+            let ty = match *import.external() {
+                External::Function(i) => {
+                    match types.types()[i as usize] {
+                        Type::Function(ref t) => t,
+                    }
+                }
+                _ => continue,
+            };
+
+            ts.push_str(import.field());
+            ts.push_str("(");
+            // TODO: probably match `arg` to catch exhaustive errors in the
+            // future
+            for (i, _arg) in ty.params().iter().enumerate() {
+                if i > 0 {
+                    ts.push_str(", ");
+                }
+                ts.push_str(&format!("arg{}: number", i));
+            }
+            ts.push_str("): ");
+            if ty.return_type().is_none() {
+                ts.push_str("void");
+            } else {
+                ts.push_str("number");
+            }
+            ts.push_str(";\n");
+        }
+        return ts;
+    }
+
+    fn typescript_wasm_exports(&self, m: &Module) -> String {
+        let imported_functions = match m.import_section() {
+            Some(s) => s.functions(),
+            None => 0,
+        };
+        let functions = match m.function_section() {
+            Some(s) => s,
+            None => return String::new(),
+        };
+        let types = match m.type_section() {
+            Some(s) => s,
+            None => return String::new(),
+        };
+        let exports = match m.export_section() {
+            Some(s) => s,
+            None => return String::new(),
+        };
+
+        let mut ts = String::new();
+        for export in exports.entries() {
+            let fn_idx = match *export.internal() {
+                Internal::Function(i) => i as usize,
+                _ => continue,
+            };
+            assert!(fn_idx >= imported_functions);
+            let function = &functions.entries()[fn_idx - imported_functions];
+            let ty = match types.types()[function.type_ref() as usize] {
+                Type::Function(ref t) => t,
+            };
+
+            ts.push_str(export.field());
+            ts.push_str("(");
+            // TODO: probably match `arg` to catch exhaustive errors in the
+            // future
+            for (i, _arg) in ty.params().iter().enumerate() {
+                if i > 0 {
+                    ts.push_str(", ");
+                }
+                ts.push_str(&format!("arg{}: number", i));
+            }
+            ts.push_str("): ");
+            if ty.return_type().is_none() {
+                ts.push_str("void");
+            } else {
+                ts.push_str("number");
+            }
+            ts.push_str(";\n");
+        }
+        return ts;
+    }
+
+    fn expose_drop_ref(&mut self) {
+        if !self.exposed_globals.insert("drop_ref") {
+            return
+        }
+        self.expose_global_slab();
+        self.expose_global_slab_next();
+        self.globals.push_str("
+            function dropRef(idx: number): void {
+                if ((idx & 1) == 1)
+                    throw new Error('cannot drop ref of stack objects');
+
+                // Decrement our refcount, but if it's still larger than one
+                // keep going
+                let obj = slab[idx >> 1];
+                if (typeof(obj) === 'number')
+                    throw new Error('corrupt slab');
+                obj.cnt -= 1;
+                if (obj.cnt > 0)
+                    return;
+
+                // If we hit 0 then free up our space in the slab
+                slab[idx >> 1] = slab_next;
+                slab_next = idx >> 1;
+            }
+        ");
+    }
+
+    fn expose_global_stack(&mut self) {
+        if !self.exposed_globals.insert("stack") {
+            return
+        }
+        self.globals.push_str("
+            let stack: any[] = [];
+        ");
+    }
+
+    fn expose_global_slab(&mut self) {
+        if !self.exposed_globals.insert("slab") {
+            return
+        }
+        self.globals.push_str("
+            let slab: ({ obj: any, cnt: number } | number)[] = [];
+        ");
+    }
+
+    fn expose_global_slab_next(&mut self) {
+        if !self.exposed_globals.insert("slab_next") {
+            return
+        }
+        self.globals.push_str("
+            let slab_next: number = 0;
+        ");
+    }
+
+    fn expose_get_object(&mut self) {
+        if !self.exposed_globals.insert("get_object") {
+            return
+        }
+        self.expose_global_stack();
+        self.expose_global_slab();
+        self.globals.push_str("
+            function getObject(idx: number): any {
+                if ((idx & 1) === 1) {
+                    return stack[idx >> 1];
+                } else {
+                    const val = slab[idx >> 1];
+                    if (typeof(val) === 'number')
+                        throw new Error('corrupt slab');
+                    return val.obj;
+                }
+            }
+        ");
+    }
+
+    fn expose_global_memory(&mut self) {
+        if !self.exposed_globals.insert("memory") {
+            return
+        }
+        self.globals.push_str("let memory: WebAssembly.Memory;\n");
+    }
+
+    fn expose_wasm_exports(&mut self) {
+        if !self.exposed_globals.insert("wasm_exports") {
+            return
+        }
+        self.globals.push_str("let wasm_exports: WasmExports;\n");
+    }
+
+    fn expose_check_token(&mut self) {
+        if !self.exposed_globals.insert("check_token") {
+            return
+        }
+        self.globals.push_str("\
+            const token = Symbol('foo');
+            function _checkToken(sym: Symbol): void {
+                if (token !== sym)
+                    throw new Error('cannot invoke `new` directly');
+            }
+        ");
+    }
+
+    fn expose_assert_num(&mut self) {
+        if !self.exposed_globals.insert("assert_num") {
+            return
+        }
+        self.globals.push_str("\
+            function _assertNum(n: number): void {
+                if (typeof(n) !== 'number')
+                    throw new Error('expected a number argument');
+            }
+        ");
+    }
+
+    fn expose_pass_string_to_wasm(&mut self) {
+        if !self.exposed_globals.insert("pass_string_to_wasm") {
+            return
+        }
+        self.expose_wasm_exports();
+        self.expose_global_memory();
+        if self.nodejs {
+            self.globals.push_str("
+                function passStringToWasm(arg: string): [number, number] {
+                    if (typeof(arg) !== 'string')
+                        throw new Error('expected a string argument');
+                    const buf = Buffer.from(arg);
+                    const len = buf.length;
+                    const ptr = wasm_exports.__wbindgen_malloc(len);
+                    buf.copy(Buffer.from(memory.buffer), ptr);
+                    return [ptr, len];
+                }
+            ");
+        } else {
+            self.globals.push_str("
+                function passStringToWasm(arg: string): [number, number] {
+                    if (typeof(arg) !== 'string')
+                        throw new Error('expected a string argument');
+                    const buf = new TextEncoder('utf-8').encode(arg);
+                    const len = buf.length;
+                    const ptr = wasm_exports.__wbindgen_malloc(len);
+                    let array = new Uint8Array(memory.buffer);
+                    array.set(buf, ptr);
+                    return [ptr, len];
+                }
+            ");
+        }
+    }
+
+    fn expose_get_string_from_wasm(&mut self) {
+        if !self.exposed_globals.insert("get_string_from_wasm") {
+            return
+        }
+        if self.nodejs {
+            self.expose_global_memory();
+            self.globals.push_str("
+                function getStringFromWasm(ptr: number, len: number): string {
+                    const buf = Buffer.from(memory.buffer).slice(ptr, ptr + len);
+                    const ret = buf.toString();
+                    return ret;
+                }
+            ");
+        } else {
+            self.expose_global_memory();
+            self.globals.push_str("
+                function getStringFromWasm(ptr: number, len: number): string {
+                    const mem = new Uint8Array(memory.buffer);
+                    const slice = mem.slice(ptr, ptr + len);
+                    const ret = new TextDecoder('utf-8').decode(slice);
+                    return ret;
+                }
+            ");
+        }
+    }
+
+    fn expose_assert_class(&mut self) {
+        if !self.exposed_globals.insert("assert_class") {
+            return
+        }
+        self.globals.push_str("
+            function _assertClass(instance: any, klass: any) {
+                if (!(instance instanceof klass))
+                    throw new Error(`expected instance of ${klass.name}`);
+                return instance.__wasmPtr;
+            }
+        ");
+    }
+
+    fn expose_borrowed_objects(&mut self) {
+        if !self.exposed_globals.insert("borrowed_objects") {
+            return
+        }
+        self.expose_global_stack();
+        self.globals.push_str("
+            function addBorrowedObject(obj: any): number {
+                stack.push(obj);
+                return ((stack.length - 1) << 1) | 1;
+            }
+
+            function popBorrowedObject(): void {
+                stack.pop();
+            }
+        ");
+    }
+
+    fn expose_take_object(&mut self) {
+        if !self.exposed_globals.insert("take_object") {
+            return
+        }
+        self.expose_get_object();
+        self.expose_drop_ref();
+        self.globals.push_str("
+            function takeObject(idx: number): any {
+                const ret = getObject(idx);
+                dropRef(idx);
+                return ret;
+            }
+        ");
+    }
+
+    fn expose_add_heap_object(&mut self) {
+        if !self.exposed_globals.insert("add_heap_object") {
+            return
+        }
+        self.expose_global_slab();
+        self.expose_global_slab_next();
+        self.globals.push_str("
+            function addHeapObject(obj: any): number {
+                if (slab_next == slab.length) {
+                    slab.push(slab.length + 1);
+                }
+                const idx = slab_next;
+                const next = slab[idx];
+                if (typeof(next) !== 'number')
+                    throw new Error('corrupt slab');
+                slab_next = next;
+                slab[idx] = { obj, cnt: 1 };
+                return idx << 1;
+            }
+        ");
+    }
+}
diff --git a/crates/wasm-bindgen-cli/src/main.rs b/crates/wasm-bindgen-cli/src/main.rs
index cabe1238..93dc9543 100644
--- a/crates/wasm-bindgen-cli/src/main.rs
+++ b/crates/wasm-bindgen-cli/src/main.rs
@@ -16,14 +16,14 @@ Usage:
 
 Options:
     -h --help               Show this screen.
-    --output-js FILE        Output JS file
+    --output-ts FILE        Output TypeScript file
     --output-wasm FILE      Output WASM file
     --nodejs                Generate output for node.js, not the browser
 ";
 
 #[derive(Debug, Deserialize)]
 struct Args {
-    flag_output_js: Option<PathBuf>,
+    flag_output_ts: Option<PathBuf>,
     flag_output_wasm: Option<PathBuf>,
     flag_nodejs: bool,
     arg_input: PathBuf,
@@ -38,10 +38,10 @@ fn main() {
     b.input_path(&args.arg_input);
     b.nodejs(args.flag_nodejs);
     let ret = b.generate().expect("failed to generate bindings");
-    if let Some(ref js) = args.flag_output_js {
-        ret.write_js_to(js).expect("failed to write JS output file");
+    if let Some(ref ts) = args.flag_output_ts {
+        ret.write_ts_to(ts).expect("failed to write TypeScript output file");
     } else {
-        println!("{}", ret.generate_js());
+        println!("{}", ret.generate_ts());
     }
     if let Some(ref wasm) = args.flag_output_wasm {
         ret.write_wasm_to(wasm).expect("failed to write wasm output file");
diff --git a/tests/classes.rs b/tests/classes.rs
index 7443ce45..203ef308 100644
--- a/tests/classes.rs
+++ b/tests/classes.rs
@@ -31,10 +31,13 @@ fn simple() {
                 }
             }
         "#)
-        .file("test.js", r#"
+        .file("test.ts", r#"
             import * as assert from "assert";
+            import { Exports, Imports } from "./out";
 
-            export function test(wasm) {
+            export const imports: Imports = {};
+
+            export function test(wasm: Exports) {
                 const r = wasm.Foo.new();
                 assert.strictEqual(r.add(0), 0);
                 assert.strictEqual(r.add(1), 1);
@@ -91,10 +94,13 @@ fn strings() {
                 }
             }
         "#)
-        .file("test.js", r#"
+        .file("test.ts", r#"
             import * as assert from "assert";
+            import { Exports, Imports } from "./out";
 
-            export function test(wasm) {
+            export const imports: Imports = {};
+
+            export function test(wasm: Exports) {
                 const r = wasm.Foo.new();
                 r.set(3);
                 let bar = r.bar('baz');
@@ -143,9 +149,10 @@ fn exceptions() {
             }
         "#)
         .file("test.js", r#"
-            import * as assert from "assert";
+            var assert = require("assert");
 
-            export function test(wasm) {
+            exports.imports = {};
+            exports.test = function(wasm) {
                 assert.throws(() => new wasm.A(), /cannot invoke `new` directly/);
                 let a = wasm.A.new();
                 a.free();
@@ -161,6 +168,63 @@ fn exceptions() {
                 assert.throws(() => c.foo(d), /expected instance of A/);
                 d.free();
                 c.free();
+            };
+        "#)
+        .file("test.d.ts", r#"
+            import { Exports, Imports } from "./out";
+
+            export const imports: Imports;
+
+            export function test(wasm: Exports): void;
+        "#)
+        .test();
+}
+
+#[test]
+fn pass_one_to_another() {
+    test_support::project()
+        .file("src/lib.rs", r#"
+            #![feature(proc_macro)]
+
+            extern crate wasm_bindgen;
+
+            use wasm_bindgen::prelude::*;
+
+            wasm_bindgen! {
+                pub struct A {}
+
+                impl A {
+                    pub fn new() -> A {
+                        A {}
+                    }
+
+                    pub fn foo(&self, _other: &B) {
+                    }
+
+                    pub fn bar(&self, _other: B) {
+                    }
+                }
+
+                pub struct B {}
+
+                impl B {
+                    pub fn new() -> B {
+                        B {}
+                    }
+                }
+            }
+        "#)
+        .file("test.ts", r#"
+            import { Exports, Imports } from "./out";
+
+            export const imports: Imports = {};
+
+            export function test(wasm: Exports) {
+                let a = wasm.A.new();
+                let b = wasm.B.new();
+                a.foo(b);
+                a.bar(b);
+                a.free();
             }
         "#)
         .test();
diff --git a/tests/imports.rs b/tests/imports.rs
index 5bfc72a8..4a54ce6b 100644
--- a/tests/imports.rs
+++ b/tests/imports.rs
@@ -23,29 +23,28 @@ fn simple() {
                 }
             }
         "#)
-        .file("test.js", r#"
+        .file("test.ts", r#"
+            import { Exports, Imports } from "./out";
             import * as assert from "assert";
 
-            let ARG = null;
-            let ANOTHER_ARG = null;
+            let ARG: string | null = null;
+            let ANOTHER_ARG: number | null = null;
 
-            export const imports = {
-                env: {
-                    foo(s) {
-                        assert.strictEqual(ARG, null);
-                        assert.strictEqual(s, "foo");
-                        ARG = s;
-                    },
-                    another(s) {
-                        assert.strictEqual(ANOTHER_ARG, null);
-                        assert.strictEqual(s, 21);
-                        ANOTHER_ARG = s;
-                        return 35;
-                    },
+            export const imports: Imports = {
+                foo(s) {
+                    assert.strictEqual(ARG, null);
+                    assert.strictEqual(s, "foo");
+                    ARG = s;
+                },
+                another(s) {
+                    assert.strictEqual(ANOTHER_ARG, null);
+                    assert.strictEqual(s, 21);
+                    ANOTHER_ARG = s;
+                    return 35;
                 },
             };
 
-            export function test(wasm) {
+            export function test(wasm: Exports) {
                 assert.strictEqual(ARG, null);
                 wasm.bar("foo");
                 assert.strictEqual(ARG, "foo");
diff --git a/tests/jsobjects.rs b/tests/jsobjects.rs
index 0faef599..6db5219f 100644
--- a/tests/jsobjects.rs
+++ b/tests/jsobjects.rs
@@ -19,21 +19,20 @@ fn simple() {
                 }
             }
         "#)
-        .file("test.js", r#"
+        .file("test.ts", r#"
+            import { Exports, Imports } from "./out";
             import * as assert from "assert";
 
-            let ARG = null;
+            let ARG: string | null = null;
 
-            export const imports = {
-                env: {
-                    foo(s) {
-                        assert.strictEqual(ARG, null);
-                        ARG = s;
-                    },
+            export const imports: Imports = {
+                foo(s) {
+                    assert.strictEqual(ARG, null);
+                    ARG = s;
                 },
             };
 
-            export function test(wasm) {
+            export function test(wasm: Exports) {
                 assert.strictEqual(ARG, null);
                 let sym = Symbol('test');
                 wasm.bar(sym);
@@ -62,21 +61,20 @@ fn owned() {
                 }
             }
         "#)
-        .file("test.js", r#"
+        .file("test.ts", r#"
+            import { Exports, Imports } from "./out";
             import * as assert from "assert";
 
-            let ARG = null;
+            let ARG: Symbol | null = null;
 
-            export const imports = {
-                env: {
-                    foo(s) {
-                        assert.strictEqual(ARG, null);
-                        ARG = s;
-                    },
+            export const imports: Imports = {
+                foo(s) {
+                    assert.strictEqual(ARG, null);
+                    ARG = s;
                 },
             };
 
-            export function test(wasm) {
+            export function test(wasm: Exports) {
                 assert.strictEqual(ARG, null);
                 let sym = Symbol('test');
                 wasm.bar(sym);
@@ -114,22 +112,21 @@ fn clone() {
                 }
             }
         "#)
-        .file("test.js", r#"
+        .file("test.ts", r#"
+            import { Exports, Imports } from "./out";
             import * as assert from "assert";
 
             let ARG = Symbol('test');
 
-            export const imports = {
-                env: {
-                    foo1(s) { assert.strictEqual(s, ARG); },
-                    foo2(s) { assert.strictEqual(s, ARG); },
-                    foo3(s) { assert.strictEqual(s, ARG); },
-                    foo4(s) { assert.strictEqual(s, ARG); },
-                    foo5(s) { assert.strictEqual(s, ARG); },
-                },
+            export const imports: Imports = {
+                foo1(s) { assert.strictEqual(s, ARG); },
+                foo2(s) { assert.strictEqual(s, ARG); },
+                foo3(s) { assert.strictEqual(s, ARG); },
+                foo4(s) { assert.strictEqual(s, ARG); },
+                foo5(s) { assert.strictEqual(s, ARG); },
             };
 
-            export function test(wasm) {
+            export function test(wasm: Exports) {
                 wasm.bar(ARG);
             }
         "#)
@@ -162,21 +159,20 @@ fn promote() {
                 }
             }
         "#)
-        .file("test.js", r#"
+        .file("test.ts", r#"
+            import { Exports, Imports } from "./out";
             import * as assert from "assert";
 
             let ARG = Symbol('test');
 
-            export const imports = {
-                env: {
-                    foo1(s) { assert.strictEqual(s, ARG); },
-                    foo2(s) { assert.strictEqual(s, ARG); },
-                    foo3(s) { assert.strictEqual(s, ARG); },
-                    foo4(s) { assert.strictEqual(s, ARG); },
-                },
+            export const imports: Imports = {
+                foo1(s) { assert.strictEqual(s, ARG); },
+                foo2(s) { assert.strictEqual(s, ARG); },
+                foo3(s) { assert.strictEqual(s, ARG); },
+                foo4(s) { assert.strictEqual(s, ARG); },
             };
 
-            export function test(wasm) {
+            export function test(wasm: Exports) {
                 wasm.bar(ARG);
             }
         "#)
diff --git a/tests/simple.rs b/tests/simple.rs
index 74eedfad..659e4584 100644
--- a/tests/simple.rs
+++ b/tests/simple.rs
@@ -24,10 +24,13 @@ fn add() {
                 }
             }
         "#)
-        .file("test.js", r#"
+        .file("test.ts", r#"
             import * as assert from "assert";
+            import { Exports, Imports } from "./out";
 
-            export function test(wasm) {
+            export const imports: Imports = {};
+
+            export function test(wasm: Exports) {
                 assert.strictEqual(wasm.add(1, 2), 3);
                 assert.strictEqual(wasm.add(2, 3), 5);
                 assert.strictEqual(wasm.add3(2), 5);
@@ -58,8 +61,12 @@ fn string_arguments() {
                 }
             }
         "#)
-        .file("test.js", r#"
-            export function test(wasm) {
+        .file("test.ts", r#"
+            import { Exports, Imports } from "./out";
+
+            export const imports: Imports = {};
+
+            export function test(wasm: Exports) {
                 wasm.assert_foo("foo");
                 wasm.assert_foo_and_bar("foo2", "bar");
             }
@@ -89,10 +96,13 @@ fn return_a_string() {
                 }
             }
         "#)
-        .file("test.js", r#"
+        .file("test.ts", r#"
             import * as assert from "assert";
+            import { Exports, Imports } from "./out";
 
-            export function test(wasm) {
+            export const imports: Imports = {};
+
+            export function test(wasm: Exports) {
                 assert.strictEqual(wasm.clone("foo"), "foo");
                 assert.strictEqual(wasm.clone("another"), "another");
                 assert.strictEqual(wasm.concat("a", "b", 3), "a b 3");
@@ -118,12 +128,20 @@ fn exceptions() {
             }
         "#)
         .file("test.js", r#"
-            import * as assert from "assert";
+            var assert = require("assert");
 
-            export function test(wasm) {
+            exports.imports = {};
+            exports.test = function(wasm) {
                 assert.throws(() => wasm.foo('a'), /expected a number argument/);
                 assert.throws(() => wasm.bar(3), /expected a string argument/);
-            }
+            };
+        "#)
+        .file("test.d.ts", r#"
+            import { Exports, Imports } from "./out";
+
+            export const imports: Imports;
+
+            export function test(wasm: Exports): void;
         "#)
         .test();
 }