TypeScript : Using advanced types for expressive code

Ajay Bhosale
7 min readDec 13, 2020

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.

Now comes the Part 2 with a new concept called waypoint. The rules for Actions are also bit different. The ship’s facing direction has no meaning now. I am going to define a single type to represent these changes.

type Snwp = [x: number, y: number, wx: number, wy: number];

I know, there is no word like Snwp, here it stands for Ship and Waypoint. x and y for position for ship, and wx and wy for position of waypoint.

Now we have two state types, lets use generic to refine some of the earlier types.

type Action<T> = (s: T, v: number) => T;
type ActionMap<T> = Record<ActionType, Action<T>>;

Action take a generic type and returns a updated value of same type, while ActionMap is also adjusted accordingly.

Lets work on the navigation rules for Snwp.

const snwpNav: ActionMap<Snwp> = {
N: ([x, y, wx, wy], v) => [x, y, wx, wy + v],
S: ([x, y, wx, wy], v) => [x, y, wx, wy - v],
E: ([x, y, wx, wy], v) => [x, y, wx + v, wy],
W: ([x, y, wx, wy], v) => [x, y, wx - v, wy],
L: ([x, y, wx, wy], v) => [x, y, ...rotate(wx, wy, v, L)],
R: ([x, y, wx, wy], v) => [x, y, ...rotate(wx, wy, v, R)],
F: ([x, y, wx, wy], v) => [x + wx * v, y + wy * v, wx, wy],
};

The rotate implementation is little bit challenging, we need to visit the graph again. Also note that rotate just returns updated wx and wy, but with (spread operator) it becomes part of the state tuple.

Rotation of Waypoints

R is clockwise rotation, and L is anticlockwise rotation of Waypoint. R90 and L270 are equivalent, they switch the values of x and y coordinates and toggle the sign of the y axis. R180/L180 toggles signs of both x and y.

const rotate = (
wx: number,
wy: number,
value: number,
turn: Turn
): [number, number] => {
if (value == 180) {
return [wx * -1, wy * -1];
}
if ((value === 90 && turn === R)
||
(value === 270 && turn == L)) {
return [wy, -wx];
}
return [-wy, wx];
};

Time to finish part 2, note that Waypoint initial position is 10, 1.

function calculate2(instructions: Navigation[]){
const [x, y] = instructions.reduce(
(state, [action, value]) => snwpNav[action](state, value),
[0, 0, 10, 1] as Snwp
);
return Math.abs(x) + Math.abs(y);
};

All good, time for final refactoring. If you see calculate and calculate2 functions are structurally identical.

Identical calculate and calculat2 functions

With generics it can be refactored as follows

But TypeScript is not happy with our destructing of generic T into x and y. We can cheat and cast the result from reduce to any , but then that will be make Anders Hejlsberg (brain behind TypeScript) unhappy.

Lets tell TypeScript that T is a Tuple with first two elements being number, and that will take care of destructing x and y. Luckily for us both the Ship and Snwp store the state of ship into first two elements.

Note : Variadic Tuple Types is a new feature of TypeScript 4.0.

function calculate<T extends [number, number, ...any]>(

Here is the final snippet

Merged calculate function.

That’s it. You can check the complete solution here. From my experience, the biggest strength of TypeScript is that it helps to augment types, which leads to very clear and domain rich code.

Code to call the calculate function.

mapLine is an utility function to read input.

Thanks for reading.

--

--