Difficulty: Beginner
Solidity provides a rich type system designed for the constraints of the EVM. Understanding data types is critical because they directly impact gas costs, storage layout, and security. Solidity types fall into two categories: value types (which are always copied when assigned or passed as arguments) and reference types (which can be passed by reference and must have a specified data location).
Value types include booleans (bool), integers (int/uint of various sizes), addresses (address/address payable), fixed-size byte arrays (bytes1 through bytes32), and enums. The uint type is an alias for uint256, a 256-bit unsigned integer - this is the EVM's native word size. Smaller integer types (uint8, uint16, uint32, etc.) exist and can save gas when packed together in storage, but individual operations on them are not cheaper because the EVM operates on 256-bit words internally. The address type holds a 20-byte Ethereum address, and address payable adds the ability to send ETH via .transfer() or .send().
Reference types include dynamically-sized arrays, fixed-size arrays, strings, bytes (dynamic byte array), structs, and mappings. These types require a data location annotation - storage, memory, or calldata - which determines where the data lives and how it behaves. Structs allow you to define custom composite types, grouping related data together. They can contain any type except mappings (when used in memory) and can be nested.
Mappings are one of Solidity's most important and unique data structures. A mapping is a hash table that maps keys to values, declared as `mapping(keyType => valueType)`. Unlike arrays or structs, mappings have no concept of length or iteration - you cannot enumerate all keys or values. Every possible key maps to a default value (0 for integers, false for bool, address(0) for addresses). Mappings can only exist in storage (not memory or calldata) and are the primary way to associate data with addresses - for example, tracking token balances with `mapping(address => uint256) public balances`.
State variables are stored permanently on the blockchain in contract storage, which is organized as a key-value store of 256-bit slots. How variables are packed into these slots has significant gas implications. Multiple variables smaller than 256 bits can be packed into a single slot if declared consecutively - for example, two uint128 values fit in one slot. This is called tight packing and can save substantial gas on storage reads and writes. Understanding storage layout is essential for gas optimization and for avoiding subtle bugs with delegatecall and proxy patterns.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract DataTypes {
// ============ Value Types ============
// Boolean
bool public isActive = true;
// Unsigned integers (uint8 to uint256)
uint8 public smallNumber = 255; // Max: 255
uint256 public bigNumber = 1e18; // 1 ETH in wei
uint public defaultUint = 42; // uint is alias for uint256
// Signed integers (int8 to int256)
int256 public temperature = -40;
// Address types
address public owner;
address payable public treasury;
// Fixed-size byte arrays
bytes32 public dataHash; // Often used for hashes
bytes1 public singleByte = 0xff;
// Enum
enum Status { Pending, Active, Completed, Cancelled }
Status public currentStatus = Status.Pending;
// ============ Reference Types ============
// String (dynamic, reference type)
string public name = "My Contract";
// Dynamic byte array
bytes public dynamicData;
// Fixed-size array
uint[5] public fixedArray = [1, 2, 3, 4, 5];
// Dynamic array
uint[] public dynamicArray;
// Struct
struct User {
string name;
address wallet;
uint256 balance;
bool isVerified;
uint256 registeredAt;
}
// Mapping (key => value hash table)
mapping(address => User) public users;
mapping(address => mapping(address => uint256)) public allowances; // Nested mapping
mapping(uint256 => address[]) public groupMembers; // Mapping to array
// Array of structs
User[] public allUsers;
// ============ Constants and Immutables ============
// Constants are evaluated at compile time (no storage slot)
uint256 public constant MAX_SUPPLY = 1_000_000 * 1e18;
address public constant DEAD_ADDRESS = address(0xdead);
// Immutables are set once in the constructor (cheaper than regular state vars)
uint256 public immutable deploymentTime;
uint256 public immutable chainId;
constructor(address payable _treasury) {
owner = msg.sender;
treasury = _treasury;
deploymentTime = block.timestamp;
chainId = block.chainid;
}
// ============ Working with Types ============
function registerUser(string calldata _name) external {
// Create a new User struct
User memory newUser = User({
name: _name,
wallet: msg.sender,
balance: 0,
isVerified: false,
registeredAt: block.timestamp
});
// Store in mapping
users[msg.sender] = newUser;
// Push to array
allUsers.push(newUser);
}
function addToArray(uint _value) external {
dynamicArray.push(_value);
}
function getArrayLength() external view returns (uint) {
return dynamicArray.length;
}
function updateStatus(Status _newStatus) external {
currentStatus = _newStatus;
}
// Type casting examples
function typeCasting() external pure returns (uint256) {
uint8 a = 100;
uint256 b = uint256(a); // Widening: safe, always works
uint16 c = 1000;
uint8 d = uint8(c); // Narrowing: truncates! d = 232 (1000 % 256)
return b + uint256(d);
}
}
This comprehensive example covers all major Solidity data types. Value types (bool, uint, int, address, bytes32, enum) are copied on assignment. Reference types (string, bytes, arrays, structs) require data location specifiers. Mappings are storage-only hash tables. Constants cost no gas to read (compiled into bytecode), and immutables are set once in the constructor and are cheaper than regular state variables.
uint, address, mapping, struct