Skip to content

Solidity Data Types, Mappings and Structs

DodaTech Updated 2026-06-22 7 min read

In this tutorial, you'll learn Solidity data types including value types, mappings, structs, arrays, and storage location best practices. Why it matters: choosing the right data structures in Solidity directly impacts gas costs, contract security, and overall efficiency of your decentralized applications. By the end, you'll confidently use mappings, structs, and arrays in your contracts.

Solidity data types fall into two categories: value types (passed by value) and reference types (passed by reference), with storage locations determining how data persists and how much gas operations cost.

Value Types in Solidity

Value types include booleans, integers, addresses, and fixed-size byte arrays. They are copied when assigned or passed to functions.

Type Size Range Example
bool 1 byte true/false bool public isActive = true;
uint256 32 bytes 0 to 2^256-1 uint256 public balance = 1000;
uint8 1 byte 0 to 255 uint8 public small = 200;
int256 32 bytes -2^255 to 2^255-1 int256 public temp = -10;
address 20 bytes Ethereum address address public owner = msg.sender;
address payable 20 bytes Address with transfer payable(addr).transfer(1 ether);
bytes32 32 bytes Fixed bytes bytes32 public hash;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

contract ValueTypesDemo {
    bool public isComplete;
    uint256 public counter;
    address public creator;
    
    constructor() {
        isComplete = false;
        counter = 100;
        creator = msg.sender;
    }
    
    function toggle() external {
        isComplete = !isComplete;
    }
    
    function increment(uint256 _amount) external {
        // SafeMath is built-in in Solidity 0.8+
        counter += _amount;
    }
    
    function getCreatorBalance() external view returns (uint256) {
        return creator.balance;
    }
}

Expected behavior: toggle() flips isComplete between true and false. increment(5) changes counter to 105. getCreatorBalance() returns the creator's ETH balance.

Understanding Mappings

Mappings are key-value stores that provide O(1) lookup. They are the most commonly used data structure in Solidity for tracking balances, permissions, and state.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

contract MappingExample {
    // Simple mapping: address => balance
    mapping(address => uint256) public balances;
    
    // Nested mapping: tokenId => owner => allowance
    mapping(uint256 => mapping(address => uint256)) public allowances;
    
    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }
    
    function withdraw(uint256 _amount) external {
        require(balances[msg.sender] >= _amount, "Insufficient balance");
        balances[msg.sender] -= _amount;
        payable(msg.sender).transfer(_amount);
    }
    
    function setAllowance(uint256 _tokenId, address _spender, uint256 _amount) external {
        allowances[_tokenId][_spender] = _amount;
    }
}

Expected behavior: Users deposit ETH and their balance increases. They withdraw up to their balance. Token owners can set allowances for spenders on specific token IDs.

Working with Structs

Structs let you define custom data types combining multiple fields. They are often used with mappings to create structured records.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

contract StructExample {
    struct User {
        string name;
        uint256 age;
        address wallet;
        bool isVerified;
    }
    
    mapping(address => User) public users;
    address[] public userList;
    
    function registerUser(string memory _name, uint256 _age) external {
        require(bytes(users[msg.sender].name).length == 0, "Already registered");
        
        User storage newUser = users[msg.sender];
        newUser.name = _name;
        newUser.age = _age;
        newUser.wallet = msg.sender;
        newUser.isVerified = false;
        
        userList.push(msg.sender);
    }
    
    function verifyUser(address _user) external {
        users[_user].isVerified = true;
    }
    
    function getUserCount() external view returns (uint256) {
        return userList.length;
    }
}

Expected behavior: Users register with name and age. An admin verifies them. getUserCount() returns total registered users. Each user struct is stored in a mapping keyed by address.

flowchart LR
  A[User Address] --> B[mapping
address => User] B --> C[User Struct] C --> D[name] C --> E[age] C --> F[wallet] C --> G[isVerified]

Arrays in Solidity

Solidity supports dynamic and fixed-size arrays. Storage arrays are expensive to modify but efficient to read.

Array Type Syntax Gas Cost Use Case
Fixed uint256[10] Low Known size, limited elements
Dynamic uint256[] High Unknown or growing size
Storage storage ref Very high Persistent array
Memory memory ref Low Temporary processing
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

contract ArrayExample {
    uint256[] public dynamicArray;
    uint256[10] public fixedArray;
    
    function addElement(uint256 _value) external {
        dynamicArray.push(_value);
    }
    
    function removeLast() external {
        require(dynamicArray.length > 0, "Empty array");
        dynamicArray.pop();
    }
    
    function getArray() external view returns (uint256[] memory) {
        return dynamicArray;
    }
    
    function sumArray() external view returns (uint256) {
        uint256 total;
        uint256 len = dynamicArray.length;
        for (uint256 i; i < len; i++) {
            total += dynamicArray[i];
        }
        return total;
    }
}

Expected behavior: addElement(42) appends to the array. removeLast() removes the last element. getArray() returns all elements. sumArray() returns the total.

Data Location Rules

Understanding storage, memory, and calldata is critical for writing efficient Solidity.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

contract DataLocationDemo {
    struct Item {
        uint256 id;
        string name;
    }
    
    mapping(uint256 => Item) public items;
    
    // calldata: read-only, cheapest for external params
    function addItem(uint256 _id, string calldata _name) external {
        items[_id] = Item(_id, _name);  // Copy from calldata to storage
    }
    
    // memory: temporary copy, good for processing
    function getItem(uint256 _id) external view returns (Item memory) {
        return items[_id];  // Copy from storage to memory
    }
    
    // storage: direct reference, expensive first access
    function updateItem(uint256 _id, string calldata _name) external {
        Item storage item = items[_id];  // Reference to storage
        item.name = _name;  // Modify directly in storage
    }
}

Expected behavior: addItem stores data from calldata to storage. getItem copies from storage to memory. updateItem directly modifies storage via a reference.

Common Errors and Misunderstandings

1. Unbounded Array Loops

Iterating over dynamic arrays that grow unbounded can exceed block gas limit. Use mappings with separate length tracking or pagination.

2. Mapping Iteration

Mappings are not iterable by default. You must maintain a separate array of keys to iterate over mapping entries.

3. Struct Memory Assignment

Assigning a struct from storage to memory creates a copy. Modifying the memory copy does not update storage.

4. Array Push Cost

push() on storage arrays is expensive (20k+ gas for new elements). Use it sparingly or batch updates.

5. String Comparisons

Strings are dynamic byte arrays and cannot be compared with ==. Use keccak256(abi.encodePacked(a)) == keccak256(abi.encodePacked(b)).

Practice Questions

  1. What is the difference between memory and calldata? Both are temporary. memory can be modified within a function. calldata is read-only and only available for external function parameters. calldata is cheaper.

  2. Why can't you iterate over a mapping directly? Mappings are hash tables without stored keys. You cannot enumerate keys without maintaining a separate array of keys alongside the mapping.

  3. What happens when you pass a struct to a function? It depends on the data location. storage passes a reference (modifications persist), memory passes a copy (modifications are temporary), and calldata is read-only.

  4. How do you delete a mapping entry? Use delete mappingName[key]. This resets the value to its default (zero for integers, empty for strings, zero address for addresses).

  5. What is the default value for an uninitialized uint256? Zero. All uninitialized storage variables in Solidity default to their zero-value representation.

Challenge

Build a student gradebook contract using mappings of structs. Each student has a name, ID, and array of grades. Implement functions to add students, record grades, calculate averages, and list all students. Use storage references and avoid unbounded loops.

Real-World Task

Create a decentralized task management system where users create projects with struct-based tasks, assign them to addresses, and track completion status. Use TypeScript with Hardhat to deploy and test the contract. This pattern mirrors real project management dApps on Ethereum.

Frequently Asked Questions

What is the most gas-efficient data structure in Solidity?

Mappings are the most gas-efficient for lookups at O(1) cost. However, they cannot be iterated. The most efficient combination is a mapping for data storage plus a separate array for enumeration when iteration is needed.

Can you return a struct from a Solidity function?

Yes, but the struct must be declared with memory in the return type. All fields of the struct are returned as a tuple. For complex structs with many fields, this can be expensive and may exceed gas limits.

What is the maximum size of a Solidity array?

Dynamic storage arrays can theoretically hold up to 2^256 - 1 elements, but practical limits are set by block gas limits. A single transaction cannot iterate over or manipulate very large arrays.

Next Steps

Now that you understand Solidity data types and structures, proceed to Functions, Modifiers and Events for advanced contract patterns. Then explore Smart Contract design patterns for building production-ready decentralized applications.

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro