DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

jtenner
jtenner

Posted on

AssemblyScript is *not* a subset of TypeScript

AssemblyScript definitely not a TypeScript to Web Assembly compiler. In fact, there are lots of differences between the syntaxes used in TypeScript that don't match up nicely with AssemblyScript.

This blog post is just going to be a list of all the top level things that make AssemblyScript different, and I promise to keep it up to date as I find new differences.

Number Types

JavaScript only has two number types BigInt and number.

AssemblyScript can discern between many number types, including:

  • i8 (integers!)
  • u8
  • i16
  • u16
  • i32
  • u32
  • i64
  • u64
  • f32 (floats!)
  • f64
  • isize (signed pointer compiling to i32 or i64 for wasm64)
  • usize (unsigned pointer compiling to u32 or u64 for wasm64)

For now, i32 is the number type that backs isize and usize. This is because of the two-gigabyte data limit in browsers and the 32-bit runtime of Web Assembly. At some point, both of those restrictions will be lifted. When using pointers, if you ever have to, use usize or isize because the backing type may be different later.

Operator Overloads and Top Level Decorators

class Vec3 {
  constructor(
    public x: f64 = 0.0,
    public y: f64 = 0.0,
    public z: f64 = 0.0,
  ) {}

  /**
   * This overloads the `==` operator. You can still use `===` for
   * comparing reference pointers exactly.
   */
  @operator("==")
  protected __equals(ref: Vec3): bool {
    return this.x == ref.x && this.y == ref.y && this.z == ref.z;
  }

  /**
   * This overloads the `+` operator.
   */
  @operatr("+")
  protected __add(ref: Vec3): Vec3 {
    return new Vec3(this.x + ref.x, this.y + ref.y, this.z + ref.z);
  }
}

This is now valid in AssemblyScript, but would obviously fail in a JavaScript environment.

// @ts-ignore: Add two vectors
let result = new Vec3(1, 2, 3) + new Vec3(4, 5, 6);

The reason why TypeScript does not like this statement is that Vec3 is not a number. Technically, it's possible for Vec3.prototype.valueOf() to be overridden in TypeScript, but the return value of that function will become a number instead of a reference. For this reason, operator overloading is not considered a portable feature of AssemblyScript.

It's also very common for developers to create inline functions. TypeScript tooling does not like the following syntax.

// @ts-ignore: Top level decorators are supported in AssemblyScript
@inline
export function add(left: i32, right: i32): i32 {
  return left + right;
}

This will cause the AssemblyScript function, when executed, to be compiled inline, despite the fact that TypeScript does not like this.

Not-Null Assertions

Using the postfix ! operator in TypeScript is used to describe an assertion that (at compile time) is known to not be null. Thus, the postfixed expression will result in something that is truthy.

const result = myMap.get("key")!;

In AssemblyScript, using this postfix operator is actually the equivalent of:

let result = myMap.get("key");
assert(result, "value is null! Assertion fails!");

AssemblyScript converts postscript ! operators into runtime not-null assertions.

Pointer and Equality Comparison

Normally, in TypeScript it is commonplace to use === when comparing things, because of the nature of how JavaScript works. In AssemblyScript the default comparison operator should be == unless comparing pointers exactly. For instance, this is valid JavaScript.

assert("CatDog" === "CatDog", "CatDog should be CatDog");

In AssemblyScript, this assertion will fail, because the === operator will do a pointer comparison instead of a string comparison like JavaScript does. The following snippet is required for testing string contents.

assert("CatDog" == "CatDog");

Web Assembly Intrinsics

All of the special functions used by Web Assembly are exposed via the AssemblyScript std library. For instance, it's useful to read bytes from a heap allocation using the load<T>() function.

// strings are utf16 in AssemblyScript
let buffer = changeType<ArrayBuffer>(string.toUTF8());
let len = buffer.byteLength - 1; // null terminated...
for (let i = 0; i < len; i++) {
  // load the byte at ptr + i
  let char = <i32>load<u8>(changetype<usize>(buffer) + i);
}

For more information on all the special intrinsics, you can inspect the documentation here.

Declare Keyword

Normally in TypeScript, using the declare keyword, a developer can meaningfully define a set of ambient modules and functions to describe the nature of the environment the software is running in. In AssemblyScript, this kind of namespace will result in Web Assembly imports that must be passed to the module instantiator. In this way, AssemblyScript does not exactly treat declare in an "ambient" way.

declare namespace externalNamespace {
  // only types allowed here are i32, f32 and f64
  function add(left: f64, right: f64): f64;
}

This declared function must be created in JavaScript like this:

import { instantiateStreaming } from "assemblyscript/lib/loader";

const wasm = await instantiateStreaming(fetch("../build/output.wasm"), {
  externalNamespace : { // put the namespace here
    add(left, right) { return left + right; } // put the function here
  }
});

No any type

The use of any is disallowed in AssemblyScript (as a feature!)

Undefined and Void Operator

Just like any, there is no undefined or void operator in AssemblyScript and these should be avoided too.

Null is actually 0

The funny thing about JavaScript is that we can coerce null to a 0 value. For instance, a common pitfall of developers using number properties is that null can be turned into a 0 very easily. This is a passing assertion in AssemblyScript and TypeScript.

assert(null == 0); // number coercion in JavaScript

The only difference here is that numbers cannot be nullable. For instance, the following example results in a compile-time error.

function nullableNumber(value: i32 | null): void {
}

There are no multi-value returns supported yet, and i32 numbers cannot be differentiated from null, because null itself is 0. Do not use null unless dealing with reference types.

Tree Shaking and Conditional Compilation

The AssemblyScript compiler can perform branch inlining when dealing with compile-time constants. For instance. if T is an Array, the following if-branch will be inlined, and anything after the return will be ignored by the compiler.

function length<T>(value: T): i32 {
  if (isArray<T>()) {
    return value.length;
  }
  return 0;
}

Specifically, when T is array, the if(true) branch will be inlined, and anything after value.length will be ignored.

function length<Array<i32>>(value: Array<i32>): i32 {
  return value.length;
}

Also, unused imports from different modules will simply be ignored by the compiler altogether because the compiler was built to perform "tree-shaking" at compile time.

Array Sort Differences

When dealing with an array of numbers, calling Array#sort will sort the numbers numerically instead of Alphabetically (via toString() coercion.)

In TypeScript, the following is true.

[140000, 104, 99].sort();
(3)Β [104, 140000, 99]

In AssemblyScript, this is the expected output.

[140000, 104, 99].sort()
(3)Β [99, 104, 140000]

In AssemblyScript, the sorting algorithm avoids using .toString() for number coercion and results in a properly sorted array.

Union Types

In AssemblyScript, union types are not supported. It is best to simply use the instanceof runtime check.

function log<T extends string | ArrayBuffer>(value: T): void {

}

This will result in a compile-time error because of the string | ArrayBuffer union.

function log<T>(value: T): void {
  if (isReference<T>()) {
    // useful for finding strings
    if (value instanceof string) logString(value);
  } else {
    // definitely a number type!
  }
}

Not all instanceof checks will result in runtime comparisons and instead will result in a compile-time check.

Did I miss one?

Please comment below! I am happy to update this article and keep it as an example for everyone who wants to learn about AssemblyScript.

Cheers,
@jtenner

Top comments (4)

Collapse
 
cubiclebuddha profile image
Cubicle Buddha

Thank you for publishing this comparison. I must say: I use union types all of time, so that’s unfortunate. But the mental shift from using === to == will be a strong barrier to entry for most JS devs. I think your example of the string equality perfectly summarized why it’s very unintuitive for JS devs who spent years understanding type coercion. I wonder why the language creators of AssemblyScript decided to go that route. I mean I understand not supporting unions since that’s probably rather complicated, but changing the way type coercion works... that’s a tough change to accept.

Collapse
 
jtenner profile image
jtenner Author • Edited on

This little feature is actually one of the reasons why I love the language. It wasn't hard for me to get used to at all, and I've spent my whole life in JavaScript.

First of all, since you can guaruntee type correctness at compile time, the === operator becomes pointless anyway. You probably won't compare an i32 with a Vec3 ever, thus, all the benefits of using this strict equality operator are gone.

Not every feature in the Javascript specification can be ported because the nature of Web Assembly makes things complicated. Also, being able to program your own equality comparison using the @operator("==") syntax makes it all worth it.

Just like everything else in life, some things take time and practice.

Collapse
 
cubiclebuddha profile image
Cubicle Buddha

Yup, software design is all about tradeoffs.

Collapse
 
trusktr profile image
Joe Pea

This is nice! So if we write strongly-typed code, the == should work fine in JS too. Curious to know all best practices for portable code. I suppose it would sprout from the concepts of this article.

🌚 Life is too short to browse without dark mode