From dynamic to static typing in three steps
Tags: elm, javascript, typescript, webdev
Published: 2021-10-12 22:08:44

---
title: From dynamic to static typing in three steps
published: true
description: A simple goal, three different experiences. What happen when we try to leverage the type systems of JavaScript, TypeScript, and Elm?
tags: elm, javascript, typescript, webdev
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tfetg5kwo126tgg9avsc.png
---
*TLDR; Jump to the [conclusions](#conclusions).*
    
We have been told that a robust static type system can reduce the number of bugs in our applications, transforming a 2 a.m. production issue into a red squiggly in our text editor. This is an appealing proposition.

In this post, we will set the stage with some definition, a scenario, and a goal and see how this little adventure goes. We will then try to draw some conclusions.
### What do Dynamic and Static mean?

* A **dynamic** type system is a system where types are checked at **runtime**.
* A **static** type system is a system where types are checked at **compile time**.

### Scenario

Let's imagine that our code needs a simple function that returns the last element of an array (let's call it "`last`").

### Goal 🏁

Our goal is to have a system that would warn us if we try to call this function with anything other than an array and also ensures that our functions accept arrays as input and return one element (or error, in case the array is empty) as output.

This is the behavior we would like to get:

```typescript
last([ 1, 2 ])     // Should return 2

last([ "1", "2" ]) // Should return "2"

last([])           // Should return some kind 
                   // of error, because an 
                   // empty array does not 
                   // have a last element
```
These calls instead should not be allowed by the type system:
```typescript
last()             // Should not be allowed
last(42)           // Should not be allowed
last("42")         // Should not be allowed
last(null)         // Should not be allowed
last(undefined)    // Should not be allowed
```
## 1. JavaScript as starter

Let's start from JavaScript. Here is our simple function:

```javascript
const last = (arr) => arr[ arr.length - 1 ]
```
These are the results of calling it. `PASS` and `FAIL` refer to our goal requirement stated above.

```javascript
last([1,2])     // PASS: 2
last(["1","2"]) // PASS: "2"
last([])        // PASS: undefined
last()          // FAIL: Crash
last(42)        // FAIL: undefined
last("42")      // FAIL: "2"
last(null)      // FAIL: Crash
last(undefined) // FAIL: Crash
```
We got 3 PASSES and 5 FAILS. JavaScript does its best to keep our script running even when we send values that are not arrays, like `42` and `"42"`. After all, both of them yield some kind of result, so why not? But for more drastic types, like `null` or `undefined`, also the [weakly typed JavaScript](https://en.wikipedia.org/wiki/JavaScript#Weakly_typed) fails, throwing a couple of errors:
```
Uncaught TypeError: Cannot read properties
of undefined (reading 'length')

Uncaught TypeError: Cannot read properties
of null (reading 'length')
```
JavaScript is lacking a mechanism to warn us about a possible failure before executing the script itself. So our scripts, if not properly tested, may crash directly in our users' browsers... in production at 2 a.m.
## 2. TypeScript to the rescue

TypeScript is a superset of JavaScript so we can recycle the same function written before and see what TypeScript has to offer, out of the box, starting with a loose setting.

The difference that we see at this point is that the result of calling `last` without arguments changed from crashing our application in JavaScript to this error in TypeScript:
```
Expected 1 arguments, but got 0.
```
This is an improvement! All other behaviors remain the same, but we get a new warning:
```
Parameter 'arr' implicitly has an 'any' type,
but a better type may be inferred from usage.
```
It seems that TypeScript tried to infer the type of this function but was not able to do it, so it defaulted to `any`. In TypeScript, `any` means that everything goes, no checking is done, similar to JavaScript. 

This are the types inferred by TypeScript:

```TypeScript
last: (arr: any) => any
```

Let's instruct the type checker that we want this function to only accepts arrays of number or arrays of strings. In TypeScript we can do this by adding a type annotation with `number[] | string[]`:
```typescript
const last = (arr: number[] | string[]) => 
    arr[ arr.length - 1 ]
```
We could also have used `Array<number> | Array<string>` instead of `number[] | string[]`, they are the same thing.

This is the behaviour now:

```typescript
last([1,2])     // PASS: 2
last(["1","2"]) // PASS: "2"
last([])        // PASS: undefined
last()          // PASS: Not allowed
last(42)        // PASS: Not allowed
last("42")      // PASS: Not allowed
last(null)      // FAIL: Crash
last(undefined) // FAIL: Crash
```
It is a substantial improvement! 6 PASSES and 2 FAILS.

We are still getting issues with `null` and `undefined`. Time to give TypeScript more power! Let's activate these flags

* `noImplicitAny` - Enable error reporting for expressions and declarations with an implied `any` type. Before we were only getting warnings, now we should get errors.
* `strictNullChecks` - Will make `null` and `undefined` to have their distinct types so that we will get a type error if we try to use them where a concrete value is expected.

And boom! Our last two conditions are now met. Calling the function with either `null` or `undefined` generate the error

```
Argument of type 'null' is not assignable 
to parameter of type 'number[] | string[]'.

Argument of type 'undefined' is not assignable
to parameter of type 'number[] | string[]'.
```
Let's look at the type annotation (you can usually see it when you mouse-hover the function name or looking at the `.D.TS` tab if you use the online playground).

```typescript
const last: (arr: number[] | string[]) =>
    string | number;
```
This seems slightly off as we know that the function can also return `undefined` when we call `last` with an empty array, as empty arrays don't have the last element. But the inferred type annotation says that only strings or numbers are returned.

This can create issues if we call this function ignoring the fact that it can return undefined values, making our application vulnerable to crashes, exactly what we were trying to avoid.

We can rectify the problem by providing an explicit type annotation also for the returned values
```typescript
const last = 
    (arr: number[] | string[]): string | number | undefined => 
        arr[ arr.length - 1 ]
```
I eventually find out that there is also a flag for this, it is called `noUncheckedIndexedAccess`. With this flag set to true, the type `undefined` will be inferred automatically so we can roll back our latest addition.

One extra thing. What if we want to use this function with a list of booleans? Is there a way to tell this function that any type of array is fine? ("any" is intended here as the English word "any" and not the TypeScript type `any`).

Let's try with Generics:
```typescript
const last = <T>(arr: T[]) =>
	arr[arr.length - 1]
```
It works, now `boolean` and possibly other types are accepted. the final type annotation is:
```typescript
const last: <T>(arr: T[]) => T | undefined;
```
*Note: If you get some error while using Generics like, for example, `Cannot find name 'T'`, is probably caused by the JSX interpreter. I think it gets confused thinking that `<T>` is HTML. In the online playground, you can disable it by choosing `none` in `TS Config > JSX`.*

To be pedantic, it seems that we still have a small problem here. If we call `last` like this:
```typescript
last([])            // undefined
last([undefined])   // undefined
```
We get back the same value even though the arguments we used to call the function were different. This means that if `last` returns `undefined`, we cannot be 100% confident that the input argument was an empty array, it could have been an array with an undefined value at the end.

But it is good enough for us, so let's accept this as our final solution! 🎉

*To learn more about TypeScript,  you can find excellent material on the [official documentation website](https://www.typescriptlang.org/docs/), or you can [check the example of this post](https://www.typescriptlang.org/play?noUncheckedIndexedAccess=true&allowUnreachableCode=true&allowUnusedLabels=true&noUnusedLocals=true&noUnusedParameters=true&esModuleInterop=false&target=8&jsx=0&useUnknownInCatchVariables=true&exactOptionalPropertyTypes=true&noFallthroughCasesInSwitch=true&noImplicitOverride=true&noPropertyAccessFromIndexSignature=true&ssl=8&ssc=1&pln=10&pc=1#code/PTAEGMHsDsGcBdQBsCGDQF5QCgSn-gBQoBOJAlJgHyikkDatZAdEgKbQDm8AFqALSgAjKAC62XGChxEqdFkkEidAFyhoAVwC2AIzYNRoAD6gEJAJZd6oyhhqKlSuozqsO3PoJHjs09HMQsAB4AFSpiMjUQ61sqbABIZ1d2Ll4BYR8-SHZWSE5CAMJ6IQAaACYbcl8YWGy2XPzC+gAiIWaS0Gay5srquDqGgrR4IvgSDTYOgDMUJFg2XqycpDyhhCKNaAATNinLNi3FmoGVxuGio-7l1cLyKqX607WR5oAWbru+2uuz9ffPh6DQqbHZ7aAHAHHH7PQiaJBIT5AA)  in the online playground.*
## 3. Elm for the typed-FP experience
How is the experience of reaching the same goal using a functional language? 

Let's [rewrite our function in Elm](https://ellie-app.com/fny8L3Nn556a1):

```elm
last arr = get (length arr - 1) arr
```
This is the outcome of calling the function, for all our cases:

```elm
last (fromList [ 1, 2 ])     -- PASS: Just 2
last (fromList [ "1", "2" ]) -- PASS: Just "2" 
last (fromList [ True ])     -- PASS: Just True 
last (fromList [])           -- PASS: Nothing
last ()                      -- PASS: Not allowed
last 42                      -- PASS: Not allowed
last "42"                    -- PASS: Not allowed
last Nothing                 -- PASS: Not allowed
```
We got all PASS, all the code is correctly type-checked, everything works as expected out of the box. Elm could infer all the types correctly and we didn't need to give any hint to the Elm compiler. The goal is reached! 🎉

How about the "pedantic" problem mentioned above? These are the results of calling `last` with `[]` and `[ Nothing ]`.

```elm
last (fromList [])           -- Nothing
last (fromList [ Nothing ])  -- Just Nothing
```
Nice! We got two different values so we can now discriminate between these two cases.

Out of curiosity, the inferred type annotation of `last` is:
```elm
last : Array a -> Maybe a
```
*To learn more about Elm, the [official guide](https://guide.elm-lang.org/) is the perfect place to start, or you can [check the example of this post](https://ellie-app.com/fny8L3Nn556a1)  in the online playground.*
## Conclusions
This example covers only certain aspects of a type system, so it is far from being an exhaustive analysis but I think we can already extrapolate some conclusions.
### JavaScript
**Plain JavaScript is lacking any capability of warning us if something is wrong** before being executed. It is great for building prototypes when we only care for the happy paths, but if we need reliability better not to use it plain.
### TypeScript

TypeScript is a powerful tool designed to allow us to work seamlessly with the idiosyncrasies of the highly dynamic language that is JavaScript. 

Adding static types on top of a weakly typed dynamic language, while remaining a superset of it, is not a simple task and comes with  trade-offs.

TypeScript allows certain operations that can’t be known to be safe at compile-time. When a type system has this property, it is said to be "not sound". **TypeScript requires us to write type annotations to help to infer the correct types. TypeScript cannot prove correctness.**

This also means that sometimes is necessary to fight with the TypeScript compiler to get things right.

### Elm
Elm took a different approach from its inception, breaking free from JavaScript. This allowed building a language with an ergonomic and coherent type system that is baked in the language itself.

The Elm type system is "sound", all types are proved correct in the entire code base, including all external dependencies (The concept of `any` does not exist in Elm).

The type system of Elm also does extra things like handling missing values and errors so the concepts of `null`, `undefined`, `throw` and `try/catch` are not needed. Elm also comes with immutability and purity built-in.

This is how **Elm guarantees the absence of runtime exceptions**, exonerating us from the responsibility of finding all cases where things can go wrong so that we can concentrate on other aspects of coding.

**In Elm, type annotations are completely optional and the inferred types are always correct**. We don't need to give hints to the Elm inference engine.

So if the Elm compiler complains, it means that objectively there is a problem in the types.

Elm is like a good assistant that does their job without asking questions but doesn't hesitate to tell us when we are wrong.
    

*The header illustration is derived from a work by [ Pikisuperstar](https://www.freepik.com/free-vector/set-torii-gates-water_9925874.htm).*