Table of contents

  1. Introduction
  2. Numeric types
  3. Strings
  4. Booleans
  5. Arrays and hash maps
  6. Byte arrays
  7. Nil
  8. Optional types
  9. Block types
  10. Object types
  11. Dynamic types
  12. Self types
  13. Void types


Inko comes with various types that are available by default. Some of these types are defined in the runtime, while others only exist at compile time.

Numeric types

There are two built-in numeric types in Inko: Integer, and Float. The Integer type is used for arbitrarily sized (signed) integers, while Float is used for IEEE 754 64 bits floating points.


The String type is used for strings, such as 'hello' and "hello". Both single and double quoted strings are of the exact same type.

Each String is a Unicode string, using UTF-8 as the encoding. Invalid UTF-8 sequences are replaced with the Unicode replacement sign � (U+FFFD).


There are two boolean types available in Inko: True, and False. Both these objects are instances of the Boolean object, though you should avoid using Boolean directly other than in type signature (e.g. when accepting a boolean argument).

Arrays and hash maps

Two collection types are available by default: Array, and HashMap. An Array is used for storing a list of values of the same type. A HashMap is a hash map used to map keys of type K to values of type V.

These collections can be used in type signatures like any other generic type:

def take_array_of_integers(array: Array!(Integer)) {


def take_hash_map_mapping_strings_to_integers(map: HashMap!(String, Integer)) {


Byte arrays

Byte arrays efficiently store a sequence of bytes. Byte arrays require an explicit import before you can use them:

import std::byte_array::ByteArray

Once imported, the ByteArray type can be used in a type signature like any other type:

import std::byte_array::ByteArray

def bytes_to_string(bytes: ByteArray) {
  # ...


Nil is a type used to represent the absence of a value. A Nil responds to any message, returning Nil itself. For some messages there is a custom implementation, in which case the return type may be something other than Nil.

Optional types

Optional types are types that can either be a particular type, or Nil. These types are created using the syntax ?T, where T is the type. For example, to define an optional Integer you would use ?Integer:

def take_integer_or_nil(value: ?Integer) {

Block types

A Block is a method, closure, or lambda. For type signatures you can use do to refer to a closure, or lambda to refer to a lambda:

def take_closure(block: do) {


def take_lambda(block: lambda) {


A lambda type can be passed to a closure type, but not the other way around. When using do or lambda you can also specify the arguments, throw type, and return type:

def take_closure(block: do (Integer) !! SomeErrorType -> Integer) {


To accept a Block of any kind, just use Block in the type signature:

def take_any_block(block: Block) {


When using Block in the type signature, you can not specify any argument types, throw types, or return types.

Object types

Pretty much everything in Inko is an object, including the types mentioned above. Custom objects can be created using the object keyword:

object Person {



Dynamic types

The Dynamic type is a special type that only exists at compile time. When something is Dynamic, any other type can be passed to it. When defining methods you can simply leave out the argument types or return type, and they will be inferred as Dynamic:

def take_dynamic_value(value) {


For let expressions you must define the type explicitly, because by default the type of a let binding is inferred based on the value:

let dynamic_value: Dynamic = 10

For closures and lambdas, the arguments will default to Dynamic only if the closure or lambda is not directly passed as an argument:

let block = do (number) { number }


Here the number argument is inferred as Dynamic, because the compiler does not know what type to use when type checking the block. When passing the block directly, the compiler can infer this:

def some_method(block: do (Integer)) {


some_method do (number) { number }

Here the number argument is inferred as an Integer.

Self types

The type Self can be used in a method in an object or trait to refer to the object that implements the method:

object Person {
  def take_person(person: Self) {


The Self type is currently evaluated upon definition, and not upon being referenced. This is likely to change in the future.

Void types

The type Void is used to signal the compiler that an expression will never return. Direct use of this type is heavily discouraged. A value of type Void can be passed to any other type, since during runtime this will never truly happen (because a Void expression can not return). The inverse (passing T to a Void) is not possible.