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)
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.
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 ani32
with aVec3
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.
Yup, software design is all about tradeoffs.
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.