​TypeScript Utilities For Candid On The IC

Photo by Uby Yanes on Unsplash
​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]; };
​toNullableconverts 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)