Skip to content

TypeScript

Today I Learned: LibYear

When writing software, it's difficult (if not impossible) to write everything from scratch. We're either using a framework or various third-party libraries to make our code work.

On one hand, this is a major win for the community because we're not all having to solve the same problem over and over again. Could you imagine having to implement your own authentication framework (on second thought...)

However, this power comes at a cost. These libraries aren't free as in beer, but more like puppies. So if we're going to take in the library, then we need to make sure that our dependencies are up-to-date with the latest and greatest. There are new features, bug fixes, and security patches occurring all the time and the longer we let a library drift, the more painful it can be to upgrade.

If the library is leveraging semantic versioning, then we can take a guess on the likelihood of a breaking change base on which number (Major.Minor.Maintenance) has changed.

  • Major - We've made a breaking change that's not backward compatible. Your code may not work anymore.
  • Minor - We've added added new features that you might be interested in or made other changes that are backwards compatible.
  • Maintenance - We've fixed some bugs, sorry about that!

Keeping Up With Dependencies

For libraries that have known vulnerabilities, you can leverage tools like GitHub's Dependabot to auto-create pull requests that will upgrade those dependencies for you. Even though the tool might be "noisy", this is a great way to take an active role in keeping libraries up to date.

However, this approach only works for vulnerabilities, what about libraries that are just out-of-date? There's a cost/benefit of upgrading where the longer you go between the upgrades, the riskier the upgrade will be.

In the JavaScript world, we know that dependencies are listed in the package.json file with minimum versions and the package-lock.json file states the exact versions to use.

Using LibYear

I was working with one of my colleagues the other day and he referred me to a library called LibYear that will check your package.json and lock file to determine how much "drift" that you have between your dependencies.

Under the hood, it's combining the npm outdated and npm view <package> commands to determine the drift.

What I like about this tool is that you can use this as a part of a "software health" report for your codebase.

As engineers, we get pressure to ship features and hit delivery dates, but it's our responsibility to make sure that our codebase is in good shape (however we defined that term). I think this library is a good way for us to capture a data point about software health which then allows the team to make a decision on whether we should update our libraries now (or defer).

The nice thing about the LibYear package is that it lends itself to be ran in a pipeline and then you could take those results and post them somewhere. For example, maybe you could write your own automation bot that could post the stats in your Slack or Teams chat.

It looks like there's already a GitHub Action for running this tool today, so you could start there as a basis.

Today I Learned - Iterating Through Union Types

In a previous post, we cover on how using union types in TypeScript is a great approach for domain modeling because it limits the possible values that a type can have.

For example, let's say that we're modeling a card game with a standard deck of playing cards. We could model the domain as such.

1
2
3
4
5
type Rank = "Ace" | "Two" | "Three" | "Four" | "Five" | "Six" | "Seven"
           | "Eight" | "Nine" | "Ten" |"Jack" | "Queen" | "King"
type Suite = "Hearts" | "Clubs" | "Spades" | "Diamonds"

type Card = {rank:Rank; suite:Suit}

With this modeling, there's no way to create a Card such that it has an invalid Rank or Suite.

With this definition, let's create a function to build the deck.

function createDeck(): Card[] {
  const ranks = ["Ace", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Jack", "Queen", "King"];
  const suites = ["Hearts", "Clubs", "Spades", "Diamonds"];

  const deck:Card[] = [];
  for (const rank of ranks) {
    for (const suite of suites) {
      deck.push({rank, suite});
    }
  }
  return deck;
}

This code works, however, I don't like the fact that I had to formally list the option for both Rank and Suite as this means that I have two different representtions for Rank and Suite, which implies tthat if we needed to add a new Rank or Suite, then we'd need to add it in two places (a violation of DRY).

Doing some digging, I found this StackOverflow post that gave a different way of defining our Rank and Suite types. Let's try that new definition.

1
2
3
4
const ranks = ["Ace", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Jack", "Queen", "King"] as const;
type Rank = typeof ranks[number];
const suites = ["Hearts", "Clubs", "Spades", "Diamonds"] as const;
type Suite = typeof suites[number]

In this above code, we're saying that ranks cannot change (either by assignment or by operations like push). With that definition, we can say that Rank is some entry in the ranks array. Similar approach for our suites array and Suite type.

I prefer this approach much more because we have our ranks and suites defined in one place and our code reads cleaner as this says Here are the possible ranks and Rank can only be one of those choices.

Limitations

The main limitation is that it only works for "enum" style unions. Let's change example and say that we want to model a series of shapes with the following.

1
2
3
4
5
type Circle = {radius:number};
type Square = {length:number};
type Rectangle = {height:number, width:number}

type Shape = Circle | Square | Rectangle

To use the same trick, we would need to have an array of constant values. However, we can't have a constant value for any of the Shapes because there are an infinite number of valid Circles, Squares, and Rectangles.

Exploring Map with Property Based Thinking

When thinking about software, it's natural to think about the things that it can do (its features like generating reports or adding an item to a cart).

But what about the properties that those actions have? Those things that are always true?

In this post, let's take a look at a fundamental tool of functional programming, the map function.

All the code examples in this post will be using TypeScript, but the lessons hold for other languages with Map (or Select if you're coming from .NET).

Examining Map

In JavaScript/TypeScript, map is a function for arrays that allow us to transform an array of values into an array of different values.

For example, let's say that we have an array of names and we want to ensure that each name is capitalized, we can write the following:

1
2
3
4
5
6
const capitalize = (name:string): string => {
  return n[0].toUpperCase() + n.substring(1);
}
const names = ["alice","bob","charlotte"];

const properNames = names.map(capitalize);

In our example, as long as we have a pure function that takes a string and returns a new type, then map will work.

What Does Map Guarantee?

Map is a cool function because it has a lot of properties that we get for free.

  1. Maintains Length - If you call map on an array of 3 elements, then you'll get a new array with 3 elements. If you call map on an empty array, you'll get an empty array.

  2. Maintains Type - If you call map on array of type T with a function that goes from T to U, then every element in the new array is of type U.

  3. Maintains Order - If you call map on array with one function, then call map with a function that "undoes" the original map, then you end up with the original array.

Writing Property Based Tests

To prove these properties, we can write a set of unit tests. However, it would be hard to write a single test that that covers a single property.

Most tests are example based in the sense that for a specific input, we get a specific output. Property based tests, on the other hand, uses random data and ensures that a property holds for all inputs. If it finds an input where the property fails, the test fails and you know which input caused the issue.

Most languages have a tool for writing property-based tests, so we'll be using fast-check for writing property based tests and jest for our test runner

Checking Length Property

import fc from "fast-check";

describe("map", () => {
  it("maintains length", () => {
    // This is known as the identify function
    // as it returns whatever input it received
    const identity = <T>(a: T): T => a;

    // Fast Check assert that the following holds for all arrays of integers
    fc.assert(
      // data is the array of numbers
      fc.property(fc.array(fc.integer()), (data): void => {
        // We call the map function with the identify function
        const result = data.map(identity);

        // We make sure that our result has the same length
        expect(result).toHaveLength(data.length);
      })
    );
  });

If we run this test, we'll end up passing. But what is the value of data?

By adding a console.log in the test, we'll see the following values printed when we run the test (there are quite a few, so we'll examine the first few).

console.log
    [
       2125251991,  1334674146,
      -1531633149,   332890473,
       1313556939,   907640912,
        887735692, -1979633703,
       -259341001,  2015321027
    ]

  console.log
    [ 1307879257 ]

  console.log
    []

  # quite a few more...

Checking Type Property

We've proven that the length property is being followed, so let's look at how we can ensure that result has the right type.

To keep things simple, we're going to start with a string[] and map them to their lengths, yielding number[].

If map is working, then the result should be all numbers.

We can leverage typeof to check the type of each element in the array.

// An additional test in the describe block

it("maintains type", () => {
  const getLength = (s:string)=>s.length;
  fc.assert(
    // asserting with an array of strings
    fc.property(fc.array(fc.string()), (data): void => {
      // mapping to lengths of strings
      const result = data.map(getLength);

      // checking that all values are numbers
      const isAllValid = result.every((x) => typeof x === "number");
      expect(isAllValid).toBeTruthy();
    })
  );
});

Like before, we can add a console.log to the test to see what strings are being generated

console.log
  [ 'ptpJTR`G4', 's >xmpXI', 'H++%;a3Y', 'OFD|+X8', 'gp' ]

console.log
  [ 'Rq', '', 'V&+)Zy2VD8' ]


console.log
  [ 'o%}', '$o', 'w7C', 'O+!e', 'NS$:4\\9aq', 'xPbb}=F7h', 'z' ]

console.log
  [ '' ]

console.log
  [ 'apply', '' ]

console.log
  []
## And many more entries...

Checking Order Property

For our third property, we need to ensure that the order of the array is being maintained.

To make this happen, we can use our identity function from before and check that our result is the same as the input. If so, then we know that the order is being maintained.

it("maintains order", () => {
  const identity = <T>(a: T) => a;

  fc.assert(
    fc.property(fc.array(fc.string()), (data): void => {
      const result = data.map(identity);

      expect(result).toEqual(data);
    })
  );
});

And with that, we've verified that our third property holds!

So What, Why Properties?

When I think about the code I write, I'm thinking about the way it works, the way it should work, and the ways it shouldn't work. I find example based tests to help understand a business flow because of it's concrete values while property based tests help me understand the general guarantees of the code.

I find that once I start thinking in properties, my code became cleaner because there's logic that I no longer had to write. In our map example, we don't have to write checks for if we have null or undefined because map always returns an array (empty in the worse case). There's also no need to write error handling because as long as the mapping function is pure, map will always return an array.

For those looking to learn more about functional programming, you'll find that properties help describe the higher level constructs (functors, monoids, and monads) and what to look for.

Finding properties can be a challenge, however, Scott Wlaschin (of FSharpForFunAndProfit) has a great post talking about design patterns that I've found to be immensely helpful.

Scaling Effectiveness with Docs - Finding Stale Docs

In a previous post, I argued that to help your team be effective, you need to have up-to-date docs, and to have this happen, you need some way of flagging stale documentation.

In this series, I show you how you can automate this process by creating a seed script, a check script, and then automating the check script. In today's post, let's develop the check script.

Breaking Down the Check Script

At a high level, our script will need to perform the following steps:

  1. Specify the location to search.
  2. Find all the markdown files in directory.
  3. Get the "Last Reviewed" line of text.
  4. Check if the date is more than 90 days in the past.
  5. If so, print the file to the screen.

Specifying Location

Our script is going to search over our repository, however, I don't want our script to be responsible for cloning and cleaning up those files. Since the long term plan is for our script to run through GitHub Actions, we can have the pipeline be responsible for cloning the repo.

This means that our script will have to be told where to search and since it can't take in manual input, we're going to use an environment variable to tell the script where to search.

First, let's create a .env file that will store the path of the repository:

.env
REPO_DIRECTORY="ABSOLUTE PATH GOES HERE"

From there, we can start working on our script to have it use this environment variable.

index.ts
import { load } from "https://deno.land/std@0.195.0/dotenv/mod.ts";

await load({ export: true }); // this loads the env file into our environment

const directory = Deno.env.get("REPO_DIRECTORY");

if (!directory) {
  console.log("Couldn't retrieve the REPO_DIRECTORY value from environment.");
  Deno.exit();
}
console.log(directory);

If we were to run our Deno script with the following command deno run --allow-read --allow-env ./index.ts, we should see the environment variable getting logged.

Finding all the Markdown Files

Now that we have a directory, we need a way to get all the markdown files from that location.

Doing some digging, I didn't find a built-in library for doing this, but building our own isn't too terrible.

By using Deno.readDir/Sync, we can get all the entries in the specified directory.

From here, we can then recurse into the other folders and get their markdown files as well.

Let's create a new file, utility.ts and add a new function, getMarkdownFilesFromDirectory

utility.ts
export function getMarkdownFilesFromDirectory(directory: string): string[] {
  // let's get all the files from the directory
  const allEntries: Deno.DirEntry[] = Array.from(Deno.readDirSync(directory));

  // Get all the markdown files in the current directory
  const markdownFiles = allEntries.filter(
    (x) => x.isFile && x.name.endsWith(".md")
  );
  // Find all the folders in the directory
  const folders = allEntries.filter(
    (x) => x.isDirectory && !x.name.startsWith(".")
  );
  // Recurse into each folder and get their markdown files
  const subFiles = folders.flatMap((x) =>
    getMarkdownFilesFromDirectory(`${directory}/${x.name}`)
  );
  // Return the markdown files in the current directory and the markdown files in the children directories
  return markdownFiles.map((x) => `${directory}/${x.name}`).concat(subFiles);
}

With this function in place, we can update our index.ts script to be the following:

index.ts
import { load } from "https://deno.land/std@0.195.0/dotenv/mod.ts";
import { getMarkdownFilesFromDirectory } from "./utility.ts";

const directory = Deno.env.get("REPO_DIRECTORY");

if (!directory) {
  console.log("Couldn't retrieve the REPO_DIRECTORY value from environment.");
  Deno.exit();
}

const files = getMarkdownFilesFromDirectory(directory);
console.log(files);

Running the script with deno run --allow-read --allow-env ./index.ts, should get a list of all the markdown files being printed to the screen.

Getting the Last Reviewed Text

Now that we have each file, we need a way to get their last line of text.

Using Deno.readTextFile/Sync, we can get the file contents. From there, we can convert them to lines and then find the latest occurrence of Last Reviewed

Let's add a new function, getLastReviewedLine to the utility.ts file.

utility.ts
export function getLastReviewedLine(fullPath: string): string {
  // Get the contents of the file, removing extra whitespace and blank lines
  const fileContent = Deno.readTextFileSync(fullPath).trim();

  // Convert block of text to a array of strings
  const lines = fileContent.split("\n");

  // Find the last line that starts with Last Reviewed On
  const lastReviewed = lines.findLast((x) => x.startsWith("Last Reviewed On"));

  // If we found it, return the line, otherwise, return an empty string
  return lastReviewed ?? "";
}

Let's try this function out by modifying our index.ts file to display files that don't have a Last Reviewed On line.

index.ts
import { load } from "https://deno.land/std@0.195.0/dotenv/mod.ts";
import {
  getMarkdownFilesFromDirectory,
  getLastReviewedLine,
} from "./utility.ts";

const directory = Deno.env.get("REPO_DIRECTORY");

if (!directory) {
  console.log("Couldn't retrieve the REPO_DIRECTORY value from environment.");
  Deno.exit();
}

const files = getMarkdownFilesFromDirectory(directory);
files
  .filter((x) => getLastReviewedLine(x) !== "")
  .forEach((s) => console.log(s)); // print them to the screen

Determining If A Page Is Stale

At this point, we can get the "Last Reviewed On" line from a file, but we've got some more business rules to implement.

  • If there's a Last Reviewed On line, but there's no date, then the files needs to be reviewed
  • If there's a Last Reviewed On line, but the date is invalid, then the file needs to be reviewed
  • If there's a Last Reviewed On line, and the date is more than 90 days old, then the file needs to be reviewed.
  • Otherwise, the file doesn't need review.

We know from our filter logic that we're only going to be looking at lines that start with "Last Reviewed On", so now we need to extract the date.

Since we assume our format is Last Reviewed On, we can use substring to get the rest of the line. We're also going to assume that the date will be in YYYY/MM/DD format.

utility.ts
export function doesFileNeedReview(line: string): boolean {
  if (!line.startsWith("Last Reviewed On: ")) {
    return true;
  }
  const date = line.replace("Last Reviewed On: ", "").trim();
  const parsedDate = new Date(Date.parse(date));
  if (!parsedDate) {
    return true;
  }

  // We could something like DayJS, but trying to keep libraries to a minimum, we can do the following
  const cutOffDate = new Date(new Date().setDate(new Date().getDate() - 90));

  return parsedDate < cutOffDate;
}

Let's update our index.ts file to use the new function.

index.ts
import { load } from "https://deno.land/std@0.195.0/dotenv/mod.ts";
import {
  getMarkdownFilesFromDirectory,
  getLastReviewedLine,
} from "./utility.ts";

const directory = Deno.env.get("REPO_DIRECTORY");

if (!directory) {
  console.log("Couldn't retrieve the REPO_DIRECTORY value from environment.");
  Deno.exit();
}

getMarkdownFilesFromDirectory(directory)
  .filter((x) => getLastReviewedLine(x) !== "")
  .filter((x) => doesFileNeedReview(x))
  .forEach((s) => console.log(s)); // print them to the screen

And just like that, we're able to print stale docs to the screen. At this point, you could create a scheduled batch job and start using this script.

However, if you wanted to share this with others (and have this run not on your box), then stay tuned for the final post in this series where we put this into a GitHub Action and post a message to Slack!

Having Coffee with Deno - Automating All the Things

Welcome to the final installment of our Deno series, where we build a script that pairs up people for coffee.

In the last post, we added the ability to post messages into a Slack channel instead of copying from a console window.

The current major problem is that we have to remember to run the script. We could always set up a cron job or scheduled task, however, what happens when we change machines? What if our computer stops working? What if someone else changes the script, how will we remember to get the latest and run it?

Having Coffee with Deno - Sharing the News

Welcome to the third installment of our Deno series, where we build a script that pairs up people for coffee.

In the last post, we're dynamically pulling members of the Justice League from GitHub instead of a hardcoded list.

Like any good project, this approach works, but now the major problem is that we have to run the script, copy the output, and post it into our chat tool so everyone knows the schedule.

It'd be better if we could update our script to post this message instead. In this example, we're going to use Slack and their incoming webhook, but you could easily tweak this approach to work with other tools like Microsoft Teams or Discord.

The Approach

In order to for the script to post to Slack, we'll need to make the following changes:

  1. Follow these docs from Slack to create an application and enable the incoming webhooks.
  2. Test that we can post a simple message to the channel
  3. From here, we'll need to add code to make a POST call to the webhook with a message
  4. Tweak the formatting so it looks nicer

Justice League Planning

Creating the Slack Application and creating the Webhook

For this step, we'll follow the instructions in the docs, ensuring that we're hooking it up to the right channel.

After following the steps, you should see something like the following:

Image of Slack App with Incoming Webhook

We can test that things are working correctly by running the curl command provided. If the message Hello World appears in the channel, congrats, you've got the incoming webhook created!

Modifying the Script to POST to Webhook

We have the Slack app created and verified that the incoming webhook is working, so we'll need to add this integration to our script.

Since we have this incoming webhook URL and Slack recommends treating this as a secret, we'll need to add this to our .env file.

Keep your webhook ULR safe image

.env
GITHUB_API_TOKEN="<yourTokenHere>"
SLACK_WEBHOOK="<yourWebHookHere>"

With this secret added, we can write a new function, sendMessage, that'll make the POST call to Slack. Since this is a new integration, we'll add a new file, slack.ts to put it in.

slack.ts
// Using axiod for the web connection
import axiod from "https://deno.land/x/axiod@0.26.2/mod.ts";

// function to send a message to the webhook
async function sendMessage(message: string): Promise<void> {
  // Get the webhookUrl from our environment
  const webhookUrl = Deno.env.get("SLACK_WEBHOOK")!;

  try {
    // Send the POST request
    await axiod.post(webhookUrl, message, {
      headers: {
        "Content-Type": "application/json",
      },
    });
  } catch (error) {
    // Error handling
    if (error.response) {
      return Promise.reject(
        `Failed to post message: ${error.response.status}, ${error.response.statusText}`
      );
    }
    return Promise.reject(
      "Failed for non status reason " + JSON.stringify(error)
    );
  }
}

export { sendMessage };

With sendMessage done, let's update index.ts to use this new functionality.

index.ts
import { load } from "https://deno.land/std@0.195.0/dotenv/mod.ts";
import {
  GetOrganizationMemberResponse,
  getMembersOfOrganization,
} from "./github.ts";
import { sendMessage } from "./slack.ts";
import { Pair, createPairsFrom, shuffle } from "./utility.ts";

await load({ export: true });

// Replace this with your actual organization name
const organizationName = "JusticeLeague";
const names = await getMembersOfOrganization(organizationName);
const pairs = createPairsFrom(shuffle(names));
const message = createMessage(pairs);
// Slack expects the payload to be an object of text, so we're doing that here for now
await sendMessage(JSON.stringify({ text: message }));

function createMessage(pairs: Pair<GetOrganizationMemberResponse>[]): string {
  const mapper = (p: Pair<GetOrganizationMemberResponse>): string =>
    `${p.first.login} meets with ${p.second.login}${
      p.third ? ` and ${p.third.login}` : ""
    }`;
  return pairs.map(mapper).join("\n");
}

And if we were to run the above, we can see the following message get sent to Slack.

Message from Random Coffee

Nice! We could leave it here, but we could make the message prettier (having an unordered list and italicizing names), so let's work on that next.

Pretty Printing the Message

So far, we could leave the messaging as is, however; it's a bit muddled. To help it pop, let's make the following changes.

  • Italicize the names
  • Start each pair with a bullet point

Since Slack supports basic Markdown in the messages, we can use the _ for italicizing and - for the bullet points. So let's modify the createMessage function to add this formatting.

index.ts
function createMessage(pairs: Pair<GetOrganizationMemberResponse>[]): string {
  // Let's wrap each name with the '_' character
  const formatName = (s: string) => `_${s}_`;

  const mapper = (p: Pair<GetOrganizationMemberResponse>): string =>
    // and start each pair with '-'
    `- ${formatName(p.first.login)} meets with ${formatName(p.second.login)}${
      p.third ? ` and ${formatName(p.third.login)}` : ""
    }`;
  return pairs.map(mapper).join("\n");
}

By making this small change, we now see the following message:

Formatted Slack Message with italics and bullets

The messaging is better, but we're still missing some clarity. For example, what date is this for? Or what's the purpose of the message? Looking through these docs, it seems like we could add different text blocks (like titles). So let's see what this could look like.

One design approach is to encapsulate the complex logic for dealing with Slack and only expose a "common-sense" API for consumers. In this regard, I think using a Facade pattern would make sense.

We want to expose the ability to set a title and to set a message through one or more lines of text. Here's what that code would look like

slack.ts
// This class allows a user to set a title and lines and then use the
// 'build' method to create the payload to interact with Slack

class MessageFacade {
  // setting some default values
  private header: string;
  private lines: string[];
  constructor() {
    this.header = "";
    this.lines = [];
  }

  // I like making these types of classes fluent
  // so that it returns itself.
  public setTitle(title: string): MessageFacade {
    this.header = title;
    return this;
  }
  public addLineToMessage(line: string | string[]): MessageFacade {
    if (Array.isArray(line)) {
      this.lines.push(...line);
    } else {
      this.lines.push(line);
    }
    return this;
  }

  // Here's where we take the content that the user provided
  // and convert it to the JSON shape that Slack expects
  public build(): string {
    // create the header block if set, otherwise null
    const headerBlock = this.header
      ? {
          type: "header",
          text: { type: "plain_text", text: this.header, emoji: true },
        }
      : null;
    // convert each line to it's own section
    const lineBlocks = this.lines.map((line) => ({
      type: "section",
      text: { type: "mrkdwn", text: line },
    }));
    return JSON.stringify({
      // take all blocks that have a value and set it here
      blocks: [headerBlock, ...lineBlocks].filter(Boolean),
    });
  }
}

With the facade in place, let's look at implementing this in index.ts

index.ts
1
2
3
4
5
6
7
8
9
// ... code to get the pairs and formatted lines

// using the facade with the fluent syntax
const message = new MessageFacade()
  .setTitle(`☕ Random Coffee ☕`)
  .addLineToMessage(formattedPairs)
  .build();

await sendMessage(message);

When we run the script now, we get the following message:

Random Coffee Message with Header and Icons

Wrapping Up

In this post, we changed our script from posting its Random Coffee message to the console window to instead posting it into a Slack channel using an Incoming Webhook. By making this change, we were able to remove a manual step (e.g., us copying the message into the channel), and we were able to use some cool emojis and better formatting.

In the final post, we'll take this one step further by automating the script using scheduled jobs via GitHub Actions.

As always, you can find a full working version of this bot on my GitHub.

Having Coffee with Deno - Dynamic Names

Welcome to the second installment of our Deno series, where we build a script that pairs up people for coffee.

In the last post, we built a script that helped the Justice League meet up for coffee.

As of right now, our script looks like the following.

index.ts
const names = [
  "Batman",
  "Superman",
  "Green Lantern",
  "Wonder Woman",
  "Static Shock", // one of my favorite DC heroes!
  "The Flash",
  "Aquaman",
  "Martian Manhunter",
];
const pairs = createPairsFrom(shuffle(names));
const message = createMessage(pairs);
console.log(message);

function shuffle<T>(items: T[]): T[] {
  const result = [...items];
  for (let i = result.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * i);
    [result[i], result[j]] = [result[j], result[i]];
  }
  return result;
}
type Pair<T> = { first: T; second: T; third?: T };
function createPairsFrom<T>(items: T[]): Pair<T>[] {
  if (items.length < 2) {
    return [];
  }
  const results = [];
  for (let i = 0; i <= items.length - 2; i += 2) {
    const pair: Pair = { first: items[i], second: items[i + 1] };
    results.push(pair);
  }
  if (items.length % 2 === 1) {
    results[results.length - 1].third = items[items.length - 1];
  }
  return results;
}
function createMessage(pairs: Pair<string>[]): string {
  const mapper = (p: Pair<string>) =>
    `${p.first} meets with ${p.second}${p.third ? ` and ${p.third}` : ""}`;

  return pairs.map(mapper).join("\n");
}

Even though this approach works, the major problem is that every time there's a member change in the Justice League (which seems to happen more often than not), we have to go back and update the list manually.

It'd be better if we could get this list dynamically instead. Given that the League are great developers, they have their own GitHub organization. Let's work on integrating with GitHub's API to get the list of names.

The Approach

To get the list of names from GitHub, we'll need to do the following.

  1. First, we need to figure out which GitHub endpoint will give us the members of the League. This, in turn, will also tell us what permissions we need for our API scope.
  2. Now that we have a secret, we need to update our script to read from an .env file.
  3. Once we have the secret being read, we can create a function to retrieve the members of the League.
  4. Miscellaneous refactoring of the main script to handle a function returning complex types instead of strings.

Justice League Planning

Laying the Foundation

Before we start, we should reactor our current file. It works, but we have a mix of utility functions (shuffle and createPairsFrom) combined with presentation functions (createMessage). Let's go ahead and move shuffle and createPairsFrom to their own module.

utility.ts
type Pair<T> = { first: T; second: T; third?: T };

function shuffle<T>(items: T[]): T[] {
  const result = [...items];
  for (let i = result.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * i);
    [result[i], result[j]] = [result[j], result[i]];
  }
  return result;
}

function createPairsFrom<T>(items: T[]): Pair<T>[] {
  if (items.length < 2) {
    return [];
  }
  const results: Pair<T>[] = [];
  for (let i = 0; i <= items.length - 2; i += 2) {
    const pair: Pair<T> = { first: items[i], second: items[i + 1] };
    results.push(pair);
  }
  if (items.length % 2 === 1) {
    results[results.length - 1].third = items[items.length - 1];
  }
  return results;
}

export { createPairsFrom, shuffle };
export type { Pair };

With these changes, we can update index.ts to be:

index.ts
import { Pair, createPairsFrom, shuffle } from "./module.ts";

const names = [
  "Batman",
  "Superman",
  "Green Lantern",
  "Wonder Woman",
  "Static Shock", // one of my favorite DC heroes!
  "The Flash",
  "Aquaman",
  "Martian Manhunter",
];
const pairs = createPairsFrom(shuffle(names));
const message = createMessage(pairs);
console.log(message);

function createMessage(pairs: Pair<string>[]): string {
  const mapper = (p: Pair<string>) =>
    `${p.first} meets with ${p.second}${p.third ? ` and ${p.third}` : ""}`;

  return pairs.map(mapper).join("\n");
}

Getting GitHub

Now that our code is more tidy, we can focus on figuring out which GitHub endpoint(s) to use to figure out the members of the Justice League.

Taking a look at the docs, we see that there are two different options.

  1. Get members of an Organization
  2. Get members of a Team

What's the difference between the two? In GitHub parlance, an Organization is an overarching entity that consists of members which, in turn, can be part of multiple teams.

Using the Justice League as an example, it's an organization that contains Batman, and Batman can be part of the Justice League Founding Team and a member of the Batfamily Team.

Since we want to pair everyone up in the Justice League, we'll use the Get members of an Organization approach.

Working with Secrets

To interact with the endpoint, we'll need to create an API token for GitHub. Looking over the docs, our token needs to have the read:org scope. We can create this token by following the instructions here about creating a Personal Auth Token (PAT).

Once we have the token, we can invoke the endpoint with cURL or Postman to verify that we can communicate with the endpoint correctly.

After we've verified, we'll need a way to get this API token into our script. Given that this is sensitive information, we absolutely should NOT check this into the source code.

Creating an ENV File

A common way of dealing with that is to use an .env file which doesn't get checked in, but our application can use it during runtime to get secrets.

Let's go ahead and create the .env file and put our API token here.

.env
GITHUB_BEARER_TOKEN="INSERT_TOKEN_HERE"

Our problem now is that if we check git status, we'll see this file listed as a change. We don't want to check this in, so let's add a .gitignore file.

Adding a .gitignore File

With the .env file created, we need to create a .gitignore file, which tells Git not to check in certain files.

Let's go ahead and add the file. You can enter the below, or you can use the Node gitignore file (found here)

.gitignore
.env # ignores all .env files in the root directory

We can validate that we've done this correctly if we run git status and don't see .env showing up anymore as a changed file.

Loading Our Env File

Now that we have the file created, we need to make sure that this file loads at runtime.

In our index.ts file, let's make the following changes.

index.ts
1
2
3
4
5
6
7
8
9
import { config as loadEnv } from "https://deno.land/x/dotenv@v3.2.2/mod.ts";
// other imports

// This loads the .env file and adds them to the environment variable list
await loadEnv({ export: true });
// Deno.env.get("name") retrieves the value from an environment variable named "name"
console.log(Deno.env.get("GITHUB_BEARER_TOKEN"));

// remaining code

When we run the script now with deno run, we get an interesting prompt:

Deno requests read access to ".env".
- Requested by `Deno.readFileSync()` API.
- Run again with --allow-read to bypass this prompt
- Allow?

This is one of the coolest parts about Deno; it has a security system that prevents scripts from doing something that you hadn't intended through its Permissions framework.

For example, if you weren't expecting your script to read from the env file, it'll prompt you to accept. Since packages can be taken over and updated to do nefarious things, this is a terrific idea.

The permissions can be tuned (e.g., you're only allowed to read from the .env file), or you can give blanket permissions. In our cases, two resources are being used (the ability to read the .env file and the ability to read the GITHUB_BEARER_TOKEN environment variable).

Let's run the command with the allow-read and allow-env flags.

deno run --allow-run --allow-env ./index.ts

If the bearer token gets printed, we've got the .env file created correctly and can proceed to the next step.

Let's Get Dynamic

Now that we have the bearer token, we can work on calling the GitHub Organization endpoint to retrieve the members.

Since this is GitHub related, we should create a new file, github.ts, to host our functions and types.

Adding axiod

In the github.ts file, we're going to be use axiod for communication. It's similar to axios in Node and is better than then the built-in fetch API.

Let's go ahead and bring in the import.

github.ts
import axiod from "https://deno.land/x/axiod@0.26.2/mod.ts";

Calling the Organization Endpoint

With axiod pulled in, let's write the function to interact with the GitHub API.

github.ts
// Brining in the axiod library
import axiod from "https://deno.land/x/axiod@0.26.2/mod.ts";

async function getMembersOfOrganization(orgName: string): Promise<any[]> {
  const url = `https://api.github.com/orgs/${orgName}/members`;
  // Necessary headers are found on the API docs
  const headers = {
    Accept: "application/vnd.github+json",
    Authorization: `Bearer ${Deno.env.get("GITHUB_BEARER_TOKEN")}`,
    "X-GitHub-Api-Version": "2022-11-28",
  };

  try {
    const resp = await axiod.get<any[]>(url, {
      headers: headers,
    });
    return resp.data;
  } catch (error) {
    // Response was received, but non-2xx status code
    if (error.response) {
      return Promise.reject(
        `Failed to get members: ${error.response.status}, ${error.response.statusText}`
      );
    } else {
      // Response wasn't received
      return Promise.reject(
        "Failed for non status reason " + JSON.stringify(error)
      );
    }
  }
}

To prove this is working, we can call this function in the index.ts file and verify that we're getting a response.

index.ts
1
2
3
4
5
6
7
8
9
import { config as loadEnv } from "https://deno.land/x/dotenv@v3.2.2/mod.ts";
import { getMembersOfOrganization } from "./github.ts";
import { Pair, createPairsFrom, shuffle } from "./utility.ts";

await loadEnv({ export: true });

const membersOfOrganization = await getMembersOfOrganization("JusticeLeague");
console.log(JSON.stringify(membersOfOrganization));
// rest of the file

Now let's rerun the script.

deno run --allow-read --allow-env ./main.ts
Deno requests net access to "api.github.com"
- Requested by `fetch` API.
- Run again with --allow-net to bypass this prompt.

Ah! Our script is now doing something new (making network calls), so we'll need to allow that permission by using the --allow-net flag.

deno run --allow-read --allow-env --allow-net ./main.ts

If everything has worked, you should see a bunch of JSON printed to the screen. Success!

Creating the Response Type

At this point, we're making the call, but we're using a pesky any for the response, which works, but it doesn't help us with what properties we have to work with.

Looking at the response schema, it seems the main field we need is login. So let's go ahead and create a type that includes that field.

github.ts
type GetOrganizationMemberResponse = {
  login: string;
};

async function getMembersOfOrganization(
  orgName: string
): Promise<GetOrganizationMemberResponse[]> {
  //code
  const resp = await axiod.get<GetOrganizationMemberResponse[]>(url, {
    headers: headers,
  });
  // rest of the code
}

We can rerun our code and verify that everything is still working, but now with better type safety.

Cleaning Up

Now that we have this function written, we can work to integrate it with our index.ts script.

index.ts
import { config as loadEnv } from "https://deno.land/x/dotenv@v3.2.2/mod.ts";
import { getMembersOfOrganization } from "./github.ts";
import { Pair, createPairsFrom, shuffle } from "./utility.ts";

await loadEnv({ export: true });

const names = await getMembersOfOrganization("JusticeLeague");
const pairs = createPairsFrom(shuffle(names));
const message = createMessage(pairs);
console.log(message);

So far, so good. The only change we had to make was to replace the hardcoded array of names with the call to getMembersOfOrganization.

Not an issue, right?

Hmmm, what's up with this? createMessage has a type error

It looks like createMessage is expecting Pair<string>[], but is receiving Pair<GetOrganizationMemberResponse>[].

To solve this problem, we'll modify createMessage to work with GetOrganizationMemberResponse.

index.ts
// Need to update the input to be Pair<GetOrganizationMemberResponse>
function createMessage(pairs: Pair<GetOrganizationMemberResponse>[]): string {
  // Need to update mapper function to get the login property
  const mapper = (p: Pair<string>): string =>
    `${p.first.login} meets with ${p.second.login}${
      p.third ? ` and ${p.third.login}` : ""
    }`;

  return pairs.map(mapper).join("\n");
}

With this last change, we run the script and verify that we're getting the correct output, huzzah!

Current Status

Congratulations! We now have a script that is dynamically pulling in heroes from the Justice League organization instead of always needing to see if Green Lantern is somewhere else or if another member of Flash's SpeedForce is here for the moment.

A working version of the code can be found on GitHub.

Having Coffee with Deno - Inspiration

In a previous post, I mention my strategy of building relationships through one-on-ones. One approach in the post was leveraging a Slack plugin, Random Coffee, to automate scheduling these impromptu conversations.

Dinosaur sitting in a coffee cup

I wanted to leverage the same idea at my current company; however, we don't use Slack, so I can't just use that bot.

High-Level Breakdown

Thinking more about it, the system wouldn't be too complicated as it has three moving parts:

  • Get list of people
  • Create random pairs
  • Post message

To make it even easier, I could hardcode the list of people, and instead of posting the message to our message application, I could print it to the screen.

With these two choices made, I would need to build something that can shuffle a list and create pairs.

Technology Choices

Even though we're hardcoding the list of names and printing a message to the screen, I know that the future state is to get the list of names dynamically, most likely through an API call. In addition, most messaging systems support using webhooks to create a message, so that would be the future state as well.

With these restrictions in mind, I know I need to use a language that is good at making HTTP calls. I also want this automation to be something that other people outside of me can maintain, so if I can use a language that we typically use, that makes this more approachable.

In my case, TypeScript fit the bill as we heavily use it in my space, the docs are solid, and it's straightforward to make HTTP calls. I'm also a fan of functional programming, which TypeScript supports nicely.

My major hurdle at this point is that I'd like to execute this single file of TypeScript, and the only way I knew how to do that was by spinning up a Node application and using something like ts-node to execute the file.

Talking to a colleague, they recommended I check out Deno as a possible solution. The more I learned about it, the more I thought this would fit perfectly. It supports TypeScript out of the box (no configuration needed), and a file can be ran with deno run, no other tools needed.

This project is simple enough that if Deno wasn't a good fit, I could always go back to Node.

With this figured out, we're going to create a Deno application using TypeScript as our language of choice.

Getting Started With Deno

  1. Install Deno via these instructions
  2. Setup your dev environment - I use VS Code, so adding the recommended extension was all I needed.

Trying Deno Out

Once Deno has been installed and configured, you can run the following script and verify everything is working correctly. It creates a new directory called deno-coffee, writes a new file and executes it via deno.

1
2
3
4
mkdir deno-coffee
cd deno-coffee
echo 'console.log("Hello World");' >> coffee.ts
deno run coffee.ts

We've got something working, so let's start building out the random coffee script.

Let's Get Percolating

As mentioned before, we're going to hardcode a list of names and print to the screen, so let's build out the rough shape of the script:

const names = [
  "Batman",
  "Superman",
  "Green Lantern",
  "Wonder Woman",
  "Static Shock", // one of my favorite DC heroes!
  "The Flash",
  "Aquaman",
  "Martian Manhunter",
];
const pairs = createPairsFrom(shuffle(names));
const message = createMessage(pairs);
console.log(message);

This code won't compile as we haven't defined what shuffle, createPairsFrom, or createMessage does, but we can tackle these one at a time.

Let's Get Random

Since we don't want the same people meeting up every time, we need a way to shuffle the list of names. We could import a library to do this, but what's the fun in that?

In this case, we're going to implement the Fisher-Yates Shuffle (sounds like a dance move).

function shuffle(items: string[]): string[] {
  // create a copy so we don't mutate the original
  const result = [...items];
  for (let i = result.length - 1; i > 0; i--) {
    // create an integer between 0 and i
    const j = Math.floor(Math.random() * i);
    // short-hand for swapping two elements around
    [result[i], result[j]] = [result[j], result[i]];
  }
  return result;
}

const words = ["apples", "bananas", "cantaloupes"];
console.log(shuffle(words)); // [ "bananas", "cantaloupes", "apples" ]

Excellent, we have a way to shuffle. One refactor we can make is to have shuffle be generic as we don't care what array element types are, as long as we have an array.

Making this refactor gives us the following:

1
2
3
4
5
6
7
8
function shuffle<T>(items: T[]): T[] {
  const result = [...items];
  for (let i = result.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * i);
    [result[i], result[j]] = [result[j], result[i]];
  }
  return result;
}

Now, we can shuffle an array of anything. Nice!

Two of a Kind

Let's take a look at the next function, createPairsFrom. We know its type signature is going fromstring[] to something, but what?

In the ideal world, our total list of names is even, so we always have equal pairs.

1
2
3
4
{first: 'Batman', second: 'Superman'},
{first: 'Green Lantern', second: 'Wonder Woman'},
{first: 'Static Shock', second: 'The Flash'},
{first: 'Aquaman', second: 'Martian Manhunter'}

But what happens if Martian Manhunter is called away and isn't available? That would leave Aquaman without a pair to have coffee with (sad trombone noise).

In the case that we have an odd number of heroes, the last pair should instead be a triple which would look like the following:

1
2
3
{first: 'Batman', second: 'Superman'},
{first: 'Green Lantern', second: 'Wonder Woman'},
{first: 'Static Shock', second: 'The Flash', third: 'Martian Manhunter'}

Given that we've been using the word Pair to represent this grouping, we have a domain term we can use. This also means that createPairsFrom has the following type signature.

1
2
3
function createPairsFrom(names: string[]): Pair[] {
  // logic
}

But what does the Pair type look like? We can model either using an optional property or by using a discriminated union.

// Using an optional property
type Pair {
  first: string,
  second: string,
  third?: string
}

// Using Discriminated Unions
type Pair = {kind: 'double', first:string, second: string}
          | {kind: 'triple', first:string, second: string; third: string}

For now, I'm thinking of going with the optional property and if we need to tweak it later, we can.

Let's go ahead and implement createPairsFrom.

function createPairsFrom(names: string[]): Pair[] {
  // if we don't have at least two names, then there are no pairs
  if (names.length < 2) {
    return [];
  }
  const results = [];
  for (let i = 0; i <= names.length - 2; i += 2) {
    const pair: Pair = { first: names[i], second: names[i + 1] };
    results.push(pair);
  }
  if (names.length % 2 === 1) {
    // we have an odd length
    // Assign the left-over name to the third of the triple
    results[results.length - 1].third = names[names.length - 1];
  }
  return results;
}

// Example execution
console.log(createPairsFrom(["apples", "bananas", "cantaloupes", "dates"])); // [{first:"apples", second:"bananas"}, {first:"cantaloupes", second:"dates"}]
console.log(createPairsFrom(["ants", "birds", "cats"])); // [{first:"ants", second:"birds", third:"cats"}]

Similarly to shuffle, we can make this function generic as it doesn't matter what the array element types are, as long as we have an array to work with.

Refactoring to generics gives us the following:

1
2
3
4
5
type Pair<T> = { first: T; second: T; third?: T };

function createPairsFrom<T>(items: T[]): Pair<T>[] {
  // same function as before
}

To the Presses!

For the last part, we need to implement createMessage. We know it has to have the following type signature:

function createMessage(pairs: Pair<string>[]): string {}

We know the following rules.

  • When it's a double, we want the message to say, X meets with Y
  • When it's a triple, we want the message to say, X meets with Y and Z

Based on this, we need a way to map from Pair to the above string. So let's write that logic.

1
2
3
4
5
6
function createMessage(pairs: Pair<string>[]): string {
  const mapper = (p: Pair<string>) =>
    `${p.first} meets with ${p.second}${p.third ? ` and ${p.third}` : ""}`;

  pairs.map(mapper);
}

From here, we can join the strings together using the \n (newline) character.

1
2
3
4
5
6
function createMessage(pairs: Pair<string>[]): string {
  const mapper = (p: Pair<string>) =>
    `${p.first} meets with ${p.second}${p.third ? ` and ${p.third}` : ""}`;

  return pairs.map(mapper).join("\n");
}

All Coming Together

With the implementation of createMessage, we can execute our script by running deno run coffee.ts

1
2
3
4
5
6
deno run coffee.ts

"Superman meets with Wonder Woman
Batman meets with The Flash
Martian Manhunter meets with Aquaman
Static Shock meets with Green Lantern"

From here, we have a working proof of concept of our idea. We could run this manually on Mondays and then post this to our messaging channel (though you might want to switch the names out). If you wanted to be super fancy, you could have this scheduled as a cron job or through Windows Task Scheduler.

The main takeaway is that we've built something we didn't have before and can continue to refine and improve. If no one likes the idea, guess what? We only had a little time invested. If it takes off, then that's great; we can spend more time making it better.

Wrapping Up

In this post, we built the first version of our Random Coffee script using TypeScript and Deno. We focused on getting our tooling working and building out the business rules for shuffling and creating the pairs.

In the next post, we'll look at making this script smarter by having it retrieve a list of names dynamically from GitHub's API!

As always, you can find a full working version of this bot on my GitHub.

The Humble Function - Foundation to Functional Programming

When learning about functional programming, you won't go far before you run into the concept of a function. But we're not talking about some syntax or keywords in the language, but from a mathematical sense.

A function is a mapping between two sets such that for every element in the first set, it's mapped to a single element in the second set.

Since a set is a collection of elements, this is similar to a type where values are part of that type. With this understanding, we can modify our definition of a function to be:

A function is a mapping between two types such that for every value in the first type, it's mapped to a single value in the second type.

So What Does a Function Look Like?

Before diving into code, let's build up our understanding of functions more. When drawing out the function, we can model it like this.

Generic mapping from A to B

Generic mapping from A to B

The first type (A) is a circle where each possible value is listed, using ... to denote a pattern. The arrows map from each value of A to a value in B.

With this example, we know we have a function if the mapping satisfies the following rule:

Every element on the left maps to a single element on the right.

This rule seems easy enough to follow, but let's look at a mapping where this rule doesn't hold.

Functional Heartbreak

Let's say that we needed to write some code that could take a given month and return the number of days it has. Given this, here's what the mapping would look like.

Days in Month Mapping

Mapping from month name to days in month

To check if we have a function, we need to see if there's any element on the left with two or more arrows coming out.

In this case, February is breaking our rule because it could map to 28 or 29, depending on if it's a leap year. Since there isn't a parameter to denote if it's a leap year, our mapping isn't consistent and can't be a function.

One way to fix this would be to change our type on the left from MonthName to MonthName and year. Making this change gives us this new mapping.

Days in Month Mapping with Month Name and Year

Month and year mapping to days in month

Hip to Be Square

Let's look at a mapping that is a function, the square function.

Square mapping from number to number

Square mapping from number to number

Does every value on the left map to a single value on the right?

Yep, every value does. In fact, there are some values on the left that map to the same value on the right, which isn't a problem.

If we wanted to, we could restrict the type on the right from number to non-negative number, but there's no harm in having it be wider than needed.

Kinds of Functions

With this understanding of functions, let's talk about the two kinds of functions we can write and how they interact with each other.

Pure Functions

First, we have the pure function. These functions depend wholly on their inputs and they do not interact with outside state. For example, pure functions won't interact with databases, file systems, random generation, or time.

Pure functions are great because they're easy to test, composed with other functions, and don't modify state. Another advantage is that pure functions can be replaced with the result of their call (in other words, you could replace a call to square(3) with its result, 9 and the program is the same). This is known as referential transparency and is a great tool for troubleshooting an application.

The main downside to pure functions is that since they don't talk to other systems (including input/output), it's impossible to write a useful program with just pure functions.

Impure Functions

Impure functions, on the other hand, focus on interacting with outside state. These functions will call to the database or other systems, get the time, and generate random data as needed.

They allow us to write useful programs because they interact with input/output, however, the trade off is that they can be harder to test, they don't compose, and since they modify state, you may not be able to run them multiple times and get the same result.

One way to identify an impure function is by looking at its type signatures. For example, a function that takes inputs but returns void has to be modifying state or talking to another system, otherwise, why would you call it? Another signature is a function that takes no inputs, but it can return a value (like readLine() from nodejs), where did it get the value from? Just short of returning a constant value, it had to get it from somewhere.

Building an Application

Building an application requires both pure and impure functions, but how do we leverage the best of both worlds?

When I think about software, I think about how data flows through the system. For example, if someone is using a console application, they're inputting their commands to the terminal, which in turn converts them to commands to run, where the output is displayed on the screen.

As such, an application is made of three kinds of functions.

  • Boundary functions - These are responsible for getting input/output. They should have zero business rules (or the least possible as they are impure functions.
  • Business functions - These are the business specific rules that need to be ran on the given inputs. As these are typically the most important of an application, they are designed as pure functions
  • Workflow functions - This is the combination of boundary components and business components to build something useful. Since they're using impure functions, this will also be impure, but I often use this as my composition root for the application.

Effervescent Applications with Fizz Buzz

To demonstrate, let's build a version of FizzBuzz that uses this mindset.

For the problem, we have the following requirements.

  • If the number is divisible by 3, print "Fizz".
  • If the number is divisible by 5, print "Buzz".
  • If the number is divisible by both 3 and 5, print "FizzBuzz".
  • If the number isn't divisible by any of these, then print the number.

Given that we're building a console application, we will need to support getting input via the console and printing to the console.

Let's go ahead and build up our boundary functions.

// Impure function that allows us to get number from user
function getInput(): number {
  // using prompt-sync https://github.com/heapwolf/prompt-sync
  const prompt = require("prompt-sync")({ sigint: true });
  const response = prompt("What number to calculate FizzBuzz to?");
  if (!+response || +response < 1) {
    console.log(
      "Invalid response, please enter a positive number greater than 1"
    );
    return getInput();
  }
  return +response;
}

// Function that wraps console.log for printing
function printOutput(content: string): void {
  console.log(content);
}

At this point, we have a way of getting a number via getInput and a way to print a string via printOutput. In printOutput, this is a tiny function with no business rules whatsoever. getInput, however, has some business rules about validation, but we'll see later on how to refactor this.

For now, let's leave these two and look into creating our business rule functions.

// Business rules for FizzBuzz
function calculateFizzBuzz(input: number): string {
  if (input % 3 == 0 && input % 5 == 0) {
    return "FizzBuzz";
  }
  if (input % 3 == 0) {
    return "Fizz";
  }
  if (input % 5 == 0) {
    return "Buzz";
  }
  return `${input}`;
}

// Helper function to create a range of numbers from [1..end]
function createRangeFromOneTo(end: number): number[] {
  if (number < 1) {
    return [];
  }
  return Array.from(Array[number].keys()).map((x) => x + 1);
}

With calculateFizzBuzz defined, we could write unit tests to ensure the correctness of the behavior. We could also create a mapping to double-check that we have a function.

Now, let's revisit our getInput function. We've got some business rules that deal with validation (e.g. the input must be a number and greater than 1). Given that this is a light business rule, we could leave it here; however, testing this becomes harder because we don't have a way to ensure that the validation works as expected.

To solve this problem, we could extract the validation logic to its own pure function and update getInput to use the new function.

function isInputValid(input: string): boolean {
  if (!+input) {
    return false;
  }
  return +input > 1;
}

function getInput(): number {
  // using prompt-sync https://github.com/heapwolf/prompt-sync
  const prompt = require("prompt-sync")({ sigint: true });
  const response = prompt("What number to calculate FizzBuzz to?");
  if (!isInputValid(response)) {
    console.log(
      "Invalid response, please enter a positive number greater than 1"
    );
    return getInput();
  }
  return +response;
}

Nice! With this in place, we can go ahead and implement our last function, the FizzBuzz workflow.

function runFizzBuzzWorkflow(): void {
  // Data coming in
  const maximumNumber = getInput();

  // Calculating results
  const results = createRangeFromOneTo(maximumNumber).map((x) =>
    calculateFizzBuzz(x)
  );

  // Print Results
  results.forEach((x) => printOutput(x));
}

// example invocation
runFizzBuzzWorkflow();

This is a straightforward implementation as we get the maximumNumber to calculate, create an array of numbers from 1 to maximumNumber, map each of those to their FizzBuzz representation, and then print them to the screen.

Let's go one step forward. In our example, we assumed that the input and output was coming from the console, but what if we needed to change to read and write to a file?

We could move the boundary functions to be parameters to runFizzBuzzWorkflow, let's take a look at what that would give us.

function runFizzBuzzWorkflow(
  readInput: () => number,
  writeOutput: (string) => void
) {
  // Data coming in
  const maximumNumber = readInput();

  // Calculating results
  const results = createRangeFromOneTo(maximumNumber).map((x) =>
    calculateFizzBuzz(x)
  );

  // Print Results
  results.forEach((x) => writeOutput(x));
}

// example invocations
runFizzBuzzWorkflow(getInput, printOutput); // using console read/write
runFizzBuzzWorkflow(() => 42, printOutput); // using hardcoded input with console log

With this change, we can now swap out how we can input or output by creating new functions with the right type signatures. This makes testing workflow components easy because you can stub in your own mocks (no need for mocking frameworks).

If you understand the power of switching out your boundaries, then you also understand other modern architectures like Ports and Adapters as they follow a similar principle.

Wrapping Up

In this post, we looked at what a function is, how it relates to types, and how to tell if a mapping is a function. From there, we covered the differences between pure and impure functions and how you need both to build any useful application. Finally, we wrapped up by implementing the FizzBuzz problem using this approach.

Better Domain Modeling with Discriminated Unions

When I think about software, I like designing software so that doing the right things are easy and doing the wrong things are impossible (or at least very hard). This approach is typically called falling into the pit of success.

Having a well-defined domain model can prevent many mistakes from happening just because the code literally won't let it happen (either through a compilation error or other mechanisms).

I'm a proponent of functional programming as it allows us to model software in a better way that can reduce the number of errors we make.

Let's at one of my favorite techniques discriminated unions.

Motivation

In the GitHub API, there's an endpoint that allows you to get the events that have occurred for a pull request.

Let's take a look at the example response in the docs.

[
  {
    "id": 6430295168,
    "url": "https://api.github.com/repos/github/roadmap/issues/events/6430295168",
    "event": "locked",
    "commit_id": null,
    "commit_url": null,
    "created_at": "2022-04-13T20:49:13Z",
    "lock_reason": null
  },
  {
    "id": 6430296748,
    "url": "https://api.github.com/repos/github/roadmap/issues/events/6430296748",
    "event": "labeled",
    "commit_id": null,
    "commit_url": null,
    "created_at": "2022-04-13T20:49:34Z",
    "label": {
      "name": "beta",
      "color": "99dd88"
    }
  },
  {
    "id": 6635165802,
    "url": "https://api.github.com/repos/github/roadmap/issues/events/6635165802",
    "event": "renamed",
    "commit_id": null,
    "commit_url": null,
    "created_at": "2022-05-18T19:29:01Z",
    "rename": {
      "from": "Secret scanning: dry-runs for enterprise-level custom patterns (cloud)",
      "to": "Secret scanning: dry-runs for enterprise-level custom patterns"
    }
  }
]

Based on the name of the docs, it seems like we'd expect to get back an array of events, let's call this TimelineEvent[].

Let's go ahead and define the TimelineEvent type. One approach is to start copying the fields from the events in the array. By doing this, we would get the following.

type TimelineEvent = {
  id: number;
  url: string;
  event: string;
  commit_id?: string;
  commit_url?: string;
  created_at: string;
  lock_reason?: string;
  label?: {
    name: string;
    color: string;
  };
  rename?: {
    from: string;
    to: string;
  };
};

The Problem

This definition will work, as it will cover all the data. However, the problem with this approach is that lock_reason, label, and rename had to be defined as nullable as they can sometimes be specified, but not always (for example, the lock_reason isn't specified for a label event).

Let's say that we wanted to write a function that printed data about TimelineEvent, we would have to write something like the following:

1
2
3
4
5
6
7
8
9
function printData(event: TimelineEvent) {
  if (event.event === "labeled") {
    console.log(event.label!.name); // note the ! here, to tell TypeScript that I know it'll have a value
  } else if (event.event == "locked") {
    console.log(event.lock_reason);
  } else {
    console.log(event.rename!.from); // note the ! here, to tell Typescript that I know it'll have a value
  }
}

The main problem is that the we have to remember that the labeled event has a label property, but not the locked property. It might not be a big deal right now, but given that the GitHub API has over 40 event types, the odds of forgetting which properties belong where can be challenging.

The pattern here is that we have a type TimelineEvent that can have different, separate shapes, and we need a type that can represent all the shapes.

The Solution

One of the cool things about Typescript is that there is a union operator (|), that allows you to define a type as one of the other types.

Let's refactor our TimelineEvent model to use the union operator.

First, we need to define the different events as their own types

type LockedEvent = {
  id: number;
  url: string;
  event: "locked"; // note the hardcoded value for event
  commit_id?: string;
  commit_url?: string;
  created_at: string;
  lock_reason?: string;
};

type LabeledEvent = {
  id: number;
  url: string;
  event: "labeled"; // note the hardcoded value for event
  commit_id?: string;
  commit_url: string;
  created_at: string;
  label: {
    name: string;
    color: string;
  };
};

type RenamedEvent = {
  id: number;
  url: string;
  event: "renamed"; // note the hardcoded value for event
  commit_id?: string;
  commit_url?: string;
  created_at: string;
  rename: {
    from: string;
    to: string;
  };
};

At this point, we have three types, one for each specific event. A LockedEvent has no knowledge of a label property and a RenamedEvent has no knowledge of a lock_reason property.

Next, we can update our definition of TimelineEvent to use the union operator as so.

type TimelineEvent = LockedEvent | LabeledEvent | RenamedEvent;

This would be read as A TimelineEvent can either be a LockedEvent or a LabeledEvent or a RenamedEvent.

With this new definition, let's rewrite the printData function.

1
2
3
4
5
6
7
8
9
function printData(event: TimelineEvent) {
  if (event.event == "labeled") {
    console.log(event.label.name); // note that we no longer need !
  } else if (event.event == "locked") {
    console.log(event.lock_reason);
  } else {
    console.log(event.rename.to); // note that we no longer need !
  }
}

Not only do we not have to use the ! operator to ignore type safety, but we also have better autocomplete (note that locked_reason and rename don't appear when working with a labeled event). Better autocomplete

Deeper Dive

At a general level, what we've modeled is a sum type and it's great for when you have a type that can take on a finite number of differing shapes.

Sum types are implemented as either tagged unions or untagged unions. Typescript has untagged unions, however, other languages like Haskell and F#, use tagged unions. Let's see what the same implementation in F# would have looked like.

// specific type definitions omitted since they're
// similar to typescript definition
// ....
type TimelineEvent = Locked of LockedEvent | Labeled of LabeledEvent | Renamed of RenamedEvent

let printData e =
    match e with
    | Locked l -> printf "%s" l.lock_reason
    | Labeled l -> printf "%s" l.label.name
    | Renamed r -> printf "%s" r.rename.``to`` // the `` is needed here as to is a reserved word in F#

A tagged union is when each shape has a specific constructor. So in the F# version, the Locked is the tag for the LockedEvent, Labeled is the tag for the LabeledEvent, so on and so forth. In the Typescript example, we worked around it because the event property is on every TimelineEvent and is a different value.

If that wasn't true, then we would had to have added a field to TimelineEvent (typically called kind or tag) that would help us differentiate between the various shapes.

Wrapping Up

When defining domain models where the model can have different shapes, you can use a sum type to define the model.