Introduction to Solidity types

One of the first important steps in learning Solidity, a statically-typed language, is to get a good understanding of the types available to use. So here they are!

Note: This was written for the latest Solidity version at the time of writing which is 0.8.1.

Value Types

Variables with value types hold data directly and passing them as argument or assignment creates a copy. Let's start with the simplest of them!

Boolean

They are declared with the bool keyword and can be either true or false

bool myBoolean;   // declaration
myBoolean = true; // assignment

Integer

You can specify if a variable is a signed integer with int or unsigned with uint followed by their range by appending the number of bits needed.
For example uint8 represents an unsigned integer of 8 bits.
It goes in increment of 8 from int8 to int256 and from uint8 to uint256.  

💡 If you simply declare an int or uint it will take the maximum range by default of 256 bits.

Integer Literal

Interpreted as decimals, integer literals are comprised of numbers. Ex: 69, 88, 11...

Decimals are possible such as .3 , 1.6.

And so is scientific notation: 3e10 , -4.3e-2

💡 For better readability integers can be written with an underscore, i.e 456_000 and 456000 are both valid and equivalent.

String Literal

String literals can either be written with single ('unicorn') or double quotes ("sushi").

💡 "a long" " text" is considered the same as "a long text"

So what if you actually want to have a string with quotes in it? You need to escape them. Escaping a character is done by adding \ in front. So '\"hello!\"' becomes "hello!". Other characters that need to be escaped include newline, backslash, carriage return etc...

💡 By default string literals can only contain ASCII but unicode literals with UTF-8 encoding can be written like this:

string memory congrats = unicode"Congrats! 🎉"

Hexadecimal Literal

Hexadecimal literals are recognizable as they start with the prefix hex such as hex"0d2c". They behave a lot like string literals.

Enum

An enum is a convenient way to declare your own type with a limited, well-defined list of options:

enum CustomerType { EarlyBird, Regular, VIP }
CustomerType customer;

Behind the scenes, options are just integers starting from 0 for the first option (which is also the enum's default value). Hence, enums are explicitly convertible from and to integers.

Address

Here's where we are starting to see how Solidity was uniquely designed to write smart contracts.

They are 2 ways to declare an address:

address plainAddress;

address payable payableAddress;

The difference is that an address payable is meant to receive Ether through a method such as .transfer() which is not available on a "simple" address.

One being a sort of extension of the other, it makes sense that you can cast an address payable to an address implicitly...

address myAddress = payableAddress;

...But the opposite has to be done explicitly.

address payable foo = payable(plainAddress); 

You can learn more about the possible conversions here.

The balance of an address can be obtained by simply writing myAddress.balance. For the current contract's balance, you first convert it to an address address(this) so it becomes: address(this).balance

Address Literal

It's an hexadecimal literal such as 0xBA2E7Fed597fd0E3e70f5130BcDbbFE06bB94fe1 that matches the right format of an Ethereum address. It has to pass a checksum test to be hardcoded in a contract like this:

address deployedContract = 0xBA2E7Fed597fd0E3e70f5130BcDbbFE06bB94fe1;

It has the type address and not address payable starting from Solidity 0.8 but can be converted to the latter.

Contract

Contracts have their own type which can be converted:

  • to a contract they inherit from
  • to or from an address:

address contractAddress = address(MyContract)

⚠️ There are some subtleties in case you want to cast to an address payable and the contract does not have a receive or payable fallback function, you need to use payable(address(x)).  

On a contract type, the members are the external functions (myContract.getReward(args)) as well as all the public variables defined in it (myContract.foo): Solidity automatically creates a getter function of the same name for any variable marked as public.

Fixed-size byte array

If you want to store a sequence of bytes which you know won't change in size you can use one of the following types: byte (= bytes1), bytes2, bytes3, ..., bytes32. Pick the right size!

Function

Functions are defined this way:

function myFunction(<parameter types>) {private|internal|external|public} [pure|view|payable] [returns (<return types>)]

Let's unpack the few different options!
A function declaration starts with the function keyword, followed by the function name and then by parentheses which, optionally, contain parameters in between them such as transfer(address to, uint value)

You then need to specify and pick between

private vs. internal vs. external vs. public

Private functions can only be called from other functions inside the current contract. Their name generally start with _ by convention.

Internal functions are like private functions but can also be executed from contracts deriving from the contract they were defined in.

External functions are meant to be called from outside the contract declaring them. (You can only call them internally by using this.foo() instead of foo())

Public functions can be called from anywhere, both internally and externally.

⚠️ Needless to say that getting this right is important from a security perspective. It's good practice to restrict visibility as much as possible, only exposing what needs to be exposed. With newer versions of Solidity, a function's visibility must be declared. Using an older version and not specifying any of these keywords means opting for the default function visibility which is public.

pure vs. view vs. payable

  • payable: include this function type if you want your function to be able to receive ETH while being called.
    (Note that it doesn't mean it must be sent some eth to work. Depending on your implementation it can really well function with a payment of 0 ETH)
  • view: this function modifier ensures that the function does not modify the state, basically it doesn't save or change anything on the ethereum blockchain
  • pure: pure functions can neither save nor read data from the blockchain

💡 Marking a function with view or pure means that it doesn't cost any gas to call it from outside the current contract but does when called internally!

Function return types

Unless the function returns nothing you must state what types are returned:

uint public minimumContribution;
uint public contributorCount;
address public manager;

// ...
function getSummary() public view returns (uint, uint, address) {
        return (
            minimumContribution,
            contributorCount,
            manager
        );
    }

Reference Types

As opposed to value types, data location of a reference type must be declared: it corresponds to where the data is stored. There are 3 options:

  • storage: This is where to store permanent data written on the blockchain. It's the more expensive data location. Data in storage for a smart contract is like data in a file on your hard drive, it will exists as long the contract does.
  • memory: The memory location is for data that will be held temporarily during a function execution and discarded after. Data in memory is cheaper and analogous to data stored in RAM.
  • calldata: calldata is a special data location where function arguments are stored. This data is non-modifiable and non-persistent.
    💡 The data location for parameters of external functions must be calldata.

Array

There are 2 kinds of arrays: fixed-size arrays and dynamic-size arrays.

The way to declare them is different. For example:

  • uint[3] foo is an array that will always containing 3 unsigned integers
  • string[] bar is an array of strings of dynamic size

Either way, note that all items of an array must share the same type!

You can create arrays of any type, including array, which gives you an array of arrays. For instance, uint[][5] myArray is a dynamic array of arrays of 5 uint (each).  🤯
Arrays indices start at 0 so you access the third uint in the second dynamic arrays with myArray[1][2].
⚠️ Depending on the version of Solidity you're using, you might need to add pragma experimental ABIEncoderV2; at the top of your file to enable that.

Special arrays

bytes and string are types used for variables with arbitrary-length of raw byte data and arbitrary-length of string (UTF-8) data respectively. However, if the length can be limited to a certain number of bytes it is better to use a value type like bytes32 instead of bytes.

Storage vs memory arrays

You can create an array stored in memory with:

 function f(uint len) public pure {
       uint[] memory a = new uint[](len); // length is dynamically set
       return a.length;
 }

However its size can't change from the one provided and you can't push an element in it.

Array literals

string memory AAVE = 'AAVE';
string[3] memory bag = ['SNX', AAVE, 'UNI'];

In this example ['SNX', AAVE, 'UNI'] is an array literal.
Array literals are memory arrays of static size. Their elements, if not of the same type, should at least be convertible to the same type.

💡You can use an array slice to get only a part of an array:
- bar[2:] returns a slice from the 3rd element to the last
- bar[:3] returns a slice from the 1st element to the 3rd
- bar[1:3] returns a slice from the 2nd element to the 3rd

Struct

Structs are great for grouping variables together. They somewhat resemble objects in JavaScript. Here's how to declare one:

struct Proposal {	
    address owner;
    string name;
    string description;
    uint approvals;
}

and here's how to create an instance of our struct:

Proposal memory myProposal = Proposal({owner: msg.sender, name: "Proposal 1", description: "foo", approvals: 0});

💡 It is not possible for a struct to declare a member as a struct of its own type. However, it can contain another struct type, arrays or mappings.

struct OtherStruct {
    uint n;
}
    
struct MyStruct {
    uint[] myArray;							// ✅ OK
    mapping(address => bool) approvals;		// ✅ OK
    OtherStruct bar; 						// ✅ OK
    MyStruct foo;    						// ❌ NOT OK
}

Mapping

Mappings are like hash tables than can be declared with mapping(_keyType => _ValueType) mappingVariableName. For example in mapping(address => bool) approvers all keys have the same address type and all values are of boolean type. This is a major difference with Structs which contain key-value pairs of different types.

There are some good things to know and a few gotchas about mappings:

  • Mappings can be nested!
  • The value type can be any type (even Contract type).
  • All key-value pairs exist and are initialized with the default value according to the value type (empty string "" for a mapping of strings, false for booleans, etc...).
  • The key type cannot be a mapping, struct or array.
  • Keys are not stored! So there is no way to retrieve them all, or loop through all the assigned keys (or not without a data structure on top of it anyway). Mappings are used to lookup one value with a specific key.
  • Data location of a mapping can only be storage. As a result, mappings or arrays and structs containing a mapping cannot be used as parameters or parameters of public functions.

The more items you have in array the more costly it becomes to loop through it. If you face this problem, a much more efficient solution in terms of computation and gas can be to use a mapping instead.

🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁

This post is meant to be an introduction so a lot things about conversion, operations etc has been omitted. For more details on Solidity types, please refer to the official Solidity documentation, making sure to select the version corresponding to the version pragma of your Solidity files.
Comments? Questions? Feedback? You spotted a mistake? Please hit me up on Twitter @RaphaelRoullet !