Solidity Mapping Type Made Easy [+Video]

In this article, you’ll dive into detail on the Solidity mapping type and learn about mapping type specifics.

It’s part of our long-standing tradition to make this (and other) articles a faithful companion, or a supplement to the official Solidity documentation, starting with docs for this article’s topic.

Mapping Type

We have seen mapping type many times before this article (e.g., here and here), but we’ll say a few more words about it this time. So far in our Solidity tutorial series, it has proven to be a very practical data type/data structure, but getting to know it better will provide us with an even better knowledge toolkit.

Mapping types use syntax in the form of mapping(KeyType => ValueType), which is also the way we declare the variables of mapping type: mapping(KeyType => ValueType) variableName.

For the KeyType we can use any of the built-in value types, such as bytes, string, contract, or enum type. Besides those, other user-defined or complex types, such as mappings, structs, or array types are not supported to be used as the KeyType.

On the other hand, valueType can be literally any type, also including mapping, array, or struct type.

The right way to understand how mapping type works is by imagining a hash table, but a special one, which is virtually initialized so that every possible key already exists. Also, each (virtually initialized) key is mapped to a value with a byte representation set to all zeros, which is the mapping’s default value.

Since mapping is virtually pre-initialized to default values, there is no length property or a special way of setting keys or values, dissimilar to some other, popular programming languages that usually have a specific way of setting and unsetting their mapping-equivalent elements (such as with dictionaries).

Therefore, keys and values cannot be erased without additional information about the keys with specifically assigned values, since there is no mechanism for differentiating between the keys containing default (zero) values or the keys containing some non-zero value; they all look like they have some assigned value.

Even more, there is no way to tell if a zero value is explicitly assigned, or if it’s a default value.

If we would have a dynamic storage array with mapping as its base type, clearing the array would set its length property to 0, but reallocating the array would still refer to the same mappings which would remain intact (docs).

Mapping Specifics

As mappings exist only in storage memory area and therefore can only have roles of state variables, storage reference types in functions, or as parameters for library functions.

Mappings cannot be used as parameters or return parameters of publicly visible contract functions, and by extension, the same rule applies to arrays and structs containing mappings.

When we create a state variable of mapping type and declare it as public, Solidity will automatically create a getter function, and the KeyType becomes a parameter for the getter function (docs).

In that case, if the ValueType is a value type or a struct, type, the getter function returns the ValueType. If ValueType is itself an array or a mapping type, the getter has one parameter for each KeyType.

This pattern continues recursively, i.e. for each level of recursion, another parameter is added to the getter, such as in the example:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;

contract MultiDepthMapping {
    mapping(uint => mapping(uint => uint)) public multiDepthMap;

    function setMapValue(uint depth_0, uint depth_1, uint val)
    public
    {
        multiDepthMap[depth_0][depth_1] = val;
    }
}

By executing this contract example, we would get an automatically generated getter function multiDepthPath(), as illustrated with the picture:

πŸ’‘ Note: We should remember that a public variable is dual in nature in that it can be accessed both as an internal and an external variable. When accessing a public variable as an external variable, we use syntax this.<variableName> and this invokes the getter function of the variable. Otherwise, when we access the variable as an internal variable, we’d use syntax <variableName>, and the variable is read directly from the storage, i.e. its getter function is not used.

In the next example, the MappingData contract contains a public balances mapping, whose key type is an address, and the value type is a uint. We use this mapping to map an Ethereum address to an unsigned integer, i.e. uint value.

As we just mentioned, since uint is our value type, the getter will return its value matching its type, i.e. uint.

This behavior is also reflected in the mappingUser contract, which returns the value for a specified address via the m’s getter function (m is a variable of MappingData type).

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;

contract MappingData {
    mapping(address => uint) public balances;

    function update(uint newBalance) public {
        balances[msg.sender] = newBalance;
    }
}

contract MappingUser {
    function f() public returns (uint) {
        MappingData m = new MappingData();
        m.update(100);
        return m.balances(address(this));
    }
}

Another example represents a simplified version of an ERC20 token, where _allowances state variable shows a mapping placed inside another mapping, i.e. nested mapping, and is used for recording the amount a client is allowed to withdraw from the account.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;

contract MappingExample {

    mapping (address => uint256) private _balances;
    mapping (address => mapping (address => uint256)) private _allowances;

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);

    function allowance(address owner, address spender) public view returns (uint256) {
        return _allowances[owner][spender];
    }

    function transferFrom(address sender, address recipient, uint256 amount) public returns (bool) {
        require(_allowances[sender][msg.sender] >= amount, "ERC20: Allowance not high enough.");
        _allowances[sender][msg.sender] -= amount;
        _transfer(sender, recipient, amount);
        return true;
    }

    function approve(address spender, uint256 amount) public returns (bool) {
        require(spender != address(0), "ERC20: approve to the zero address");

        _allowances[msg.sender][spender] = amount;
        emit Approval(msg.sender, spender, amount);
        return true;
    }

    function _transfer(address sender, address recipient, uint256 amount) internal {
        require(sender != address(0), "ERC20: transfer from the zero address");
        require(recipient != address(0), "ERC20: transfer to the zero address");
        require(_balances[sender] >= amount, "ERC20: Not enough funds.");

        _balances[sender] -= amount;
        _balances[recipient] += amount;
        emit Transfer(sender, recipient, amount);
    }
}

πŸ’‘ Info: ERC20 is the technical standard for fungible tokens created using the Ethereum blockchain.

A fungible token is one that is interchangeable with another tokenβ€”where the well-known non-fungible tokens (NFTs) are not interchangeable…

Tokens, in this regard, are a representation of an asset, right, ownership, access, cryptocurrency, or anything else that is not unique in and of itself but can be transferred. (more)

Conclusion

In this article, we analyzed the mapping type and went into detail about its specifics, ways of use, limitations, etc.

First, we discussed what can types can be used for the KeyType and ValueType parts of mapping. We also mentioned how an array would behave if it had mapping as its base type.

Second, we learned about where mapping type variables can be used, what roles they can acquire, and what limitations they have in terms of usage. We also illustrated how their getter functions behave and work.

What’s Next?

This tutorial is part of our extended Solidity documentation with videos and more accessible examples and explanations. You can navigate the series here (all links open in a new tab):