This PR finishes adding types (partial types were added in #1592, #1593 did some prerequisite rearrangement).

To generate the types correctly in `pyproxy.js`, I changed the protocols like `PyProxyLengthMethods` into ES6 classes (they used to just be objects). This was needed because otherwise Typescript makes the protocol into a namespace but I want it to be a type to indicate that an object has certain methods. To compensate for this change, I had to modify `getPyProxyClass` accordingly. This change required a number of syntactical changes.

I also added a bunch of predicate methods to the base `PyProxy` class to test whether the proxy supports certain extra methods. These are needed to use as Typescript type guards, but they probably are also useful by themselves when code needs to do reflection on the proxies.

I don't know whether the exposed types or the type guard predicates are well named, I would be happy for recommendations of improvements to the names -- both the type names and the predicates are public and deserve to be named with care.

JsDoc does not recognize typescript type guards or typescript type definitions, so I added a JsDoc plugin which strips out type definitions and transforms typeguards `x is y` ==> `boolean`. There's one JsDoc typescript parse error that still happens but it doesn't cause any harm to the output.

I also changed the way that we collect javascript API docs to be a bit more robust. In particular, it used to be organized by file. I changed it as follows: 
1. any API part of `globalThis` or marked with `@memberof globalThis` is treated as a global, 
2. any API of a class whose name starts with `PyProxy` is treated as a part of the `PyProxy` API,
3. anything else is assumed to be part of the pyodide API

As before, things marked `@private` are left out.

I also added tests for the type declarations using `tpd`. They are a new CI step.


ENH More complete typescript types. This PR finishes adding types (partial types were added in #1592, #1593 did some prerequisite rearrangement).

To generate the types correctly in `pyproxy.js`, I changed the protocols like `PyProxyLengthMethods` into ES6 classes (they used to just be objects). This was needed because otherwise Typescript makes the protocol into a namespace but I want it to be a type to indicate that an object has certain methods. To compensate for this change, I had to modify `getPyProxyClass` accordingly. This change required a number of syntactical changes.

I also added a bunch of predicate methods to the base `PyProxy` class to test whether the proxy supports certain extra methods. These are needed to use as Typescript type guards, but they probably are also useful by themselves when code needs to do reflection on the proxies.

I don't know whether the exposed types or the type guard predicates are well named, I would be happy for recommendations of improvements to the names -- both the type names and the predicates are public and deserve to be named with care.

JsDoc does not recognize typescript type guards or typescript type definitions, so I added a JsDoc plugin which strips out type definitions and transforms typeguards `x is y` ==> `boolean`. There's one JsDoc typescript parse error that still happens but it doesn't cause any harm to the output.

I also changed the way that we collect javascript API docs to be a bit more robust. In particular, it used to be organized by file. I changed it as follows: 
1. any API part of `globalThis` or marked with `@memberof globalThis` is treated as a global, 
2. any API of a class whose name starts with `PyProxy` is treated as a part of the `PyProxy` API,
3. anything else is assumed to be part of the pyodide API

As before, things marked `@private` are left out.

I also added tests for the type declarations using `tpd`. They are a new CI step.


JS_FILE(python2js_buffer_init, () => {
  0, 0; /* Magic, see include_js_file.h */

  /**
   * Determine type and endianness of data from format. This is a helper
   * function for converting buffers from Python to Javascript, used in
   * PyProxyBufferMethods and in `toJs` on a buffer.
   *
   * To understand this function it will be helpful to look at the tables here:
   * https://docs.python.org/3/library/struct.html#format-strings
   *
   * @arg format {String} A Python format string (caller must convert it to a
   *      Javascript string).
   * @arg errorMessage {String} Extra stuff to append to an error message if
   *      thrown. Should be a complete sentence.
   * @returns A pair, an appropriate TypedArray constructor and a boolean which
   *      is true if the format suggests a big endian array.
   * @private
   */
  Module.processBufferFormatString = function (formatStr, errorMessage = "") {
    if (formatStr.length > 2) {
      throw new Error(
        "Expected format string to have length <= 2, " +
          `got '${formatStr}'.` +
          errorMessage
      );
    }
    let formatChar = formatStr.slice(-1);
    let alignChar = formatStr.slice(0, -1);
    let bigEndian;
    switch (alignChar) {
      case "!":
      case ">":
        bigEndian = true;
        break;
      case "<":
      case "@":
      case "=":
      case "":
        bigEndian = false;
        break;
      default:
        throw new Error(
          `Unrecognized alignment character ${alignChar}.` + errorMessage
        );
    }
    let arrayType;
    switch (formatChar) {
      case "b":
        arrayType = Int8Array;
        break;
      case "s":
      case "p":
      case "c":
      case "B":
      case "?":
        arrayType = Uint8Array;
        break;
      case "h":
        arrayType = Int16Array;
        break;
      case "H":
        arrayType = Uint16Array;
        break;
      case "i":
      case "l":
      case "n":
        arrayType = Int32Array;
        break;
      case "I":
      case "L":
      case "N":
      case "P":
        arrayType = Uint32Array;
        break;
      case "q":
        if (globalThis.BigInt64Array === undefined) {
          throw new Error(
            "BigInt64Array is not supported on this browser." + errorMessage
          );
        }
        arrayType = BigInt64Array;
        break;
      case "Q":
        if (globalThis.BigUint64Array === undefined) {
          throw new Error(
            "BigUint64Array is not supported on this browser." + errorMessage
          );
        }
        arrayType = BigUint64Array;
        break;
      case "f":
        arrayType = Float32Array;
        break;
      case "d":
        arrayType = Float64Array;
        break;
      case "e":
        throw new Error("Javascript has no Float16 support.");
      default:
        throw new Error(
          `Unrecognized format character '${formatChar}'.` + errorMessage
        );
    }
    return [arrayType, bigEndian];
  };

  /**
   * Convert a 1-dimensional contiguous buffer to Javascript.
   *
   * In this case we can just slice the memory out of the wasm HEAP.
   * @param {number} ptr A pointer to the start of the buffer in wasm memory
   * @param {number} stride The size of the entries in bytes
   * @param {number} n The number of entries
   * @returns A new ArrayBuffer with the appropriate data in it (not a view of
   *  the WASM heap)
   * @private
   */
  Module.python2js_buffer_1d_contiguous = function (ptr, stride, n) {
    "use strict";
    let byteLength = stride * n;
    // Note: slice here is a copy (as opposed to subarray which is not)
    return HEAP8.slice(ptr, ptr + byteLength).buffer;
  };

  /**
   * Convert a 1d noncontiguous buffer to Javascript.
   *
   * Since the buffer is not contiguous we have to copy it in chunks.
   * @param {number} ptr The WAM memory pointer to the start of the buffer.
   * @param {number} stride The stride in bytes between each entry.
   * @param {number} suboffset The suboffset from the Python Buffer protocol.
   *  Negative if no suboffsets. (see
   *  https://docs.python.org/3/c-api/buffer.html#c.Py_buffer.suboffsets)
   * @param {number} n The number of entries.
   * @param {number} itemsize The size in bytes of each entry.
   * @returns A new ArrayBuffer with the appropriate data in it (not a view of
   *  the WASM heap)
   * @private
   */
  Module.python2js_buffer_1d_noncontiguous = function (
    ptr,
    stride,
    suboffset,
    n,
    itemsize
  ) {
    "use strict";
    let byteLength = itemsize * n;
    // Make new memory of the appropriate size
    let buffer = new Uint8Array(byteLength);
    for (let i = 0; i < n; ++i) {
      let curptr = ptr + i * stride;
      if (suboffset >= 0) {
        curptr = HEAP32[curptr / 4] + suboffset;
      }
      buffer.set(HEAP8.subarray(curptr, curptr + itemsize), i * itemsize);
    }
    return buffer.buffer;
  };

  /**
   * Convert an ndarray to a nested Javascript array, the main function.
   *
   * This is called by _python2js_buffer_inner (defined in python2js_buffer.c).
   * There are two layers of setup that need to be done to get the base case of
   * the recursion right.
   *
   * The last dimension of the array is handled by the appropriate 1d array
   * converter: python2js_buffer_1d_contiguous or
   * python2js_buffer_1d_noncontiguous.
   *
   * @param {number} ptr The pointer into the buffer
   * @param {number} curdim What dimension are we currently working on? 0 <=
   * curdim < ndim.
   * @param {number} bufferData All of the data out of the Py_buffer, plus the
   * converter function: ndim, format, itemsize, shape (a ptr), strides (a ptr),
   * suboffsets (a ptr), converter,
   * @returns A nested Javascript array, the result of the conversion.
   * @private
   */
  Module._python2js_buffer_recursive = function (ptr, curdim, bufferData) {
    "use strict";
    // When indexing HEAP32 we need to divide the pointer by 4
    let n = HEAP32[bufferData.shape / 4 + curdim];
    let stride = HEAP32[bufferData.strides / 4 + curdim];
    let suboffset = -1;
    if (bufferData.suboffsets !== 0) {
      suboffset = HEAP32[bufferData.suboffsets / 4 + curdim];
    }
    if (curdim === bufferData.ndim - 1) {
      // Last dimension, use appropriate 1d converter
      let arraybuffer;
      if (stride === bufferData.itemsize && suboffset < 0) {
        arraybuffer = Module.python2js_buffer_1d_contiguous(ptr, stride, n);
      } else {
        arraybuffer = Module.python2js_buffer_1d_noncontiguous(
          ptr,
          stride,
          suboffset,
          n,
          bufferData.itemsize
        );
      }
      return bufferData.converter(arraybuffer);
    }

    let result = [];
    for (let i = 0; i < n; ++i) {
      // See:
      // https://docs.python.org/3/c-api/buffer.html#pil-style-shape-strides-and-suboffsets
      let curPtr = ptr + i * stride;
      if (suboffset >= 0) {
        curptr = HEAP32[curptr / 4] + suboffset;
      }
      result.push(
        Module._python2js_buffer_recursive(curPtr, curdim + 1, bufferData)
      );
    }
    return result;
  };

  /**
   * Get the appropriate converter function.
   *
   * The converter function takes an ArrayBuffer and returns an appropriate
   * TypedArray. If the buffer is big endian, the converter will convert the
   * data to little endian.
   *
   * The converter function does something special if the format character is
   * "?" or "s". If it's "?" we return an array of booleans, if it's "s" we
   * return a string.
   *
   * @param {string} format The format character of the buffer.
   * @param {number} itemsize Should be one of 1, 2, 4, 8. Used for big endian
   * conversion.
   * @returns A converter function ArrayBuffer => TypedArray
   * @private
   */
  Module.get_converter = function (format, itemsize) {
    "use strict";
    let formatStr = UTF8ToString(format);
    let [ArrayType, bigEndian] = Module.processBufferFormatString(formatStr);
    let formatChar = formatStr.slice(-1);
    switch (formatChar) {
      case "s":
        let decoder = new TextDecoder("utf8");
        return (buff) => decoder.decode(buff);
      case "?":
        return (buff) => Array.from(new Uint8Array(buff), (x) => !!x);
    }

    if (!bigEndian) {
      return (buff) => new ArrayType(buff);
    }
    let getFuncName;
    let setFuncName;
    switch (itemsize) {
      case 2:
        getFuncName = "getUint16";
        setFuncName = "setUint16";
        break;
      case 4:
        getFuncName = "getUint32";
        setFuncName = "setUint32";
        break;
      case 8:
        getFuncName = "getFloat64";
        setFuncName = "setFloat64";
        break;
      default:
        throw new Error(`Unexpected size ${itemsize}`);
    }
    function swapFunc(buff) {
      let dataview = new DataView(buff);
      let getFunc = dataview[getFuncName].bind(dataview);
      let setFunc = dataview[setFuncName].bind(dataview);
      for (let byte = 0; byte < dataview.byteLength; byte += itemsize) {
        // Get value as little endian, set back as big endian.
        setFunc(byte, getFunc(byte, true), false);
      }
      return buff;
    }
    return (buff) => new ArrayType(swapFunc(buff));
  };
});

JS_FILE(python2js_buffer_init, () => {
  0, 0; /* Magic, see include_js_file.h */

  /**
   * Determine type and endianness of data from format. This is a helper
   * function for converting buffers from Python to Javascript, used in
   * PyProxyBufferMethods and in `toJs` on a buffer.
   *
   * To understand this function it will be helpful to look at the tables here:
   * https://docs.python.org/3/library/struct.html#format-strings
   *
   * @arg format {String} A Python format string (caller must convert it to a
   *      Javascript string).
   * @arg errorMessage {String} Extra stuff to append to an error message if
   *      thrown. Should be a complete sentence.
   * @returns A pair, an appropriate TypedArray constructor and a boolean which
   *      is true if the format suggests a big endian array.
   * @private
   */
  Module.processBufferFormatString = function (formatStr, errorMessage = "") {
    if (formatStr.length > 2) {
      throw new Error(
        "Expected format string to have length <= 2, " +
          `got '${formatStr}'.` +
          errorMessage
      );
    }
    let formatChar = formatStr.slice(-1);
    let alignChar = formatStr.slice(0, -1);
    let bigEndian;
    switch (alignChar) {
      case "!":
      case ">":
        bigEndian = true;
        break;
      case "<":
      case "@":
      case "=":
      case "":
        bigEndian = false;
        break;
      default:
        throw new Error(
          `Unrecognized alignment character ${alignChar}.` + errorMessage
        );
    }
    let arrayType;
    switch (formatChar) {
      case "b":
        arrayType = Int8Array;
        break;
      case "s":
      case "p":
      case "c":
      case "B":
      case "?":
        arrayType = Uint8Array;
        break;
      case "h":
        arrayType = Int16Array;
        break;
      case "H":
        arrayType = Uint16Array;
        break;
      case "i":
      case "l":
      case "n":
        arrayType = Int32Array;
        break;
      case "I":
      case "L":
      case "N":
      case "P":
        arrayType = Uint32Array;
        break;
      case "q":
        if (globalThis.BigInt64Array === undefined) {
          throw new Error(
            "BigInt64Array is not supported on this browser." + errorMessage
          );
        }
        arrayType = BigInt64Array;
        break;
      case "Q":
        if (globalThis.BigUint64Array === undefined) {
          throw new Error(
            "BigUint64Array is not supported on this browser." + errorMessage
          );
        }
        arrayType = BigUint64Array;
        break;
      case "f":
        arrayType = Float32Array;
        break;
      case "d":
        arrayType = Float64Array;
        break;
      case "e":
        throw new Error("Javascript has no Float16 support.");
      default:
        throw new Error(
          `Unrecognized format character '${formatChar}'.` + errorMessage
        );
    }
    return [arrayType, bigEndian];
  };

  /**
   * Convert a 1-dimensional contiguous buffer to Javascript.
   *
   * In this case we can just slice the memory out of the wasm HEAP.
   * @param {number} ptr A pointer to the start of the buffer in wasm memory
   * @param {number} stride The size of the entries in bytes
   * @param {number} n The number of entries
   * @returns A new ArrayBuffer with the appropriate data in it (not a view of
   *  the WASM heap)
   * @private
   */
  Module.python2js_buffer_1d_contiguous = function (ptr, stride, n) {
    "use strict";
    let byteLength = stride * n;
    // Note: slice here is a copy (as opposed to subarray which is not)
    return HEAP8.slice(ptr, ptr + byteLength).buffer;
  };

  /**
   * Convert a 1d noncontiguous buffer to Javascript.
   *
   * Since the buffer is not contiguous we have to copy it in chunks.
   * @param {number} ptr The WAM memory pointer to the start of the buffer.
   * @param {number} stride The stride in bytes between each entry.
   * @param {number} suboffset The suboffset from the Python Buffer protocol.
   *  Negative if no suboffsets. (see
   *  https://docs.python.org/3/c-api/buffer.html#c.Py_buffer.suboffsets)
   * @param {number} n The number of entries.
   * @param {number} itemsize The size in bytes of each entry.
   * @returns A new ArrayBuffer with the appropriate data in it (not a view of
   *  the WASM heap)
   * @private
   */
  Module.python2js_buffer_1d_noncontiguous = function (
    ptr,
    stride,
    suboffset,
    n,
    itemsize
  ) {
    "use strict";
    let byteLength = itemsize * n;
    // Make new memory of the appropriate size
    let buffer = new Uint8Array(byteLength);
    for (let i = 0; i < n; ++i) {
      let curptr = ptr + i * stride;
      if (suboffset >= 0) {
        curptr = HEAP32[curptr / 4] + suboffset;
      }
      buffer.set(HEAP8.subarray(curptr, curptr + itemsize), i * itemsize);
    }
    return buffer.buffer;
  };

  /**
   * Convert an ndarray to a nested Javascript array, the main function.
   *
   * This is called by _python2js_buffer_inner (defined in python2js_buffer.c).
   * There are two layers of setup that need to be done to get the base case of
   * the recursion right.
   *
   * The last dimension of the array is handled by the appropriate 1d array
   * converter: python2js_buffer_1d_contiguous or
   * python2js_buffer_1d_noncontiguous.
   *
   * @param {number} ptr The pointer into the buffer
   * @param {number} curdim What dimension are we currently working on? 0 <=
   * curdim < ndim.
   * @param {number} bufferData All of the data out of the Py_buffer, plus the
   * converter function: ndim, format, itemsize, shape (a ptr), strides (a ptr),
   * suboffsets (a ptr), converter,
   * @returns A nested Javascript array, the result of the conversion.
   * @private
   */
  Module._python2js_buffer_recursive = function (ptr, curdim, bufferData) {
    "use strict";
    // When indexing HEAP32 we need to divide the pointer by 4
    let n = HEAP32[bufferData.shape / 4 + curdim];
    let stride = HEAP32[bufferData.strides / 4 + curdim];
    let suboffset = -1;
    if (bufferData.suboffsets !== 0) {
      suboffset = HEAP32[bufferData.suboffsets / 4 + curdim];
    }
    if (curdim === bufferData.ndim - 1) {
      // Last dimension, use appropriate 1d converter
      let arraybuffer;
      if (stride === bufferData.itemsize && suboffset < 0) {
        arraybuffer = Module.python2js_buffer_1d_contiguous(ptr, stride, n);
      } else {
        arraybuffer = Module.python2js_buffer_1d_noncontiguous(
          ptr,
          stride,
          suboffset,
          n,
          bufferData.itemsize
        );
      }
      return bufferData.converter(arraybuffer);
    }

    let result = [];
    for (let i = 0; i < n; ++i) {
      // See:
      // https://docs.python.org/3/c-api/buffer.html#pil-style-shape-strides-and-suboffsets
      let curPtr = ptr + i * stride;
      if (suboffset >= 0) {
        curptr = HEAP32[curptr / 4] + suboffset;
      }
      result.push(
        Module._python2js_buffer_recursive(curPtr, curdim + 1, bufferData)
      );
    }
    return result;
  };

  /**
   * Get the appropriate converter function.
   *
   * The converter function takes an ArrayBuffer and returns an appropriate
   * TypedArray. If the buffer is big endian, the converter will convert the
   * data to little endian.
   *
   * The converter function does something special if the format character is
   * "?" or "s". If it's "?" we return an array of booleans, if it's "s" we
   * return a string.
   *
   * @param {string} format The format character of the buffer.
   * @param {number} itemsize Should be one of 1, 2, 4, 8. Used for big endian
   * conversion.
   * @returns A converter function ArrayBuffer => TypedArray
   * @private
   */
  Module.get_converter = function (format, itemsize) {
    "use strict";
    let formatStr = UTF8ToString(format);
    let [ArrayType, bigEndian] = Module.processBufferFormatString(formatStr);
    let formatChar = formatStr.slice(-1);
    switch (formatChar) {
      case "s":
        let decoder = new TextDecoder("utf8");
        return (buff) => decoder.decode(buff);
      case "?":
        return (buff) => Array.from(new Uint8Array(buff), (x) => !!x);
    }

    if (!bigEndian) {
      return (buff) => new ArrayType(buff);
    }
    let getFuncName;
    let setFuncName;
    switch (itemsize) {
      case 2:
        getFuncName = "getUint16";
        setFuncName = "setUint16";
        break;
      case 4:
        getFuncName = "getUint32";
        setFuncName = "setUint32";
        break;
      case 8:
        getFuncName = "getFloat64";
        setFuncName = "setFloat64";
        break;
      default:
        throw new Error(`Unexpected size ${itemsize}`);
    }
    function swapFunc(buff) {
      let dataview = new DataView(buff);
      let getFunc = dataview[getFuncName].bind(dataview);
      let setFunc = dataview[setFuncName].bind(dataview);
      for (let byte = 0; byte < dataview.byteLength; byte += itemsize) {
        // Get value as little endian, set back as big endian.
        setFunc(byte, getFunc(byte, true), false);
      }
      return buff;
    }
    return (buff) => new ArrayType(swapFunc(buff));
  };
});
