TypeScript Utilities For Candid On The IC
I developed several helpers in TypeScript to interact with my canister smart contracts.
If it can make your life easier too, here are those I use the most.
Nullable
The Candid description that is generated for nullable types does not exactly match what I commonly used in JavaScript for optional types (see this post for the why and how).
For example, if we generate an interface for such a Motoko code snippet:
actor Example {
public shared query func list(filter: ?Text) : async [Text] {
let results: [Text] = myFunction(filter);
return results;
};
}
The definition of the optional parameter filter will not be interpreted as a string that can potentially be undefined but, rather as a one-element length array that contains a string or is empty.
export interface _SERVICE {
list: (arg_0: [] | [string]) => Promise<Array<string>>;
}
That is why I created functions to convert back and forth optional values.
export const toNullable = <T>(value?: T): [] | [T] => {
return value ? [value] : [];
};
export const fromNullable = <T>(value: [] | [T]): T | undefined => {
return value?.[0];
};
toNullable converts an object - that can either be of type T or undefined - to what’s expected to interact with the IC and, fromNullable do the opposite.
Dates
System Time (nanoseconds since 1970–01–01) gets parsed to bigint and exported as a type Time in Candid definition.
export type Time = bigint;
To convert JavaScript Date to big numbers, the built-in object BigInt can be instantiated by multiplying seconds to nano seconds.
export const toTimestamp = (value: Date): Time => {
return BigInt(value.getTime() * 1000 * 1000);
};
The other way around works by converting first the big numbers to their primitive Number types and dividing it to seconds.
export const fromTimestamp = (value: Time): Date => {
return new Date(Number(value) / (1000 * 1000));
};
To support Nullable timestamps values, I also created the
following helpers that extend above converters and return the
appropriate optional arrays.
export const toNullableTimestamp = (value?: Date): [] | [Time] => {
const time: number | undefined = value?.getTime();
return value && !isNaN(time) ? [toTimestamp(value)] : [];
};
export const fromNullableTimestamp =
(value?: [] | [Time]): Date | undefined => {
return !isNaN(parseInt(`${value?.[0]}`)) ?
fromTimestamp(value[0]) : undefined;
};
Blob
Binary blobs are described in Candid as Array of numbers. To save untyped data in smart contracts (assuming the use case allows
such risk) while still preserving types on the frontend side, we can stringify objects, converts these to blobs and gets their contents as binary data contained in an ArrayBuffer.
export const toArray =
async <T>(data: T): Promise<Array<number>> => {
const blob: Blob = new Blob([JSON.stringify(data)],
{type: 'application/json; charset=utf-8'});
return [...new Uint8Array(await blob.arrayBuffer())];
};
To convert back an Array of numbers to a specific object type, the Blob type can be used again but, this time a textual conversion shall be used to parse the results.
export const fromArray =
async <T>(data: Array<number>): Promise<T> => {
const blob: Blob = new Blob([new Uint8Array(data)],
{type: 'application/json; charset=utf-8'});
return JSON.parse(await blob.text());
};
Both conversions are asynchronous because interacting with the blob object requires resolving promises in JavaScript.
Conclusion
I hope this short blog post and few utilities will be useful for you to start well with the Internet Computer, it is really a fun technology.
To infinity and beyond!
David
For more adventures, follow me on Twitter
(post original published Medium on Nov 16, 2021)