Skip to main content

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 PatternBehavior
No this.* or EVM global accessPerforms computation only, no blockchain data needed (free to call)
Reads this.* onlyCan 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.valueCan 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:

TypeScriptBehavior
public (or no modifier)Anyone can call this function
privateOnly callable from within this contract
protectedOnly callable from this contract and child contracts
static methodsInternal 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 implementation
  • bid(uint256 amount) — a forwarding overload that calls bid(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 implementation
  • configure(uint256 a, uint256 b) — forwards to configure(a, b, 10)
  • configure(uint256 a) — forwards to configure(a, 5, 10)
caution

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.

info

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.

info

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:

contracts/utils.ts
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)) {
// ...
}
}
}