Algorand Typescript
Algorand TypeScript is a partial implementation of the TypeScript programming language that runs on the Algorand Virtual Machine (AVM). It includes a statically typed framework for developing Algorand smart contracts and logic signatures, with TypeScript interfaces to underlying AVM functionality that works with standard TypeScript tooling.
It maintains the syntax and semantics of TypeScript, so a developer who knows TypeScript can make safe assumptions about the behavior of the compiled code when running on the AVM. Algorand TypeScript is also executable TypeScript that can be run and debugged on a Node.js virtual machine with transpilation to EcmaScript and run from automated tests.
Benefits of using Algorand Typescript
- Rapid development: Typescript’s concise syntax allows for quick prototyping and iteration of smart contract ideas.
- Lower barrier to entry: Typescript’s popularity means more developers can transition into blockchain development without learning a new language.
- Ease of Use: Algorand Typescript is designed to work with standard Typescript tooling, making it easy for developers familiar with Typescript to start building smart contracts on Algorand.
- Efficiency: Algorand Typescript is compiled for execution on the AVM by PuyaTS, an optimizing compiler that ensures the resulting AVM bytecode execution semantics match the given Typescript code. This makes deployment and calling easy.
- Modularity: Algorand Typescript supports modular solution components, facilitating efficient parallel development by small, effective teams, reducing architectural complexity, and allowing developers to pick and choose the specific tools and capabilities they want to use based on their needs and what they are comfortable with.
Typescript Implementation for AVM
Algorand Typescript maintains the syntax and semantics of Typescript, supporting a subset of the language that will grow over time. However, due to the restricted nature of the AVM, it will never be a complete implementation.
Algorand TypeScript is compiled for execution on the AVM by PuyaTs, a TypeScript frontend for the Puya optimizing compiler that ensures the resulting AVM bytecode execution semantics that match the given TypeScript code. PuyaTs produces output directly compatible with AlgoKit-typed clients to simplify deployment and calling.
Differences from Standard Typescript
- Types Affect Behavior: In TypeScript, using types, as expressions, or type arguments don’t affect the compiled JS. In Algorand Typescript, however, types fundamentally change the compiled TEAL. For example, the literal expression 1 results in int 1 in TEAL, but 1 as uint8 results in byte 0x01. This also means that arithmetic is done differently on these numbers and they have different overflow protections.
- Numbers Can Be Bigger: In TypeScript, numeric literals with absolute values equal to 2^53 or greater are too large to be represented accurately as integers. In Algorand Typescript, however, numeric literals can be much larger (up to 2^512) if properly type casted as uint512.
- Types May Be Required: All JavaScript is valid TypeScript, but that is not the case with Algorand Typescript. In certain cases, types are required and the compiler will throw an error if they are missing. For example, types are always required when defining a method or when defining an array.
Supported Primitives
Algorand TypeScript supports several primitive types and data structures that are optimized for blockchain operations. These primitives are designed to work efficiently with the AVM while maintaining familiar TypeScript syntax. Understanding these primitives and their constraints is crucial for writing performant smart contracts.
Static Arrays
Static arrays are the most efficient and capable type of arrays in TypeScript for Algorand development. They have a fixed length and offer improved performance and type safety. For example, StaticArray <uint64, 10>
for an array of 10 unsigned 64-bit integers.
Static arrays can be partially initialized. Uninitialized elements default to undefined or zero bytes, depending on the context.
const x: <StaticArray, 3> = [1] // [1, undefined, undefined]const y: <StaticArray, 3> = [1, 0, 0] // [1, 0, 0]
To iterate over a static array, use for...of
which provides a clean syntax and supports continue/break statements:
staticArrayIteration(): uint64 { const a: StaticArray<uint64, 3> = [1, 2, 3]; let sum = 0;
for (const v of a) { sum += v; } return sum; // 6 }
Supported Methods: length
Dynamic Arrays
Dynamic arrays are supported in Algorand Typescript. Algorand Typescript will chop off the length prefix of dynamic arrays during runtime. Nested dynamic types are encoded as dynamic tuples, this requires much more opcodes to read/write the tuple head and tail values.
Supported Methods: pop
, push
, splice
, length
Pass by Reference
All arrays and objects are passed by reference even if in contract state, much like TypeScript. Algorand Typescript, however, will not let a function mutate an array that was passed as an argument. If you wish to pass by value you can use clone.
const x: uint64[] = [1, 2, 3];const y = x;y[0] = 4;
log(y); // [4, 2, 3]log(x); // [4, 2, 3]
const z = clone(x);z[1] = 5;
log(x); // [4, 2, 3] note x has NOT changedlog(z); // [4, 5, 3]
When instantiating an array or object, a type MUST be defined. For example, const x: uint64[] = [1, 2, 3]
. If you omit the type, the compiler will throw an error.
Objects
Object can be defined much like in TypeScript. The same efficiencies of static vs dynamic types also applies to objects. Under the hood, Algorand Typescript objects are just tuples. For example [uint64, uint8] is the same byteslice as { foo: uint64, bar: uint8 }
. The order of elements in the tuple depends on the order they are defined in the type definition. For example, the following definitions result in the same byteslice.
type MyType = { foo: uint64, bar: uint8 }...const x: MyType = { foo: 1, bar: 2}const y: MyType = { bar: 2, foo: 1 }
Numbers
Integers
The Algorand Virtual Machine (AVM) natively supports unsigned 64-bit integers (uint64). Using uint64 for numeric operations ensures optimal performance. You can, however, use any of the number types defined in ARC-0004. You can define specific-width unsigned integers with the uint<N>
generic type. This type takes one type argument, which is the bit width. The bit width must be divisible by 8.
// Correct: Unsigned 64-bit integerconst n1: UInt<64> = 1;
// Correct: Unsigned 8-bit integerconst n2: UInt<8> = 1;
Unsigned Fixed-Point Decimals
To represent decimal values, use the ufixed<N, M>
generic type. The first type argument is the bit width, which must be divisible by 8. The second argument is the number of decimals places, which must be less than 160.
// Correct: Unsigned 64-bit with two decimal placesconst price: UFixed<64, 2> = 1.23;
// Incorrect: Missing type definitionconst invalidPrice = 1.23; // ERROR: Missing type
// Incorrect: Precision exceeds defined decimal placesconst invalidPrice2: UFixed<64, 2> = 1.234; // ERROR: Precision of 2 decimal places, but 3 provided
Math Operations
Algorand TypeScript requires explicit handling of math operations to ensure type safety and prevent overflow errors. Here are the key points about math operations:
-
Basic arithmetic operations (+, -, *, /) are supported but require explicit type handling
-
Results of math operations must be explicitly typed using either:
- A constructor:
const sum = Uint64(x + y)
- Type annotation:
const sum: uint64 = x + y
- Return type annotation:
function add(x: uint64, y: uint64): uint64 { return x + y }
- A constructor:
-
For non-uint64 types, overflow checks are performed at construction time:
const a = UintN8(255);const b = UintN8(255);const c = UintN8(a + b); // Error: Overflow
- For better performance with smaller integer types, use uint64 for intermediate calculations:
const a: uint64 = 255;const b: uint64 = 255;const c: uint64 = a + b;return UintN8(c - 255); // Only convert at the end
Limitations
While TypeScript offers a rich set of primitives, certain features and types are either unsupported or have significant limitations within the Algorand ecosystem.
- Dynamic types and booleans are much more expensive to use and have some limitations.
- Anything beyond dynamic arrays of static types is very inefficient and hence not recommended. For example,
uint64[]
is fairly efficient butuint64[][]
is much less efficient. Nested dynamic types are encoded as dynamic tuples, this requires much more opcodes to read/write the tuple head and tail values - Algorand Typescript will not let a function mutate an array that was passed as an argument.
- For instantiating a static array by putting the length in a bracket (i.e.,
uint64[10]
) is NOT valid TypeScript syntax thus not officially supported by Algorand Typescript. forEach
is not supported in Algorand TypeScript. Usefor...of
loops instead, which also enables continue/break functionality.- Dynamic arrays support the
splice
method but it is rather heavy in terms of opcode cost so it should be used sparringly. - No Object methods are supported in Algorand Typescript.
- At the TypeScript level, all numbers are aliases to the standard number class. This is to ensure all arithmetic operators function on all numeric types as expected since they cannot be overwritten in TypeScript. As such, any number-related type errors might not show in the IDE and will only throw an error during compilation.
PuyaTs Compiler
Algorand TypeScript is compiled for execution on the AVM by PuyaTs, a TypeScript frontend for the Puya optimising compiler that ensures the resulting AVM bytecode execution semantics that match the given TypeScript code. PuyaTs produces output that is directly compatible with AlgoKit typed clients to make deployment and calling easy.
Testing and Debugging
The algorand-typescript-testing
package allows for efficient unit testing of Algorand TypeScript smart contracts in an offline environment. It emulates key AVM behaviors without requiring a network connection, offering fast and reliable testing capabilities with a familiar TypeScript interface.
Best Practices
- Use Static Types: Always define explicit types for arrays, tuples, and objects to leverage TypeScript’s static typing benefits.
- Prefer
UInt<64>
: UtilizeUInt<64>
for numeric operations to align with AVM’s native types, enhancing performance and compatibility. - Use the StaticArray generic type to define static arrays and avoid specifying array lengths using square brackets (e.g., number[10]) as it is not valid TypeScript syntax in this context.
- Limit Dynamic Arrays: Avoid excessive use of dynamic arrays, especially nested ones, to prevent inefficiencies. Also, splice is rather heavy in terms of opcode cost so it should be used sparringly.
- Immutable Data Structures: Use immutable patterns for arrays and objects. Instead of mutating arrays directly, create new arrays with the desired changes (e.g.,
myArray = [...myArray, newValue]
). - Efficient Iteration: Use
for...of
loops for iterating over arrays, which also enables continue/break functionality. - Type Casting: Use constructors (e.g.,
UintN8
,UintN<64>
) rather thanas
keyword for type casting.