SCALE Codecs for JavaScript and TypeScript
A TypeScript implementation of SCALE (Simple Concatenated Aggregate Little-Endian) transcoding (see Rust implementation here), which emphasizes JS-land representations and e2e type-safety. These types are described below.
⚠️ This TypeScript implementation of Parity's SCALE Codecs is in beta. If you encounter a bug or want to give feedback on the API design, please create an issue.
Setup
If you're using Deno, simply import via the denoland/x
specifier.
import * as $ from "https://deno.land/x/scale/mod.ts";
If you're using Node, install as follows.
npm install parity-scale-codec
NOTE: The published package name is (while in beta) subject to change
Then import as follows.
import * as $ from "parity-scale-codec";
Usage
- Import the library
- Define a codec via the library's functions, whose names correspond to types
- Utilize the codec you've defined
Example
import * as $ from "https://deno.land/x/scale/mod.ts";
const $person = $.object(
["name", $.str],
["nickName", $.str],
["superPower", $.option($.str)],
);
const valueToEncode = {
name: "Magdalena",
nickName: "Magz",
superPower: "Hydrokinesis",
};
const encodedBytes: Uint8Array = $person.encode(valueToEncode);
const decodedValue: Person = $person.decode(encodedBytes);
assertEquals(decodedValue, valueToEncode);
To extract the JS-native TypeScript type from a given codec, use the Native
utility type.
type Person = $.Native<typeof $person>;
/* {
name: string;
nickName: string;
superPower: string | undefined;
} */
In cases where codecs are exceptionally large, we may want to spare the TS checker of extra work.
interface Person {
name: string;
nickName: string;
superPower: string | undefined;
}
const $person: Codec<Person> = $.object(
["name", $.str],
["nickName", $.str],
["superPower", $.option($.str)],
);
This has the added benefit of producing type errors if the codec does not directly mirror the TS type.
const $person: Codec<Person> = $.object(
// ~~~~~
// ^ error (message below)
["nickName", $.str],
["superPower", $.option($.str)],
);
Hovering over the error squigglies will reveal the following diagnostic.
Type 'Codec<{ nickName: string; superPower: string | undefined; }>' is not assignable to type 'Codec<Person>'.
The types returned by 'decode(...)' are incompatible between these types.
Type '{ nickName: string; superPower: string | undefined; }' is not assignable to type 'Person'.
Codec Naming
This library adopts a convention of denoting codecs with a $
– $.foo
for built-in codec, and $foo
for user-defined codecs. This makes codecs easily distinguishable from other values, and makes it easier to have codecs in scope with other variables:
interface Person { ... }
const $person = $.object(...)
const person = { ... }
Here, the type, codec, and a value can all coexist without clashing, without having to resort to wordy workarounds like personCodec
.
The main other library this could possibly clash with is jQuery, and its usage has waned enough that this is not a serious problem.
While we recommend following this convention for consistency, you can, of course, adopt an alternative convention if the $
is problematic – $.foo
can easily become s.foo
or scale.foo
with an alternate import name.
Types
Primitives
$.bool; // Codec<boolean>
$.u8; // Codec<number>
$.i8; // Codec<number>
$.u16; // Codec<number>
$.i16; // Codec<number>
$.u32; // Codec<number>
$.i32; // Codec<number>
$.u64; // Codec<bigint>
$.i64; // Codec<bigint>
$.u128; // Codec<bigint>
$.i128; // Codec<bigint>
$.u256; // Codec<bigint>
$.i256; // Codec<bigint>
// https://docs.substrate.io/v3/advanced/scale-codec/#compactgeneral-integers
$.compact; // Codec<number | bigint>
$.str; // Codec<string>
Arrays
$.sizedArray($.u8, 2); // Codec<[number, number]>
$.array($.u8); // Codec<number[]>
$.tuple($.bool, $.u8, $.str); // Codec<[boolean, number, string]>
Objects
const $person = $.object(
["name", $.str],
["nickName", $.str],
["superPower", $.option($.str)],
);
$person; /* Codec<{
name: string;
nickName: string;
superPower: string | undefined;
}> */
Options
$.option($.u8); // Codec<number | undefined>
Unions
Explicitly Discriminated
const $strOrNum = $.union(
(value) => { // Discriminate
if (typeof value === "string") {
return 0;
} else if (typeof value === "number") {
return 1;
} else {
throw new Error("Unreachable");
}
},
$.str, // Member 0
$.u8, // Member 1
);
$strOrNum; // Codec<string | number>
Tagged Unions
const $pet = $.taggedUnion(
"_tag",
["dog", ["bark", $.str]],
["cat", ["purr", $.str]],
);
$pet; /* Codec<
| { _tag: "dog"; bark: string }
| { _tag: "cat"; purr: string }
> */
Key Literals (aka., Native TypeScript Enums)
enum Dinosaur {
Liopleurodon = "Liopleurodon",
Kosmoceratops = "Kosmoceratops",
Psittacosaurus = "Psittacosaurus",
}
const $dinosaur = $.keyLiteralUnion(
Dinosaur.Liopleurodon,
Dinosaur.Kosmoceratops,
Dinosaur.Psittacosaurus,
);
$dinosaur; // Codec<Dinosaur>
Numeric Enums
enum Dinosaur {
Liopleurodon,
Kosmoceratops,
Psittacosaurus,
}
const $dinosaur = $.u8 as Codec<Dinosaur>;
$dinosaur; // Codec<Dinosaur>
Instance
Sometimes, you may want to instantiate a class with the decoded data / encode data from a class instance. In these situations, we can leverage the instance
codec factory. A common use case for instance
codecs is Error
subtypes.
class MyError extends Error {
constructor(
readonly code: number,
readonly message: string,
readonly payload: {
a: string;
b: number;
c: boolean;
},
) {
super();
}
}
const $myError = $.instance(
MyError,
["code", $.u8],
["message", $.str],
[
"payload",
$.object(
["a", $.str],
["b", $.u8],
["c", $.bool],
),
],
);
$myError; // Codec<MyError>
Results
Result
s are initialized with an Ok
codec and an Error
instance codec.
class MyError {
constructor(readonly message: string) {}
}
const $myError = $.instance(MyError, ["message", $.str]);
const $myResult = $.result($.str, $myError);
$myResult; // Codec<string | MyError>