Elm Type Annotations

As Elm is a statically typed language, everything has a type – even nothing. Type annotations are an optional feature and the compiler can infer the types of your functions and value. Even though you do not have to write the type annotations yourself you probably should: They document your code and make it more explicit.

What are type annotations?

When I wrote about functions in Elm I already briefly touched on type annotations, but there is much more to them. To start lets recap quickly: Type annotations describe the input and output types of functions.

add : Int -> Int -> Int
add a b =
    a + b

It starts with the function name which is separated from the argument and return types by a colon. The type of the return value is not any different than the argument types, it is just the last element of the list:

<function: add> : Int      -> Int      -> Int
| function name | type arg1 | type arg2 | type result |

Partial application and currying

There is a good reason for the return type not being any different from the arguments: In Elm, functions are curried by default. In case they do not receive all arguments listed in the type annotation, they will return a partially applied function. To illustrate this let’s use our add function from above and let it capture the first argument:

add2 : Int -> Int
add2 = add 2

add2 3
-- 5 : Int

The function add2 is created by applying the value 2 to the add function. This returns a new function that only needs one argument to be fullfilled.

As a matter of fact, every function in Elm takes only one argument: Functions that look like they are taking multiple arguments are simply using single argument functions behind the scenes. This way type annotation are also telling you about partial application of a function: If you give a function only some of its arguments the result is another function that is taking the rest of the original functions arguments.

Optional, but strongly encouraged

The Elm compiler can infer the types and does not need them as an explicit information. You will learn quickly though that the type annotations are not meant for the compiler but for yourself: They let you know at a glance how many and what kinds of arguments a function expects. Therefore they are very useful for documentation purposes. And they are a kind of documentation that won’t go out of date: The compiler assures that the stated types match the function definition.

Every function in the standard library has a type annotation. Before you can publish a library on the Elm Packages site you are required to have type annotations for your functions. This is not just nice in terms of basic documentation, but also for third-party tools and editor plugins, which can use the annotations to offer code completion or additional documentation.

Besides the documentation aspect you will be guided towards better code: By writing and thinking about the type annotations first you define the interface of your functions: Hence you have not only to think about the types of the arguments but also their order – which is particularly interesting in terms of partial application.

Constraining types and type variables

In warning mode the compiler will let you know about missing type annotations and it will also present the infered annotation. This can help as a starting point for more complex type definitions (i.e. for records), nevertheless you want to end up with your own annotations: Not only are they more expressive, but you can also be more restrictive than the autogenerated annotations:

subtract a b =
    a - b
-- <function> : number -> number -> number

In this simple case the compiler infers the type number. It is a special kind of type variable* which can either be an Int or Float.

Type variables are used to describe more than one possible type: They have lowercase names, making them easy to distinguish from concrete types, which are capitalized. When a type variable is used multiple times in an annotation, all occurrences must resolve to the same type. If you want to use different types, you have to use different names for the type variables.

Oftentimes you want to be explicit about the types that should be used. By defining a type annotation you can constrain the function to only accept concrete types:

subtract : Float -> Float -> Float
subtract a b =
    a - b

* Besides number there are two more special cases: comparable and appendable. Those three keywords for type variables define traits of values and point to a superset of possible types.

More on type annotations

Even though I described type annotations only in conjunction with functions, you can also use them for values like tuples, lists or records:

coordinates : (Float, Float)
coordinates = (53.1201749, 8.5962037)

list : List number
list = [ 1, 2, 3, 4 ]

rect : { width : Int, height : Int }
rect = { width = 10, height = 20 }

Type Annotations in elm-repl

When you try to use type annotations in the elm-repl you will get a syntax error pointing out the unexpected occurrence of the colon. This happened to me some times when I tried to copy and paste code I wanted to test in the REPL. The message is not really intuitive at first and made me wonder what was wrong. It is simply due to the fact that the elm-repl unfortunately does not support type annotations yet. In case you need to test code with type annotations you have to use elm-reactor or the Try Environment on elm-lang.org.