In this article series, we tried to get into Elm as quickly as possible, by buiding three mini apps up to this point. In this article we’ll shift our focus on some more theoretical concepts, before we move onto the more advanced stuff.

A close-up of a race track with an Elm logo overlaid

Note: Examples in this article use Elm 0.19.

Note that in this article, we’ll be using the online elmrepl, exensively.

Tip #1: Convert HTML to Elm online

It is a nice exercise to try to convert some convoluted HTML code to Elm code by yourself.

However, there is an online converter available at:

We can simply paste in the HTML code to the left pane of the provided URL, and get the Elm representation in the right-hand panel on the website.

Tip #2: Function signatures for HTML elements

All HTML elements share the same function signature pattern:

<element> : List (Attribute msg) -> List (HTML msg) -> HTML msg

Every HTML element takes in two lists: a List of Attributes and a List of Children elements. Then, they return an Html msg. If the value returned from a function does not emit a message, that code will return the msg type.

In other words, every element returns the value of HTML. This HTML value is of type msg, because they end up as plain HTML nodes and they will not (cannot!) change our app’s state.

That’s why it is perfectly possible to just keep writing Elm’s HTML functions in our main function and never add the view or update, and still have a working web page. It is possible because we are rendering our code without any messages, so effectively, there is never going to be anything to update, and thus, we can do without the update function. That’s why we can replace the implicit type of msg with a:

main : HTML a

By convention, a stands for anything. Since the main function will never return a message, we can be more explicit about it, and have the following code as the function signature:

main : HTML Never

Now, we are explicitly declaring that we will never return a message. The Type of Never cannot ever be constructed.

Tip #3: You don’t have to pass the view code directly into the main function

Instead, you can assign it to the view function, which will then be passed to the main function. This way, we are beginning to make our code more modular and reusable.

To do this, you’ll need to set an updated main function:

main : HTML Never
main =

The referenced view is now on its own:

view : HTML Never
view =

A nice thing about this setup is that we can now pass our entire view to a wrapping div, for example, like this:

main : HTML Never
main =
    div [] [ view ]

Tip #4: How type alias of Model works

By adding a type alias for our Model, we’ll make it easier to change our Model from something other than Int (in the second article in this article series), if we ever decide to do so, which makes our code more maintainable.

type alias Model =

Next, let’s update type annotations throughout the app:

model : Model
model =

update : Msg -> Model -> String
update msg model =

view : Model -> HTML Msg
view model =

It’s obvious what we are doing in the code: we are aliasing the value of Int with the type alias Model. It is very important to understand this and commit it to memory since this simple example shows exactly what type alias does and how it works.

Making these changes, we can now see an interesting pattern: the update function’s type annotation uses the Msg, with the capital M, while we pass it the msg, with the small m, since it’s the first argument.

Similarly, we are passing the model with the small letter m to our view, but in our view function’s type annotation, we are referencing the Model with the capital letter M.

What’s going on here? The explanation is simple. We can think of the lowercase instances as simply generic labels, which can be anything. For example, consider the following changes to our code.

First, let’s replace the existing update function’s msg with this:

update a model =
    case a of

Next let’s replace the existing view function’s model with a, as follows:

view a =
                , p [ class "mt-5 lead" ] [ text a ]

The fact that we replaced both msg and model with the more generic a in the preceding code, and did not break it, is great. The compiler happily performs its duties, and we still have a working app.

Type alias is used to make it easier to read complex type annotations. Also, type annotations are capitalized by default, and so are union types. Thus, both the Model and the Msg are capitalized in our code, and we cannot change them.

Tip #5: Primitive types in Elm

The primitive types in Elm include: Char, String, Bool, and number (Int and Float).

When we use single quotes, we get Chars. To get the type of String from a value, we need to surround that value in double quotes.

Multiline strings are written by enclosing any number of lines in three consecutive double quote characters.

If we just type a number, we’ll get back that same number, followed with a colon and the number type:

> 5
5 : number

If we test a decimal number, we’ll get back the type of Float:

> 3.6
3.6 : Float

To get back a value of type Int from a decimal number, let’s run the following command:

> truncate 3.14
3 : Int

Boolean values are simple. Just make sure to capitalize (otherwise you’ll get an error).

> True
True : Bool
> False
False : Bool

Tip #6: Why are some types capitalized, and some are not?

Why are some types capitalized, and some are not? If a type is capitalized, it means it is an explicit type. Basically, the number type is used for both Ints and Floats. Which one it will end up being (which explicit type it will end up being), depends on how that number is used. Put differently, number is an implicit type, since it can end up as an explicit Int or an explicit Float.

Tip #7: Data structures: lists, tuples, records, sets, arrays, and dictionaries


A list in Elm is like an array in JavaScript. For our first example, let’s type this value in Elm REPL:

[ 1, 2, 3, 4 ]

This is what we get back from the REPL:

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

Here’s a list of Floats:

[ 0.1, 0.2, 0.3, 0.4 ]

This is what we get back from the REPL:

[0.1,0.2,0.3,0.4] : List Float

What about an empty list?

> []
[] : List a

List a means that this list is empty, that is, that it can hold anything. This wraps up our short overview of Lists in Elm. Next, we will look at tuples.

Remember, mixing values in Lists in Elm will trow a TYPE MISMATCH error.


In Elm, a tuple is a data structure that can hold values of various types.

To make a Tuple in Elm REPL, let’s simply put a String and a Boolean inside parentheses:

( "abc", True )

The REPL will respond with:

("abc",True) : ( String, Bool )

A tuple can hold a maximum of nine values. Interestingly, tuples of different lengths are considered to be of different types. For example, let’s make a List that holds two tuples, using Elm REPL:

[ ( 'a', 'b' ), ( 'c', 'd' ) ]

This expression will evaluate to:

[('a','b'),('c','d')] : List ( Char, Char )

What REPL tells us is that the above values are in a List of two tuples, holding values of Char type.

Let’s try to vary the number of Chars in the second tuple:

[ ( 'a', 'b' ), ( 'c' ) ]

This will throw a Type Mismatch error. Indeed, for two tuples to be considered to be of the same type, they have to hold the same number of values, and those values also need to be of the same type.

The maximum number of values a tuple can hold in Elm is 9. If you try to add 10 or more values to a tuple, Elm will throw an error. Let’s try this out:


Here’s the error that comes back from the REPL:

---- Elm 0.19.0 ----------------------------------------------------------------
Read <> to learn more: exit, help, imports, etc.
> ('1','2','3','4','5','6','7','8','9','0')
-- BAD TUPLE --------------------------------------------------------------- elm

I only accept tuples with two or three items. This has too many:

4|   ('1','2','3','4','5','6','7','8','9','0')
I recommend switching to records. Each item will be named, and you can use the
`point.x` syntax to access them.

Note: Read <> for more comprehensive advice on
working with large chunks of data in Elm.


Records in Elm use curly brackets, and a label for each value must be provided. Records can also hold multiple values, and these values types’ don’t have to match. For example:

{ color="blue", quantity=17 }

In REPL, we get back:

{ color = "blue", quantity = 17 } : { color : String, quantity : number }

We will use records a lot in our Elm programs, as records allow us to model the data in a wide variety of scenarios.

An example where we already used a record in a previous article:

main = Browser.sandbox 
    { init = 5
    , update = update
    , view = view 

In the above code, the Browser.sandbox function takes a Record as its single parameter.

To test out the code in the REPL, we can just pass it the record itself:

{ init = 5, view = view, update = update }

Here’s the error that REPL throws:

I cannot find a `update` variable:

4|   { init = 5, view = view, update = update }
These names seem close though:


Hint: Read <> to see how `import`
declarations work in Elm.

-- NAMING ERROR ------------------------------------------------------------ elm

I cannot find a `view` variable:

4|   { init = 5, view = view, update = update }
These names seem close though:


Hint: Read <> to see how `import`
declarations work in Elm.    

The error shows since there are no varables of update or view available.

So let’s add it and try again:

> view = "view info"
"view info" : String
> update = "update info"
"update info" : String
> { init = 5, view = view, update = update }
{ init = 5, update = "update info", view = "view info" }
    : { init : number, view : String, update : String }

We have assigned values of type String to the view and update variables in the Elm REPL. Then we entered the record, and the REPL returned types for each of the variables used in the record. Thus, in the preceding example, the model is of type number, the update is of type String, and the view is also of type String.


Sets are collections of unique values. Their uniqueness is guaranteed by the Elm programming language. We can instantiate sets as empty sets or use the fromList function. Creating an empty set is easy: set = Set.empty.

Let’s look at the other way of creating sets in Elm, by pointing our browser to an Ellie app example.

module Main exposing (main)

import Html exposing (Html, text)
import Set

set = Set.fromList [1,1,1,2]

main : Html msg
main =
    text (Debug.toString set)

What we did in the preceding code was, after importing Set (to the variable we named set), we assigned the returned value from the evaluated expression: Set.fromList [1,1,1,2].

Next, we gave the set variable to our main function, to render it out as a text node. Of course, before it could be rendered out, we had to convert it to a String. After pressing the Compile button in the Ellie-app, we should see the following result: Set.fromList [1,2].

Sets are useful when we are trying to find differences between data structures. Next, we’ll look at arrays.


Arrays in Elm are zero-based, just like they are in JavaScript. With arrays, we can work with elements based on their index. Like sets, arrays can be created using the fromList function.

Alternatively, we can create an empty array like this: array = Array.empty. Let’s look at another Ellie-app example to test out arrays:

module Main exposing (main)

import Html exposing (Html, text)
import Array

array = Array.fromList [1,1,1,2]
array2 = Array.get 0 array

main: Html msg
main = 
    text ((Debug.toString array) ++ " " ++ (Debug.toString array2))

In the preceding code, we have a slight twist—we grouped the concatenation of two arrays and a space, all converted to Strings, and then ran the text function on them, finally passing the value returned from the evaluation of the expression to the main function.

The compiled code will display the following result: Array.fromList [1,1,1,2] Just 1. For now, let’s just ignore what this Just result means, and let’s continue by looking at dictionaries in Elm.


Dictionaries are also created using the fromList function. Let’s open a brand new Ellie-app, and this time let’s add this code:

module Main exposing (main)

import Html exposing (Html, text)
import Dict

dict = 
    [ ("keyOne", "valueOne")
    , ("keyTwo", "valueTwo") 

main : Html msg
main =
    text (Debug.toString dict)

The result of the above code is:

Dict.fromList [("keyOne","valueOne"),("keyTwo","valueTwo")]

Dict is the data structure used to store pairs of keys and values. Keys must be unique. To learn more about this data structure, visit the official Elm lang docs.

Functions, if expressions, and types

Let’s create a new function in Elm REPL. We’ll call our function multiplyBy5:

multiplyBy5 num = 5 * num

The REPL will return this:

<function> : number -> number

The preceding line says that our multiplyBy5 function has the type of number -> number. Let’s see what type will get returned from a function that works with Strings:

appendSuffix n = n ++ "ing"

As we already know, the ++ operator is the concat operator in Elm; it will join two Strings together. Thus, expectedly, Elm REPL will return:

<function> : String -> String

As we can see, the preceding function is of type String -> String.

But, what is this String -> String? And, along the same lines, what is the Int -> Int from the previous example? String -> String simply means that the function expects a String as its argument, and will also return a String. For the Int -> Int example, the function expects a value of type Int and will also return a value of type Int.

It’s time to take a look at the basics of types in if expressions in Elm. Consider the following snippet of code and the response REPL gave it:

> time = 24
24 : number
> if time < 12 then "morning" else "afternoon"
"afternoon" : String

In the preceding code, we are running an if expression using the variable time (which we assign the value of 24). Then, we are running our comparison. Note that if expressions should actually be referred to as if-else expressions, as if expressions must have an else, otherwise they won’t work in Elm. Both the if and the else branch must be of the same type. That’s why in the preceding example we are making sure that either result we get is of type String.

In the next article, we’ll look at working with an improved Fruit to eat app.