Functions
Class methods define the actions users can take on your contract. Skittles automatically handles optimization and access control.
Basic Functions
class Token {
private balances: Record<address, number> = {};
// A pure helper — doesn't touch any contract state
add(a: number, b: number): number {
return a + b;
}
// A read-only function — looks up state without changing it
balanceOf(account: address): number {
return this.balances[account];
}
// A state-changing function — modifies balances
transfer(to: address, amount: number): boolean {
this.balances[msg.sender] -= amount;
this.balances[to] += amount;
return true;
}
}
State Mutability Inference
You never need to annotate how your functions interact with the blockchain. Skittles analyzes each function body to determine the behavior:
| Access Pattern | Behavior |
|---|---|
No this.* or EVM global access | Performs computation only, no blockchain data needed (free to call) |
Reads this.* only | Can be called without a transaction (free) |
Reads EVM globals (msg.sender, block.*, tx.*, self, gasleft()) | Can be called without a transaction (free) |
Writes this.* | Requires a transaction (costs gas) |
Accesses msg.value | Can receive ETH payments |
The inference also propagates through call chains. If function A calls this.B(), and B writes state, then A is also marked as state modifying. This uses a fixpoint iteration to handle indirect call chains.
Visibility
Function visibility controls who can call your functions:
| TypeScript | Behavior |
|---|---|
public (or no modifier) | Anyone can call this function |
private | Only callable from within this contract |
protected | Only callable from this contract and child contracts |
static methods | Internal helpers (not callable externally) |
class Token {
// Public function - anyone can call
public transfer(to: address, amount: number): boolean {
/* ... */
}
// Internal helper - only this contract can use
private _transfer(from: address, to: address, amount: number): void {
/* ... */
}
}
Virtual and Override
By default, all functions can be overridden by child contracts. Use the override keyword to mark a function as overriding a parent:
class BaseToken {
transfer(to: address, amount: number): boolean {
// base implementation
return true;
}
}
class MyToken extends BaseToken {
override transfer(to: address, amount: number): boolean {
// custom implementation that replaces the parent's
return true;
}
}
Function Overloading
TypeScript supports function overloading through overload signatures, and Skittles compiles these into separate Solidity functions. This is commonly used in Solidity standards like ERC721's safeTransferFrom:
class Token {
transfer(to: address, amount: number): boolean;
transfer(to: address, amount: number, data: string): boolean;
transfer(to: address, amount: number, data?: string): boolean {
// implementation
return true;
}
}
The overload signatures (without bodies) define the public API, while the implementation signature (with the body) provides the logic. Skittles generates a separate Solidity function for each overload signature:
- The overload with the most parameters gets the implementation body
- Shorter overloads automatically forward to the longest overload with default values
Default Parameter Values
Function parameters can have default values, just like in TypeScript. Skittles generates overloaded Solidity functions so callers can omit trailing arguments:
class Auction {
public bid(amount: number, maxGas: number = 100000): boolean {
// implementation
return true;
}
}
This generates two Solidity functions:
bid(uint256 amount, uint256 maxGas)— the full implementationbid(uint256 amount)— a forwarding overload that callsbid(amount, 100000)
Multiple default parameters work as expected. Each trailing default creates an additional overload:
class Auction {
public configure(a: number, b: number = 5, c: number = 10): number {
return a + b + c;
}
}
This generates three Solidity functions:
configure(uint256 a, uint256 b, uint256 c)— full implementationconfigure(uint256 a, uint256 b)— forwards toconfigure(a, b, 10)configure(uint256 a)— forwards toconfigure(a, 5, 10)
Default-valued parameters must be contiguous and trailing. Patterns that put a non-default parameter after a default (for example, f(a: number = 1, b: number)) are rejected by the compiler, even though they are valid TypeScript. Always list all required (non-default) parameters first, followed by all parameters with defaults.
Constructor parameters also support default values, but use a different strategy: default parameters become local variable declarations inside the constructor body instead of generating overloads.
Arrow Functions
Arrow function properties work just like regular methods:
class Token {
private _validate = (amount: number): boolean => {
return amount > 0;
};
}
Multiple Return Values
Functions can return multiple values using TypeScript tuple types. This is common in Solidity for functions like getReserves():
class Pair {
private reserve0: number = 0;
private reserve1: number = 0;
getReserves(): [number, number, number] {
return [this.reserve0, this.reserve1, block.timestamp];
}
}
The tuple return type [number, number, number] compiles to Solidity's multi-value return returns (uint256, uint256, uint256), and the array literal [a, b, c] compiles to a Solidity tuple (a, b, c).
You can also destructure tuple return values directly:
class Pair {
private reserve0: number = 0;
private reserve1: number = 0;
getReserves(): [number, number] {
return [this.reserve0, this.reserve1];
}
public getSum(): number {
const [r0, r1] = this.getReserves();
return r0 + r1;
}
}
This compiles to Solidity's native tuple destructuring: (uint256 r0, uint256 r1) = getReserves();
Getters and Setters
TypeScript get and set accessors work as you'd expect:
class Token {
private _paused: boolean = false;
get paused(): boolean {
return this._paused;
}
set paused(value: boolean) {
this._paused = value;
}
}
Receive and Fallback
Name a method receive to handle plain ETH transfers to your contract. This function is called when someone sends ETH to your contract:
class Staking {
public receive(): void {
this._deposit(msg.sender, msg.value);
}
}
Similarly, name a method fallback to handle calls to functions that don't exist on your contract.
Require Optimization
Skittles automatically optimizes your error handling. When you write if (condition) throw new Error(...), Skittles converts it into the most gas-efficient pattern:
// TypeScript
if (this.balances[msg.sender] < amount) {
throw new Error("Insufficient balance");
}
The condition is automatically negated, and comparison operators are flipped (< becomes >=, == becomes !=, etc.) to produce the most efficient bytecode.
This optimization only applies when the if block contains a single throw new Error(...) statement with no else branch. Custom errors (SkittlesError) use a different optimization pattern.
Standalone Functions
Functions declared outside of classes (at file level) are compiled as internal helper functions available to all contracts in that file:
function calculateFee(amount: number, bps: number): number {
return (amount * bps) / 10000;
}
These can also be arrow functions:
const calculateFee = (amount: number, bps: number): number => {
return (amount * bps) / 10000;
};
When shared across files, standalone functions are available to all contracts. See Cross File Support.
Type Guards
TypeScript type guard functions using the is keyword are supported. The is annotation is stripped and the function compiles to a standard boolean-returning internal function:
enum Status { Active, Paused, Stopped }
function isActive(s: Status): s is Status.Active {
return s == Status.Active;
}
export class Vault {
status: Status;
public doAction(): void {
if (isActive(this.status)) {
// ...
}
}
}