In the discussion on #328 the consensus seems to be that people like type definitions. This moves toward generating types using jsdoc as suggested by @bollwyvl in that thread.

I don't have much Typescript experience so I would very much appreciate it if people could test them out. The types are available as CircleCI artifacts here:
https://3914-322074228-gh.circle-artifacts.com/0/root/repo/build/pyodide.d.ts

@bollwyvl said that

> types that aren't tested are probably worse than no types at all

If anyone could contribute tests, that would be very helpful (I don't know what they should look like). 

The main limitation I ran into with this approach is that Typescript supports Typescript types in jsdoc annotations but jsdoc does not. This means that if we want to automatically generate docs, we have to stick to the much more limited syntax that jsdoc supports. For example, `isPyProxy` should be typed `obj is PyProxy` but this approach does not allow that as far as I can tell. 
Some basic types. In the discussion on #328 the consensus seems to be that people like type definitions. This moves toward generating types using jsdoc as suggested by @bollwyvl in that thread.

I don't have much Typescript experience so I would very much appreciate it if people could test them out. The types are available as CircleCI artifacts here:
https://3914-322074228-gh.circle-artifacts.com/0/root/repo/build/pyodide.d.ts

@bollwyvl said that

> types that aren't tested are probably worse than no types at all

If anyone could contribute tests, that would be very helpful (I don't know what they should look like). 

The main limitation I ran into with this approach is that Typescript supports Typescript types in jsdoc annotations but jsdoc does not. This means that if we want to automatically generate docs, we have to stick to the much more limited syntax that jsdoc supports. For example, `isPyProxy` should be typed `obj is PyProxy` but this approach does not allow that as far as I can tell. 
import pytest
from pathlib import Path
import sys

from conftest import selenium_common

sys.path.append(str(Path(__file__).resolve().parents[2] / "src" / "py"))

from pyodide import console  # noqa: E402


def test_stream_redirection():
    my_buffer = ""

    def callback(string):
        nonlocal my_buffer
        my_buffer += string

    my_stream = console._StdStream(callback)

    print("foo", file=my_stream)
    assert my_buffer == "foo\n"
    print("bar", file=my_stream)
    assert my_buffer == "foo\nbar\n"


@pytest.fixture
def safe_sys_redirections():
    redirected = sys.stdout, sys.stderr, sys.displayhook
    try:
        yield
    finally:
        sys.stdout, sys.stderr, sys.displayhook = redirected


def test_interactive_console_streams(safe_sys_redirections):

    my_stdout = ""
    my_stderr = ""

    def stdout_callback(string):
        nonlocal my_stdout
        my_stdout += string

    def stderr_callback(string):
        nonlocal my_stderr
        my_stderr += string

    ##########################
    # Persistent redirection #
    ##########################
    shell = console._InteractiveConsole(
        stdout_callback=stdout_callback,
        stderr_callback=stderr_callback,
        persistent_stream_redirection=True,
    )

    # std names
    assert sys.stdout.name == "<stdout>"
    assert sys.stderr.name == "<stderr>"

    # std redirections
    print("foo")
    assert my_stdout == "foo\n"
    print("bar", file=sys.stderr)
    assert my_stderr == "bar\n"

    shell.push("print('foobar')")
    assert my_stdout == "foo\nfoobar\n"

    shell.push("print('foobar')")
    assert my_stdout == "foo\nfoobar\nfoobar\n"

    shell.push("1+1")
    assert my_stdout == "foo\nfoobar\nfoobar\n2\n"
    assert shell.run_complete.result() == 2

    my_stderr = ""
    shell.push("raise Exception('hi')")
    assert (
        my_stderr
        == 'Traceback (most recent call last):\n  File "<console>", line 1, in <module>\nException: hi\n'
    )
    assert shell.run_complete.exception() is not None
    my_stderr = ""
    shell.push("1+1")
    assert my_stderr == ""
    assert shell.run_complete.result() == 2

    shell.restore_stdstreams()

    my_stdout = ""
    my_stderr = ""

    print("bar")
    assert my_stdout == ""

    print("foo", file=sys.stdout)
    assert my_stderr == ""

    ##############################
    # Non persistent redirection #
    ##############################
    shell = console._InteractiveConsole(
        stdout_callback=stdout_callback,
        stderr_callback=stderr_callback,
        persistent_stream_redirection=False,
    )

    print("foo")
    assert my_stdout == ""

    shell.push("print('foobar')")
    assert my_stdout == "foobar\n"

    print("bar")
    assert my_stdout == "foobar\n"

    shell.push("print('foobar')")
    assert my_stdout == "foobar\nfoobar\n"

    shell.push("import sys")
    shell.push("print('foobar', file=sys.stderr)")
    assert my_stderr == "foobar\n"

    shell.push("1+1")
    assert my_stdout == "foobar\nfoobar\n2\n"


def test_repr(safe_sys_redirections):
    sep = "..."
    for string in ("x" * 10 ** 5, "x" * (10 ** 5 + 1)):
        for limit in (9, 10, 100, 101):
            assert len(
                console.repr_shorten(string, limit=limit, separator=sep)
            ) == 2 * (limit // 2) + len(sep)


@pytest.fixture
def safe_selenium_sys_redirections(selenium):
    # Import console early since it makes three global hiwire allocations, and we don't want to anger
    # the memory leak checker
    selenium.run_js("pyodide._module.runPythonSimple(`from pyodide import console`)")

    selenium.run_js(
        "pyodide._module.runPythonSimple(`import sys; _redirected = sys.stdout, sys.stderr, sys.displayhook`)"
    )
    try:
        yield
    finally:
        selenium.run_js(
            "pyodide._module.runPythonSimple(`sys.stdout, sys.stderr, sys.displayhook = _redirected`)"
        )


def test_interactive_console(selenium, safe_selenium_sys_redirections):
    selenium.run(
        """
        import sys
        from pyodide.console import _InteractiveConsole

        result = None

        def display(value):
            global result
            result = value

        shell = _InteractiveConsole()
        shell.display = display
        """
    )

    selenium.run("shell.push('x = 5')")
    selenium.run("shell.push('x')")
    selenium.run_js("await pyodide.runPython('shell.run_complete');")
    assert selenium.run("result") == 5

    selenium.run("shell.push('x ** 2')")
    selenium.run_js("await pyodide.runPython('shell.run_complete');")

    assert selenium.run("result") == 25

    selenium.run("shell.push('def f(x):')")
    selenium.run("shell.push('    return x*x + 1')")
    selenium.run("shell.push('')")
    selenium.run("shell.push('[f(x) for x in range(5)]')")
    selenium.run_js("await pyodide.runPython('shell.run_complete');")
    assert selenium.run("result") == [1, 2, 5, 10, 17]

    selenium.run("shell.push('def factorial(n):')")
    selenium.run("shell.push('    if n < 2:')")
    selenium.run("shell.push('        return 1')")
    selenium.run("shell.push('    else:')")
    selenium.run("shell.push('        return n * factorial(n - 1)')")
    selenium.run("shell.push('')")
    selenium.run("shell.push('factorial(10)')")
    selenium.run_js("await pyodide.runPython('shell.run_complete');")
    assert selenium.run("result") == 3628800

    # with package load
    selenium.run("shell.push('import pytz')")
    selenium.run("shell.push('pytz.utc.zone')")
    selenium.run_js("await pyodide.runPython('shell.run_complete');")
    assert selenium.run("result") == "UTC"


def test_completion(selenium, safe_selenium_sys_redirections):
    selenium.run(
        """
        from pyodide import console

        shell = console._InteractiveConsole()
        """
    )

    assert selenium.run(
        """
        [completions, start] = shell.complete('a')
        [tuple(completions), start]
        """
    ) == [
        [
            "and ",
            "as ",
            "assert ",
            "async ",
            "await ",
            "abs(",
            "all(",
            "any(",
            "ascii(",
        ],
        0,
    ]

    assert selenium.run(
        """
        [completions, start] = shell.complete('a = 0 ; print.__g')
        [tuple(completions), start]
        """
    ) == [
        [
            "print.__ge__(",
            "print.__getattribute__(",
            "print.__gt__(",
        ],
        8,
    ]


def test_interactive_console_top_level_await(selenium, safe_selenium_sys_redirections):
    selenium.run(
        """
        import sys
        from pyodide.console import _InteractiveConsole

        result = None

        def display(value):
            global result
            result = value

        shell = _InteractiveConsole()
        shell.display = display
        """
    )
    selenium.run("shell.push('from js import fetch')")
    selenium.run("shell.push('await (await fetch(`packages.json`)).json()')")
    assert selenium.run("result") == None


@pytest.fixture(params=["firefox", "chrome"], scope="function")
def console_html_fixture(request, web_server_main):
    with selenium_common(request, web_server_main, False) as selenium:
        selenium.driver.get(
            f"http://{selenium.server_hostname}:{selenium.server_port}/console.html"
        )
        selenium.javascript_setup()
        try:
            yield selenium
        finally:
            print(selenium.logs)


def test_console_html(console_html_fixture):
    selenium = console_html_fixture
    selenium.run_js(
        """
        await window.console_ready;
        """
    )
    result = selenium.run_js(
        r"""
        let result = [];
        assert(() => term.get_output().startsWith("Welcome to the Pyodide terminal emulator 🐍"))

        term.clear();
        term.exec("1+1");
        await term.ready;
        assert(() => term.get_output().trim() === ">>> 1+1\n2", term.get_output().trim());


        term.clear();
        term.exec("1+");
        await term.ready;
        result.push([term.get_output(),
`>>> 1+
[[;;;terminal-error]  File "<console>", line 1
    1+
     ^
SyntaxError: invalid syntax]`
        ]);

        term.clear();
        term.exec("raise Exception('hi')");
        await term.ready;
        result.push([term.get_output(),
`>>> raise Exception('hi')
[[;;;terminal-error]Traceback (most recent call last):]
[[;;;terminal-error]  File "<console>", line 1, in <module>]
[[;;;terminal-error]Exception: hi]`
        ]);

        term.clear();
        term.exec("from _pyodide_core import trigger_fatal_error; trigger_fatal_error()");
        await term.ready;
        result.push([term.get_output(),
`>>> from _pyodide_core import trigger_fatal_error; trigger_fatal_error()
[[;;;terminal-error]Pyodide has suffered a fatal error. Please report this to the Pyodide maintainers.]
[[;;;terminal-error]The cause of the fatal error was:]
[[;;;terminal-error]Error: intentionally triggered fatal error!]
[[;;;terminal-error]Look in the browser console for more details.]`
        ]);

        await sleep(30);
        assert(() => term.paused());
        return result;
        """
    )
    for [x, y] in result:
        assert x == y

import pytest
from pathlib import Path
import sys

from conftest import selenium_common

sys.path.append(str(Path(__file__).resolve().parents[2] / "src" / "py"))

from pyodide import console  # noqa: E402


def test_stream_redirection():
    my_buffer = ""

    def callback(string):
        nonlocal my_buffer
        my_buffer += string

    my_stream = console._StdStream(callback)

    print("foo", file=my_stream)
    assert my_buffer == "foo\n"
    print("bar", file=my_stream)
    assert my_buffer == "foo\nbar\n"


@pytest.fixture
def safe_sys_redirections():
    redirected = sys.stdout, sys.stderr, sys.displayhook
    try:
        yield
    finally:
        sys.stdout, sys.stderr, sys.displayhook = redirected


def test_interactive_console_streams(safe_sys_redirections):

    my_stdout = ""
    my_stderr = ""

    def stdout_callback(string):
        nonlocal my_stdout
        my_stdout += string

    def stderr_callback(string):
        nonlocal my_stderr
        my_stderr += string

    ##########################
    # Persistent redirection #
    ##########################
    shell = console._InteractiveConsole(
        stdout_callback=stdout_callback,
        stderr_callback=stderr_callback,
        persistent_stream_redirection=True,
    )

    # std names
    assert sys.stdout.name == "<stdout>"
    assert sys.stderr.name == "<stderr>"

    # std redirections
    print("foo")
    assert my_stdout == "foo\n"
    print("bar", file=sys.stderr)
    assert my_stderr == "bar\n"

    shell.push("print('foobar')")
    assert my_stdout == "foo\nfoobar\n"

    shell.push("print('foobar')")
    assert my_stdout == "foo\nfoobar\nfoobar\n"

    shell.push("1+1")
    assert my_stdout == "foo\nfoobar\nfoobar\n2\n"
    assert shell.run_complete.result() == 2

    my_stderr = ""
    shell.push("raise Exception('hi')")
    assert (
        my_stderr
        == 'Traceback (most recent call last):\n  File "<console>", line 1, in <module>\nException: hi\n'
    )
    assert shell.run_complete.exception() is not None
    my_stderr = ""
    shell.push("1+1")
    assert my_stderr == ""
    assert shell.run_complete.result() == 2

    shell.restore_stdstreams()

    my_stdout = ""
    my_stderr = ""

    print("bar")
    assert my_stdout == ""

    print("foo", file=sys.stdout)
    assert my_stderr == ""

    ##############################
    # Non persistent redirection #
    ##############################
    shell = console._InteractiveConsole(
        stdout_callback=stdout_callback,
        stderr_callback=stderr_callback,
        persistent_stream_redirection=False,
    )

    print("foo")
    assert my_stdout == ""

    shell.push("print('foobar')")
    assert my_stdout == "foobar\n"

    print("bar")
    assert my_stdout == "foobar\n"

    shell.push("print('foobar')")
    assert my_stdout == "foobar\nfoobar\n"

    shell.push("import sys")
    shell.push("print('foobar', file=sys.stderr)")
    assert my_stderr == "foobar\n"

    shell.push("1+1")
    assert my_stdout == "foobar\nfoobar\n2\n"


def test_repr(safe_sys_redirections):
    sep = "..."
    for string in ("x" * 10 ** 5, "x" * (10 ** 5 + 1)):
        for limit in (9, 10, 100, 101):
            assert len(
                console.repr_shorten(string, limit=limit, separator=sep)
            ) == 2 * (limit // 2) + len(sep)


@pytest.fixture
def safe_selenium_sys_redirections(selenium):
    # Import console early since it makes three global hiwire allocations, and we don't want to anger
    # the memory leak checker
    selenium.run_js("pyodide._module.runPythonSimple(`from pyodide import console`)")

    selenium.run_js(
        "pyodide._module.runPythonSimple(`import sys; _redirected = sys.stdout, sys.stderr, sys.displayhook`)"
    )
    try:
        yield
    finally:
        selenium.run_js(
            "pyodide._module.runPythonSimple(`sys.stdout, sys.stderr, sys.displayhook = _redirected`)"
        )


def test_interactive_console(selenium, safe_selenium_sys_redirections):
    selenium.run(
        """
        import sys
        from pyodide.console import _InteractiveConsole

        result = None

        def display(value):
            global result
            result = value

        shell = _InteractiveConsole()
        shell.display = display
        """
    )

    selenium.run("shell.push('x = 5')")
    selenium.run("shell.push('x')")
    selenium.run_js("await pyodide.runPython('shell.run_complete');")
    assert selenium.run("result") == 5

    selenium.run("shell.push('x ** 2')")
    selenium.run_js("await pyodide.runPython('shell.run_complete');")

    assert selenium.run("result") == 25

    selenium.run("shell.push('def f(x):')")
    selenium.run("shell.push('    return x*x + 1')")
    selenium.run("shell.push('')")
    selenium.run("shell.push('[f(x) for x in range(5)]')")
    selenium.run_js("await pyodide.runPython('shell.run_complete');")
    assert selenium.run("result") == [1, 2, 5, 10, 17]

    selenium.run("shell.push('def factorial(n):')")
    selenium.run("shell.push('    if n < 2:')")
    selenium.run("shell.push('        return 1')")
    selenium.run("shell.push('    else:')")
    selenium.run("shell.push('        return n * factorial(n - 1)')")
    selenium.run("shell.push('')")
    selenium.run("shell.push('factorial(10)')")
    selenium.run_js("await pyodide.runPython('shell.run_complete');")
    assert selenium.run("result") == 3628800

    # with package load
    selenium.run("shell.push('import pytz')")
    selenium.run("shell.push('pytz.utc.zone')")
    selenium.run_js("await pyodide.runPython('shell.run_complete');")
    assert selenium.run("result") == "UTC"


def test_completion(selenium, safe_selenium_sys_redirections):
    selenium.run(
        """
        from pyodide import console

        shell = console._InteractiveConsole()
        """
    )

    assert selenium.run(
        """
        [completions, start] = shell.complete('a')
        [tuple(completions), start]
        """
    ) == [
        [
            "and ",
            "as ",
            "assert ",
            "async ",
            "await ",
            "abs(",
            "all(",
            "any(",
            "ascii(",
        ],
        0,
    ]

    assert selenium.run(
        """
        [completions, start] = shell.complete('a = 0 ; print.__g')
        [tuple(completions), start]
        """
    ) == [
        [
            "print.__ge__(",
            "print.__getattribute__(",
            "print.__gt__(",
        ],
        8,
    ]


def test_interactive_console_top_level_await(selenium, safe_selenium_sys_redirections):
    selenium.run(
        """
        import sys
        from pyodide.console import _InteractiveConsole

        result = None

        def display(value):
            global result
            result = value

        shell = _InteractiveConsole()
        shell.display = display
        """
    )
    selenium.run("shell.push('from js import fetch')")
    selenium.run("shell.push('await (await fetch(`packages.json`)).json()')")
    assert selenium.run("result") == None


@pytest.fixture(params=["firefox", "chrome"], scope="function")
def console_html_fixture(request, web_server_main):
    with selenium_common(request, web_server_main, False) as selenium:
        selenium.driver.get(
            f"http://{selenium.server_hostname}:{selenium.server_port}/console.html"
        )
        selenium.javascript_setup()
        try:
            yield selenium
        finally:
            print(selenium.logs)


def test_console_html(console_html_fixture):
    selenium = console_html_fixture
    selenium.run_js(
        """
        await window.console_ready;
        """
    )
    result = selenium.run_js(
        r"""
        let result = [];
        assert(() => term.get_output().startsWith("Welcome to the Pyodide terminal emulator 🐍"))

        term.clear();
        term.exec("1+1");
        await term.ready;
        assert(() => term.get_output().trim() === ">>> 1+1\n2", term.get_output().trim());


        term.clear();
        term.exec("1+");
        await term.ready;
        result.push([term.get_output(),
`>>> 1+
[[;;;terminal-error]  File "<console>", line 1
    1+
     ^
SyntaxError: invalid syntax]`
        ]);

        term.clear();
        term.exec("raise Exception('hi')");
        await term.ready;
        result.push([term.get_output(),
`>>> raise Exception('hi')
[[;;;terminal-error]Traceback (most recent call last):]
[[;;;terminal-error]  File "<console>", line 1, in <module>]
[[;;;terminal-error]Exception: hi]`
        ]);

        term.clear();
        term.exec("from _pyodide_core import trigger_fatal_error; trigger_fatal_error()");
        await term.ready;
        result.push([term.get_output(),
`>>> from _pyodide_core import trigger_fatal_error; trigger_fatal_error()
[[;;;terminal-error]Pyodide has suffered a fatal error. Please report this to the Pyodide maintainers.]
[[;;;terminal-error]The cause of the fatal error was:]
[[;;;terminal-error]Error: intentionally triggered fatal error!]
[[;;;terminal-error]Look in the browser console for more details.]`
        ]);

        await sleep(30);
        assert(() => term.paused());
        return result;
        """
    )
    for [x, y] in result:
        assert x == y
