Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

WIT Type Representations

Similar to any other guest langauge, there are multiple type systems in play when dealing with JS WebAssembly components.

Types represented in WebAssembly Interface Types (“WIT”) must be converted down to types that are familiar for Javascript, and Typescript (if dealing with jco types or jco guest-types subcommands).

This document details the type representations and usage for types that are defined in WIT and built into components.

Basic types

Here is a basic table of conversions between WIT types and JS types:

More complicated types that are built into WIT but require more work to translate are explained below.

WIT typeJS Type
u8number
u16number
u32number
u64BigInt
s8number
s16number
s32number
s64BigInt
f32number
f64number
boolboolean
charstring
stringstring

Variants (variant)

Note

See the Variant section of the WIT IDL for more information on Variants

Variants are like basic enums in most languages with one exception; members of the variant can hold a single data type. Alternative variant members may hold different types to represent different cases. For example:

variant exit-code {
  success,
  failure-code(u32),
  failure-msg(string),
}

WIT syntax

variant filter {
    all,
    none,
    some(list<string>),
}

Jco Representation

Jco represents variants as objects with a tag that represents the variant, and val that represents the content:

For example, pseudo Typescript for the of the above filter variant would look like the following:

// Filter with all
{
  tag: 'all';
}

// Filter with None
{
  tag: 'none';
}

// Filter with some and a list of strings
{
  tag: 'some';
  val: string[];
}

Note

WIT variant’s options may only contain one piece of data.

You can work around this limitation of variants by having the contained type be a tuple, (e.g. tuple<string, u32, string>), or using a named record as the related data.

Records (record)

WIT Syntax

record person {
    name: string,
    age: u32,
    favorite-color: option<string>,
}

Jco Representation

Jco represents records as the Javascript Object basic data type:

Given the WIT record above, you can expect to deal with an object similar to the following Typescript:

interface Person {
    person: string;
    age: number;
    favoriteColor?: number;
}

Note

If using jco guest-types or jco types, you will be able to use Typescript types that properly constrain the Typescript code you write.

Options (option)

WIT Syntax

option<u32, u32>
option<string, u32>

Jco Representation

Jco represents options as an optional value or undefined, so some examples:

TypeRepresentation (TS)Example
option<u32>number | undefinedoption<u32> -> number | undefined
option<option<u32>>{ tag: "some" | "none", val: number }option<u32> -> number | undefined

Warning

“single level” options are easy to reason about, but the doubly nested case (option<option<_>>) is more complex.

Due to the important distinction between a missing optional versus an option that contains an empty value, doubly-nested (or more) options are encoded with the object encoding described above, rather than as an optional value.

options in context: Records

When used in the context of a record (which becomes a JS Object), optional values are represented as optional properties (i.e in TS a propName?: value).

options in context: Function arguments/return values

When used in the context of arguments or return to a function, single level options are represented as optional values:

Consider the following interface:

interface optional {
    f: func(n: option<u32>) -> string;
}

An implementation of the function optional.f would look like the following Typescript:

function f(n?: number): string {
    if (n === undefined) {
        return 'no n provided';
    }
    return 'n was provided';
}

Result (result)

Result types, as a general concept represent a result that may or may not be present, due to a failure. A result value either contains a value that represents a completed computation (SuccessType), or some “error” that indicates a failure (ErrorType).

You can think of the type of a Result as:

Result<SuccessType, ErrorType>

The value you ultimately deal with is one or the other – either the successful result or the error that represents the failure.

WIT Syntax

result<_, string>
result<, string>
result<t,e>

Jco representation

In Javsacript, computation that fails or errors are often represented as exceptions – and depending on how the result is used, Jco adheres to that representations.

When used as an output to a function, throwing an error will suffice. Given the following WIT interface:

add-overflow: func(lhs: u32, rhs: u32) -> result<u32, string>;

The following JS function would satistfy the WIT interface:

function addOverflow(lhs, rhs) {
    let sum = lhs + rhs;
    if (Nan.isNan(sum)) {
        throw 'ERROR: addition produced non-number value';
    } else if (sum > 4294967295) {
        throw 'ERROR: u32 overflow';
    }
    return sum;
}

While JS automatically converts numbers, we must be careful to not attempt passing a number that would not fit in a u32 (unsigned 32 bit integer) via WebAssembly.

Note

How JS treats large numbers is not in focus here, but it is worth noting that Number.MAX_VALUE + Number.MAX_VALUE === Infinity.

Typescript Schema

type Result<T,E> = { tag: 'ok', val: T } | { tag: 'err', val: E };

results in context: Function return values

When a result is returned directly from a function, any thrown error of the function is treated as the result error type, while any direct return value is treated as the result success type.

Consider the following interface:

interface fallible {
    f: func(n: u32) -> result<string, string>;
}

An implementation of the function fallible.f would look like the following Typescript:

function f(n: number): string {
    if (n == 42) {
        return 'correct';
    }
    throw 'not correct';
}

results in context: Container types (record, optional, etc)

A result stored inside a container type or in non-function argument/return contexts will look like a variant type of the form { tag: 'ok', val: SuccessType } | { tag: 'err', val: ErrorType }.

For example, consider the following WIT interface:

interface fallible-reaction {
    r: func(r: result<string, string>) -> string;
}

An implementation of the function fallible-reaction.r would look like the following Typescript:

type Result<T,E> = { tag: 'ok', val: T } | { tag: 'err', val: E };

function f(input: Result<string, string>): string {
  switch (input.tag) {
    case 'ok': return `SUCCESS, returned: [${input.val}]";
    case 'err': return `ERROR, returned: [${input.val}]";
    // We we should never reach the case below
    default: throw Error("something has gone seriously wrong");
  }
}

result considerations: Idiomatic JS errors for Host implementations

When running a component in a JS host, it is likely for host functions to throw real JS errors (objects which are descendants of the Error global object), rather than the exact type expected by Jco.

This means that the default conversion mechanism for Jco would be a JS anti-pattern (i.e. throw 12345 versus throw new Error("error code 12345")).

To ensure smooth use of Jco-generated code from hosts, Error objects with a payload property will have the payload extracted as the result error type.

Consider the following WIT:

type error-code = u32;

interface only-throws {
    just-throw: func() -> result<string, error-code>;
}

Consider the following host function adhering to the interface, and making use of idiomatic JS errors:

// The below code assumes interaction with a WIT which looks like a
function justThrow() {
    const plainError = new Error('Error for JS users');
    const errorWithPayload = Object.assign(plainError, { payload: 1111 });
    throw errorWithPayload;
}

Tuples (tuple)

Tuples are a container type that has a fixed size, types somewhat analogous to a fixed size list.

Tuples can be combined with type renaming to produce types that carry some semantic meaning. For example:

type point = tuple<u32,u32>

Note that tuples can be combined with custom user-defined types like records and variants, options and results. For example:

variant example-var {
    nothing,
    value(u64),
}

record example-rec {
    fst: string,
    snd: u32,
}

type maybe-num = option<u32>;

type num-or-err-str = result<u32, string>;

type examples = tuple<example-rec, example-var, maybe-num, num-or-err-str>;

WIT Syntax

tuple<u32, u32>
tuple<string, u32>

Jco Representation

Jco represents tuples as lists (arrays), so some examples:

TypeRepresentation (TS)Example
tuple<u32, u32>[number, number]tuple<u32, u32> -> [number, number]
tuple<string, u32>[string, number]tuple<string, u32> -> [string, number]

List (list)

WIT Syntax

list<u8>
list<string>

Jco Representation

Jco represents lists with native Javscript Arrays, with the exception of a list<u8>:

TypeRepresentation (TS)Example
list<u8>Uint8Arraylist<u8> -> Uint8Array
list<t>T[]list<string> -> string[]

Resources (resource)

Note

See the WIT IDL description of Resources for more information

Resources represent values that cannot be copied across a component boundary. A resource handle refers to state owned by the resource’s provider, without exposing that state to its consumer. In JavaScript, Jco represents the handle as an object whose methods call back into the provider.

Resources are useful for stateful or platform-specific values such as files, sockets, and HTTP bodies. Unlike a WIT record, passing a resource does not serialize all of its fields.

The examples below use a blob resource:

WIT Syntax

package docs:resources;

interface blobs {
    resource blob {
        constructor(init: list<u8>);
        write: func(bytes: list<u8>);
        read: func() -> list<u8>;
        merge: static func(lhs: borrow<blob>, rhs: borrow<blob>) -> blob;
    }
}

world imports-blobs {
    import blobs;
}

world exports-blobs {
    export blobs;
}

Jco representation

The resource is represented as a class. WIT kebab-case names become JavaScript camelCase for functions and PascalCase for classes. The blob resource above therefore has the following approximate TypeScript shape:

class Blob implements Disposable {
    constructor(init: Uint8Array) {}

    write(bytes: Uint8Array): void {}

    read(): Uint8Array {}

    static merge(lhs: Blob, rhs: Blob): Blob {}

    [Symbol.dispose](): void {}
}

The exact declaration depends on whether the resource is imported or exported. Run jco guest-types when implementing a JavaScript guest, or inspect the declarations emitted by jco transpile when using a component from a JavaScript host. The generated types are the source of truth for the binding being used.

Imports and exports are from the guest’s perspective

It is useful to identify the provider and consumer before writing any JavaScript:

WIT world itemResource providerResource consumer
import blobsHostGuest component
export blobsGuest componentHost

An imported resource is implemented by the host and passed to the component during instantiation. An exported resource is implemented by the guest and returned to the host as part of the component’s exports.

Ownership and borrowing

A resource value is a handle with an associated lifetime. WIT uses two handle modes:

  • own<blob> transfers ownership to the callee. The receiving side is then responsible for eventually dropping the handle.
  • borrow<blob> temporarily makes the handle available to the callee. Ownership remains with the caller, and the callee must not keep using the handle after the call returns.

Writing blob in a result or parameter is shorthand for an owned handle where WIT permits that shorthand. Resource methods implicitly borrow self, so calling blob.read() does not consume blob. In the example, merge borrows both arguments and returns a new owned resource.

When a generated resource class implements Disposable, release it deterministically with a using declaration:

{
    using blob = new Blob(new Uint8Array([1, 2, 3]));
    blob.write(new Uint8Array([4]));
    console.log(blob.read());
} // blob[Symbol.dispose]() is called here

The equivalent JavaScript without a using declaration is:

const blob = new Blob(new Uint8Array([1, 2, 3]));
try {
    console.log(blob.read());
} finally {
    blob[Symbol.dispose]();
}

Warning

Do not use a resource after disposing it or after passing it to a parameter that takes own<blob>. JavaScript garbage collection is not a substitute for deterministic cleanup when the underlying resource holds files, sockets, or other limited host state.

Whether [Symbol.dispose]() appears on a particular binding is recorded in its generated declaration. A provider may also implement [Symbol.dispose]() as a cleanup hook; Jco calls that hook when the corresponding owned handle is dropped.

Importing a host resource into a guest

The imports-blobs world declares that the guest needs the host to provide the blobs interface. Generate types for the guest implementation with:

jco guest-types wit --world-name imports-blobs -o generated

The generated ambient module uses the WIT package and interface name. Guest TypeScript can import the resource class, construct it, and call its methods:

/// <reference path="./generated/imports-blobs.d.ts" />
import { Blob } from 'docs:resources/blobs';

export function processBytes(): Uint8Array {
    using left = new Blob(new Uint8Array([1, 2]));
    using right = new Blob(new Uint8Array([3, 4]));
    using merged = Blob.merge(left, right);

    merged.write(new Uint8Array([5]));
    return merged.read();
}

Blob is supplied by the host even though it looks like a normal class to the guest. The guest can only observe the operations described by WIT.

Providing the imported resource from the host

The host implements the resource with a JavaScript class. Its private fields remain entirely on the host:

class HostBlob {
    #bytes: number[];

    constructor(init: Uint8Array) {
        this.#bytes = Array.from(init);
    }

    write(bytes: Uint8Array): void {
        this.#bytes.push(...bytes);
    }

    read(): Uint8Array {
        return Uint8Array.from(this.#bytes);
    }

    static merge(lhs: HostBlob, rhs: HostBlob): HostBlob {
        return new HostBlob(Uint8Array.from([...lhs.#bytes, ...rhs.#bytes]));
    }

    [Symbol.dispose](): void {
        this.#bytes.length = 0;
    }
}

Transpile the component with explicit instantiation support:

jco transpile component.wasm -o transpiled --instantiation=async

Then provide the class under the WIT interface’s fully qualified name:

import { readFile } from 'node:fs/promises';
import { instantiate } from './transpiled/component.js';

const loader = async (path: string) => WebAssembly.compile(await readFile(new URL(path, import.meta.url)));

const instance = await instantiate(loader, {
    'docs:resources/blobs': {
        Blob: HostBlob,
    },
});

The property names match the generated JavaScript names: the WIT resource blob becomes Blob. Jco invokes methods with the original resource object as this, so private and per-instance state work as they do on an ordinary class.

Real components often have additional imports, such as WASI interfaces. Those implementations must be included in the same import object; they are omitted here to keep the resource wiring visible.

Exporting a guest resource to the host

The exports-blobs world reverses the direction: the guest provides the implementation and the host consumes it. First generate the guest declarations:

jco guest-types wit --world-name exports-blobs -o generated

Use the generated resource type as the contract for the guest class, then export the class inside an object named after the WIT interface:

/// <reference path="./generated/exports-blobs.d.ts" />
import type { Blob } from 'docs:resources/blobs';

class GuestBlob implements Blob {
    #bytes: number[];

    constructor(init: Uint8Array) {
        this.#bytes = Array.from(init);
    }

    write(bytes: Uint8Array): void {
        this.#bytes.push(...bytes);
    }

    read(): Uint8Array {
        return Uint8Array.from(this.#bytes);
    }

    static merge(lhs: GuestBlob, rhs: GuestBlob): GuestBlob {
        return new GuestBlob(Uint8Array.from([...lhs.#bytes, ...rhs.#bytes]));
    }

    [Symbol.dispose](): void {
        this.#bytes.length = 0;
    }
}

export const blobs = {
    Blob: GuestBlob,
};

The interface export is an object rather than a top-level Blob export because the WIT world exports the complete blobs interface. The shape of this object can be checked against the world module emitted by jco guest-types.

Using the exported resource from the host

After componentizing and transpiling the guest, instantiate it from the host:

jco componentize guest.js --wit wit --world-name exports-blobs \
    -o component.wasm
jco transpile component.wasm -o transpiled --instantiation=async

The instantiated component returns the exported interface. Its resource constructor and methods can be used like an ordinary JavaScript class:

import { readFile } from 'node:fs/promises';
import { instantiate } from './transpiled/component.js';

const loader = async (path: string) => WebAssembly.compile(await readFile(new URL(path, import.meta.url)));

const { blobs } = await instantiate(loader, {});

const left = new blobs.Blob(new Uint8Array([1, 2]));
const right = new blobs.Blob(new Uint8Array([3, 4]));
const merged = blobs.Blob.merge(left, right);

console.log(merged.read());

The host owns all three handles returned by their constructors and merge. If their generated declarations expose [Symbol.dispose](), the host should dispose each handle when it is no longer needed, preferably with using. Check the declarations generated by the Jco version in use: disposal support can differ between binding directions and component-model features.

The important distinction is where the classes originate:

Use caseClass implementationConnection at instantiationCalls the resource
Guest imports resourceHostHost passes { Blob: HostBlob }Guest
Guest exports resourceGuestHost receives { blobs }Host

These are the same WIT resource semantics in opposite directions. In both cases, Jco preserves the resource’s identity and routes method calls to the side that owns its underlying state.