TypeScript : Using advanced types for expressive code
By: Ajay Bhosale
Tags: Typescript, JavaScript, Advent Of Code, Clean Code, Programming

With this post I am going to explain how advanced types like Record, Union Types, and Variadic Tuple Types helps you to write expressive and readable code.

The post is based on Day 12 of AdventOfCode, If you are not aware of this awesome advent calendar of small programming puzzles, do check.

Before you proceed, please read the Day 12 puzzle description. Do check my complete solution here.

My approach for these puzzles is as follows

Be expressive with types

Declarative over imperative

Functional over procedural/OOP

Readable names for functions and variables

Let’s define some types. I use tuples instead of interface or classes, I find them more readable and less verbose. TypeScript 4.0 allows labeled tuple elements.

Direction, Turn and ActionType are union types, while Navigation is a tuple of an ActionType and a number.

type Direction = 'N' | 'S' | 'E' | 'W';

type Turn = 'L' | 'R';

type ActionType = Direction | Turn | 'F';

type Navigation = [action: ActionType, value: number];

These domain specific types improves readability as well as type checking.

const t: Turn = 'A'; // Invalid as value A is not a valid Turn

const t: Turn = 'L'; // Valid

The puzzle input is set of navigation instructions, every line can be mapped to a Navigation Type as shown in code below. The `+` sign converts a string to number. With this parsing we get an array of Navigation instructions.

/*

F10 ==> [F, 10]

N3 ==> [N, 3]

F7 ==> [F, 7]

R90 ==> [R, 90]

F11 ==> [F, 11]

*/

const lineToNav = (l: string) => [l[0], +l.substr(1)] as Navigation;

Our ship has a starting location east 0, north 0, and facing EAST. Every Navigation action changes this state in some form.

The state transition with above input as follows

// State Transitions

START east 0, north 0, facing east

1. F10 east 10, north 0, facing east

2. N3 east 10, north 3, facing east

3. F7 east 17, north 3, facing east

4. R90 east 17, north 3, facing south

5. F11 east 17, south 8, facing south

Following graph shows this transition

Ship movement

The location on the graph can be represented with two numbers, x and y.

east 0, north 0 ==> x : 0, y : 0

east 10, north 0 ==> x : 10, y : 0

east 10, north 3 ==> x : 10, y : 3

east 17, north 3 ==> x : 17, y : 3

east 17, north 3 ==> x : 17, y : 3

east 17, south 8 ==> x : 17, y : -8

You can also a complex number to represent a 2d location, Python has neat support on this, and reddit thread of 12 day solutions showcase many such examples.

So time to define Ship Type to store the state.

type Ship = [x: number, y: number, facing: Direction];

const ship : Ship = [0, 0, E];

Every ActionType (N, S, E, W, L, R, F) indicates an Action which change the state of the ship. Lets define a type Action. This will be delegate, which takes two parameters, Ship and a value and returns a updated state.

type Action = (s: Ship, v: number) => Ship

Now comes the real work of writing implementation for each Action Type. To make it type safe, I am going to declare an additional type to store this mapping.

type ActionMap = Record<ActionType, Action>;

Now you must be wondering what is Record here. Record Constructs a type with a set of properties Keys of type Type . So a variable/object with type ActionMap must have all the keys of ActionType (N, S, …) of type Action .

const shipNav: ActionMap = {

// N10 means ActionType is N, and value (v) is 10

// y indicates NORTH/SOUTH

// Moving north means y will increase, x will stay as is

N: ([x, y, f], v) => [x, y + v, f],

S: ([x, y, f], v) => [x, y - v, f],

E: ([x, y, f], v) => [x + v, y, f],

W: ([x, y, f], v) => [x - v, y, f],

L: ([x, y, f], v) => [x, y, takeTurn(f, v, L)],

R: ([x, y, f], v) => [x, y, takeTurn(f, v, R)],

F: ([x, y, f], v) => [

f === E || f === W ? x + v * factor[f] : x,

f === N || f === S ? y + v * factor[f] : y,

f,

],

};

In above code N, S, E, and W are self explanatory. L and R do not impact x and y, but impact the facing direction of the ship.

F impacts depends on direction, for example if ship is facing West and instruction is F20, it should reduce x value by 20. The variable factor is just map of impact (+ve or -ve) for each direction. TypeScript is very clever here, it understand that f is of type Direction and factor has values of every Direction, so it works well without any extra type annotations on the variable “factor”.

const factor = {E: 1, W: -1, S: -1, N: 1};

With all this in hand, what remains is to write reduce function to move from one state to another. Out actual task is calculate Manhattan distance between starting point (0, 0) and last point.

function calculate(instructions: Navigation[]){

const [x, y] = instructions.reduce(

(state, [action, value]) => shipNav[action](state, value),

[0, 0, E] as Ship

);

return Math.abs(x) + Math.abs(y);

};

The reducers take [0, 0, E] as the base and keep transforming state. By using destructing feature of TypeScript, we read final position of Ship and get the sum of its absolute values.