Under the Hood
This section is for developers who want to understand how Skittles works internally. If you're just getting started with smart contracts, you can safely skip this — everything covered here happens automatically.
How Skittles Works
Skittles is a TypeScript to Solidity compiler. You write smart contracts as TypeScript classes, and Skittles compiles them into clean, readable Solidity source code. Hardhat (or any Solidity toolchain) then compiles that Solidity to EVM bytecode that runs on the blockchain.
TypeScript (.ts) → Parser → IR → Analysis → Codegen → Solidity (.sol) → Hardhat → ABI + Bytecode
The Four Stage Pipeline
-
Parse — Your TypeScript is parsed using the official TypeScript compiler API. Classes become contracts, properties become state variables, methods become functions.
-
Analyze — The parsed code is checked for common issues like unreachable code (statements after
returnorthrow) and unused local variables. These are reported as warnings to help you catch mistakes early. -
Generate — The intermediate representation is converted to valid Solidity. Type mappings, visibility, state mutability inference, and optimizations are applied automatically.
-
Compile — The generated Solidity is written to
build/solidity/. Hardhat (or another Solidity toolchain) compiles it to ABI and EVM bytecode.
Source Maps
When Skittles generates Solidity, it also produces a source map file (.sol.map) alongside each .sol file. This maps every generated Solidity line back to the original TypeScript source file and line number.
For example, if Hardhat reports an error on line 12 of Token.sol, you can look up line 12 in Token.sol.map to find the corresponding TypeScript line:
{
"sourceFile": "contracts/Token.ts",
"mappings": {
"4": 1,
"5": 2,
"8": 5,
"12": 9
}
}
Each key is a Solidity line number and each value is the corresponding TypeScript line number. This makes it easy to trace any Solidity compiler error or runtime revert back to your TypeScript source.
Type Mappings
Here's how TypeScript types map to Solidity types:
| TypeScript | Solidity | Notes |
|---|---|---|
number | uint256 | All numbers are unsigned 256-bit integers |
string | string | UTF-8 strings |
boolean | bool | true / false |
address | address | Ethereum address |
bytes | bytes | Raw byte data |
bytes32 | bytes32 | Fixed-size 32-byte value (hashes, keys) |
Record<K, V> | mapping(K => V) | Key-value storage |
T[] | T[] | Dynamic arrays |
| Type alias | struct | Custom data structures |
| Interface | Contract interface | External API definition |
| Enum | enum | Named constants |
Visibility Mappings
| TypeScript | Solidity | Notes |
|---|---|---|
public (or no modifier) | public | Generates an automatic getter |
private | internal | More gas-efficient than Solidity's private |
protected | internal | Same as private in output |
static readonly | constant | Compile-time constant |
readonly | immutable | Set once at deployment |
State Mutability Inference
Skittles analyzes each function body to determine its Solidity state mutability:
| Access Pattern | Solidity Mutability |
|---|---|
No this.* access | pure |
Reads this.* only | view |
Writes this.*, emits events, or deletes state | (default, no annotation) |
Accesses msg.value | payable |
This inference propagates through call chains — if function A calls function B, and B writes state, then A is also marked as state-modifying. For external contract calls via Contract<T>(), the compiler respects already-known mutability annotations on interface methods: if all external calls in a function target methods annotated as view or pure (e.g. from property signatures or implements resolution), the wrapper function's mutability is computed from its own body. Unannotated interface methods are treated conservatively as state-modifying.
Automatic Optimizations
Skittles applies several optimizations to generate idiomatic Solidity:
if/throw→require(): When anifblock contains onlythrow new Error("message")with noelse, it's converted torequire()with the condition negated- String truthiness → length check:
if (str)compiles toif (bytes(str).length > 0)andif (!str)compiles toif (bytes(str).length == 0)since Solidity doesn't support implicit string-to-bool conversion private→internal: TypeScriptprivatemaps to Solidityinternalrather thanprivatefor better gas efficiencyvirtualby default: All functions are markedvirtualso child contracts can override them- Address wrapping: 42-character hex string literals are automatically wrapped in
address(...) - Memory annotations:
memorykeywords are added to string and bytes parameters automatically for...ofdesugaring:for...ofloops over arrays are converted to index-basedforloopsswitch/case→if/else: Switch statements are converted to if/else chains (Solidity has no native switch)Number.MAX_VALUE→type(uint256).max: Maximum integer valueMath.min(a, b)/Math.max(a, b)→ helper functions: Auto-generates internal_min/_maxhelpers to ensure each argument is evaluated exactly onceMath.pow(a, b)→a ** b: Uses Solidity's exponentiation operatorMath.sqrt(x)→ Babylonian method: Auto-generates an internal_sqrthelper function- Template literals →
string.concat(): Template strings are converted to Solidity string concatenation **=→x = x ** y: The**=compound assignment is desugared because Solidity has no**=operator- Local variable shadowing prevention: When a local variable has the same name as a state variable, it's automatically renamed with an underscore prefix (e.g.
result→_result) to avoid Solidity shadowing warnings - Parameter shadowing prevention: When a function parameter has the same name as another function in the contract (e.g. a setter parameter
valueshadowing a getter functionvalue()), the parameter is automatically renamed to avoid Solidity shadowing warnings
Generated Solidity Example
Here's what a simple TypeScript contract looks like after compilation:
Input (TypeScript):
import { address, msg } from "skittles";
export class Token {
totalSupply: number = 0;
private balances: Record<address, number> = {};
constructor(supply: number) {
this.totalSupply = supply;
this.balances[msg.sender] = supply;
}
balanceOf(account: address): number {
return this.balances[account];
}
transfer(to: address, amount: number): boolean {
if (this.balances[msg.sender] < amount) {
throw new Error("Insufficient balance");
}
this.balances[msg.sender] -= amount;
this.balances[to] += amount;
return true;
}
}
Output (Solidity):
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Token {
uint256 public totalSupply;
mapping(address => uint256) internal balances;
constructor(uint256 supply) {
totalSupply = supply;
balances[msg.sender] = supply;
}
function balanceOf(address account) public view virtual returns (uint256) {
return balances[account];
}
function transfer(address to, uint256 amount) public virtual returns (bool) {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
balances[to] += amount;
return true;
}
}
Notice the automatic transformations:
number→uint256,Record<address, number>→mapping(address => uint256)private→internalbalanceOfis markedview(only reads state)if/throwis optimized torequire()- Functions are marked
virtualby default - String parameters get
memoryannotations
Why Solidity?
Skittles compiles to Solidity rather than directly to EVM bytecode for several important reasons:
- Security audits: Solidity has the largest ecosystem of security auditors and automated analysis tools
- Etherscan verification: You can verify your generated Solidity on Etherscan and other block explorers, making your contracts transparent
- Tooling compatibility: Works with every existing Solidity tool — Hardhat, Foundry, Remix, Slither, and more
- Readability: The generated code is human-readable, so you can always inspect what's being deployed
- Trust: Users and auditors can verify the generated Solidity matches the TypeScript source