Skip to content

Today I Learned

Leveraging Tuples in TypeScript

In preparation for StirTrek, I'm revisiting my approach for how to implement the game of Blackjack. I find card games to be a great introduction to functional concepts as you hit the major concepts quickly and the use cases are intuitive.

Let's take a look at one of the concepts in the game, Points.

Blackjack is played with a standard deck of cards (13 Ranks and 4 Suits) where the goal is to get the closest to 21 points without going over. A card is worth Points based on its Rank. So let's go ahead and model what we know so far.

1
2
3
type Rank = "Ace" | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | "Jack" | "Queen" | "King"
type Suit = "Hearts" | "Clubs" | "Spades" | "Diamonds"
type Card = {readonly rank:Rank, readonly suit:Suit}

We know that a Card is worth points based on its rank, the rule are:

  • Cards with a Rank of 2 through 10 are worth that many points (i.e., 2's are worth 2 points, 3's are worth 3 points, ..., 10's are worth 10 points)
  • Cards with a Rank of Jack, Queen, or King are worth 10 points
  • Cards with a Rank of Ace can be worth either 1 or 11 points (depending on which one is the most advantageous)

Let's explore the Ace in more detail.

For example, if we had a hand consisting of an Ace and a King, then it could be worth either 11 (treating the Ace as a 1) or as 21 (treating the Ace as an 11). In this case, we'd want to treat the Ace as an 11 as that gives us 21 exactly (specifically, a Blackjack).

In another example, if we had a hand consisting of an Ace, 6, and Jack, then it could either be worth 17 (treating the Ace as a 1) or 27 (treating the Ace as an 11). Since 27 is greater than 21 (which would cause us to bust), we wouldn't want the Ace to be worth 11.

Creating cardToPoints

Now that we have this detail, let's take a look at trying to write the cardToPoints function.

function cardToPoints(c:Card): Points { // Note we don't know what the type of this is yet
  switch(c.rank) {
    case 'Ace': return ???
    case 'King': return 10;
    case 'Queen': return 10;
    case 'Jack': return 10;
    default:
      return c.rank; // we can do this because TypeScript knows all the remaining options for Rank are numbers
  }
}

At this point, we don't know how to score Ace because we would need to know the other cards to get points for. Since we don't have that context here, why not capture both values?

function cardToPoints(c:Card): Points { // Note we don't know what the type of this is yet
  switch(c.rank) {
    case 'Ace': return [1,11];
    case 'King': return 10;
    case 'Queen': return 10;
    case 'Jack': return 10;
    default:
      return c.rank; // we can do this because TypeScript knows all the remaining options for Rank are numbers
  }
}

In TypeScript, we can denote a tuple by using []. Going forward, TypeScript knows that it's a two element array and guarantees that we can index using 0 or 1.

This works, however, anything using cardToPoints has to deal with that it could either be a number or a tuple.

When I come across cases like this, I reach for setting up a sum type to model each case.

1
2
3
type Hard = {tag:'hard', value:number};
type Soft = {tag:'soft', value:[number,number]}; // note that value here is a tuple of number*number
type Points = Hard | Soft

Now, when I call cardToPoints, I can use the tag field to know whether I'm working with a number or a tuple.

Adding Points Together

A common workflow in Blackjack is to figure out how many points someone has. At a high level, we'd want to do the following

  • Convert each Card to Points
  • Add all the Points together

Summing things together is a common enough pattern, so we know our code is going to look something like this:

1
2
3
function handToPoints(cards:Card[]): Points {
  return cards.map((c)=>cardToPoints(c)).reduce(SOME_FUNCTION_HERE, SOME_INITIAL_VALUE_HERE);
}

We don't have the reducer function defined yet, but we do know that it's a function that'll take two Points and return a Points. So let's stub that out.

1
2
3
function addPoints(a:Points, b:Points): Points {
  // implementation
}

Since we modeled Points as a sum type, we can use the tag field to go over the possible cases

function addPoints(a:Points, b:Points): Points {
  if (a.tag === 'hard' && b.tag === 'hard') {
    // logic
  }
  if (a.tag === 'hard' && b.tag === 'soft'){
    // logic
  }
  if (a.tag === 'soft' && b.tag === 'hard'){
    // logic
  } 
  // last case is both of them are soft
}

With this skeleton in place, let's start implementing each of the branches

Adding Two Hard Values

The first case is the easiest, if we have two hard values, then we add their values together. So a King and 7 us a 17 for example.

1
2
3
4
function addHardAndHard(a:Hard, b:Hard): Points { // note that I'm defining a and b as Hard and not just Points
  const value = a.value + b.value;
  return {tag:'hard', value};
}

With this function defined, we can update addPoints like so

1
2
3
4
5
6
function addPoints(a:Points, b:Points): Points {
  if (a.tag === 'hard' && b.tag === 'hard'){
    return addHardAndHard(a,b);
  }
  // other branches
}

Adding Hard and Soft

The next two cases are the same, where we're adding a Hard value to a Soft value. For example, we're adding a 6 to an Ace. We can't assume that the answer is 7 since that might not be what the player wants. We also can't assume that the value is 17 because that might not be to the players advantage, which means that we need to keep track of both options, which implies that the result would be a Soft value. Let's go ahead and write that logic out

1
2
3
4
function addHardAndSoft(a:Hard, b:Soft): Points { // note that a is typed to be Hard and b is typed as Soft
  const [bLow, bHigh] = b.value; // destructuring the tuple into specific pieces
  return {tag:'soft', value:[a.value+bLow, a.value+bHigh]};
}

With this function in place, we can write out the next two branches

function addPoints(a:Points, b:Points): Points {
  if (a.tag === 'hard' && b.tag === 'hard'){
    return addHardAndHard(a, b);
  }
  if (a.tag === 'hard' && b.tag === 'soft'){
    return addHardAndSoft(a, b);
  }
  if (a.tag === 'soft' && b.tag === 'hard'){
    return addHardAndSoft(b, a); 
  }
  // remaining logic
}

Adding Soft and Soft

The last case we need to handle is when both Points are Soft. If we were to break this down, we have four values (aLow, aHIgh for a, and bLow,bHigh for b) we need to keep track of:

  1. aLow + bLow
  2. aHigh + bLow
  3. aLow + bHigh
  4. aHigh + bHigh

However, let's play around with this by assuming that Points in question are both Ace. We would get the following:

  1. aLow + bLow = 1 + 1 = 2
  2. aHigh + bLow = 11 + 1 = 12
  3. aLow + bHigh = 1 + 11 = 12
  4. aHigh + bHigh = 11 + 11 = 22

Right off the bat, we can discard the case 4, (aHigh + bHigh), because there is no situation where the player would want that score as they would bust.

For cases 2 and 3, they yield the same value, so they're essentially the same case.

Which means, that our real cases are

  1. aLow + bLow
  2. aHigh + bLow (which is the same as aLow + bHigh)

So let's go ahead and write that function

1
2
3
4
5
function addSoftAndSoft(a:Soft, b:Soft): Points {
  const [aLow, aHigh] = a.value;
  const [bLow] = b.value; // note that we're only grabbing the first element of the tuple here
  return {tag:'soft', value:[aLow+bLow, aHigh+bLow]};
}

Which gives us the following for addPoints

function addPoints(a:Points, b:Points): Points {
  if (a.tag === 'hard' && b.tag === 'hard'){
    return addHardAndHard(a, b);
  }
  if (a.tag === 'hard' && b.tag === 'soft'){
    return addHardAndSoft(a, b);
  }
  if (a.tag === 'soft' && b.tag === 'hard'){
    return addHardAndSoft(b, a);
  }
  return addSoftAndSoft(a as Soft, b as Soft);
}

Now that we have addPoints, let's revisit handToPoints

1
2
3
4
5
6
7
8
// Original Implementation
// function handToPoints(cards:Card[]): Points {
//   return cards.map((c)=>cardToPoints(c)).reduce(SOME_FUNCTION_HERE, SOME_INITIAL_VALUE_HERE;
// }

function handToPoints(cards:Card[]): Points {
  return cards.map((c)=>cardToPoints(c)).reduce(addPoints, SOME_INITIAL_VALUE_HERE);
}

Now we need to figure out what SOME_INITIAL_VALUE_HERE would be. When working with reduce, a good initial value would be what would we return if we had no cards in the hand? Well, they would have 0 points, right? We can use 0, but we can't just return 0 since our function returns Points, so we need to go from 0 to Points. Easy enough, we can use Hard to accomplish this.

1
2
3
4
5
6
7
function handToPoints(cards:Card[]): Points {
  const initialValue:Points = {tag:'hard', value:0};
  return cards.map((c)=>cardToPoints(c)).reduce(addPoints, initialValue);
}

const hand = [{rank:'Ace', suit:'Hearts'}, {rank:7, suit:'Clubs'}]
console.log(handToPoints(hand)); // {tag:'soft', value:[8, 18]};

For those who know a bit of category theory, you might notice that addPoints is the operation and Hard 0 is the identity for a monoid over Points.

One Last Improvement

So this code works and everything is fine, however, we can make one more improvement to addPoints. Let's take a look at what happens when we try to get the Points for the following:

1
2
3
4
5
6
7
8
const hand: Card[] = [
  {rank:'Ace', suit:'Diamonds'},
  {rank:8, suit:'Hearts'},
  {rank:4, suit:'Clubs'},
  {rank:8, suit:'Spades'}
]

console.log(handToPoints(hand)); // {tag:'soft', value:[21, 31]};

Huh, we got the right value, but we know that for Soft, it doesn't make sense to allow the player a choice between 21 and 31 because 31 is always invalid. Even though the answer isn't wrong per se, it does allow the user to do the wrong thing later on, which isn't the greatest.

Let's add one more function, normalize that will check to see if the Points is Soft with a value over 21. If so, we convert to a Hard and throw out the value over 21. Otherwise we return the value (since it's possible for someone to get a Hard score over 21).

function normalize(p:Points): Points {
  if (p.tag === 'soft' && p.value[1] > 21){
    return {tag:'hard', value:p.value[0]}
  }
  return p;
}

// updated addPoints with normalize being used
function addPoints(a:Points, b:Points): Points {
  if (a.tag === 'hard' && b.tag === 'hard'){
    return normalize(addHardAndHard(a, b));
  }
  if (a.tag === 'hard' && b.tag === 'soft'){
    return normalize(addHardAndSoft(a, b));
  }
  if (a.tag === 'soft' && b.tag === 'hard'){
    return normalize(addHardAndSoft(b, a));
  }
  return normalize(addSoftAndSoft(a as Soft, b as Soft));
}

// Note: There's some minor refactoring that we could do here (for example, creating an internal function for handling the add logic and updating `addPoints` to use that function with normalize),
// but will leave that as an exercise to the reader :)

Wrapping Up

In this post, we took a look at using tuples in TypeScript by tackling a portion of the game of Blackjack. Whether it's through using it in types (like we did for Soft) or for destructuring values (like we did in the various addX functions), they can be a handy way of grouping data together for short-term operations.

Interested in knowing more?

If you've enjoyed the above, then you might be interested in my new course (launching Summer 2025) where we build out the game of Blackjack using these concepts in TypeScript. Click here if you're interested in getting an update for when the course goes live!

Tips and Tricks with TypeScript

One of my most recent projects has been tackling how to model the card game, Love Letter. For those who've seen me present my How Functional Programming Made Me a Better Developer talk, you might recall that this was my second project to tackle in F# and that even though I was able to get some of it to work, there were some inconsistency in the rules that I wasn't able to reason about.

While implementing in TypeScript, I came across some cool tricks and thought I'd share some of them here, enjoy!

Swapping two variables

A common enough task in programming, we need to swap two values around. When I was learning C++, I had a memory trick to remember the ordering (think like a zigzag pattern)

1
2
3
4
5
6
7
int a = 100;
int b = 200;

// Note how a starts on the right, then goes left
int temp = a;
a = b;
b = temp;

You can do the same thing in TypeScript, however, you can remove the need for the temp variable by using array destructuring. The idea is that we create an array which contains the two variables to swap. We then assign this array to destructured variables (see below)

1
2
3
4
5
6
let a:number = 100;
let b:number = 200;

// using array destructuring

[a,b] = [b,a]

Drawing a Card

One of the common interactions that happens in the game is that we need to model drawing a card from a deck. As such, we will have a function that looks like the following

1
2
3
4
5
6
7
8
type Card = "Guard" | "Priest" | "Baron" | .... // other options omitted for brevity
type Deck = Card[]

function draw(d:Deck): {card:Card, restOfDeck:Deck} {
  const card = d[0];
  const restOfDeck = d.slice(1);
  return {card, restOfDeck};
}

This absolutely works, however, we can use the rest operator (...) with array destructuring to simplify this code.

1
2
3
4
5
6
7
type Card = "Guard" | "Priest" | "Baron" | .... // other options omitted for brevity
type Deck = Card[]

function draw(d:Deck): {card:Card, restOfDeck:Deck} {
  const [card, ...restOfDeck] = d; // note the rest operator before restOfDeck
  return {card, restOfDeck};
}

This code should be read as assign the first element of the array to a const called card and the rest of the array to a const called restOfDeck.

Drawing Multiple Cards with Object Destructuring

function draw(d:Deck): {card:Card, restOfDeck:Deck} {
  const [card, ...restOfDeck] = d;
  return {card, restOfDeck};
}

function drawMultipleCads(d:Deck, count:number): {cards:Card[], restOfDeck:Deck} {
  let currentDeck = d;
  const cards:Card[] = [];
  for (let i = 0; i < count; i++) {
    const result = draw(currentDeck)
    cards.push(result.card)
    currentDeck = result.restOfDeck
  }
  return {cards, restOfDeck:currentDeck};
}

This works, however, we don't actually care about result, but the specific values card and restOfDeck. Instead of referencing them via property drilling, we can use object destructuring to get their values.

function drawMultipleCads(d:Deck, count:number): {cards:Card[], restOfDeck:Deck} {
  let currentDeck = d;
  const cards:Card[] = [];
  for (let i = 0; i < count; i++) {
    const {card, restOfDeck} = draw(currentDeck) // note using curly braces
    cards.push(card)
    currentDeck = restOfDeck
  }
  return {cards, restOfDeck:currentDeck};
}

Today I Learned: Triggering PowerAutomate Through Http Request

If you're working within the Microsoft ecosystem, you might have ran across a tool called PowerAutomate, this is Microsoft's low-code solution for building out simple automation (similar to tools like If This Then Than (IFTTT)).

A common use I've used with PowerAutomate is integrating with Teams to kick off a workflow when someone posts a message. For example, you could have a trigger listening for when someone posts in your support channel to auto-respond back with a canned message and tagging whoever your support person or support team is.

The problem I've ran into with PowerAutomate is that it has a steep learning curve and isn't very intuitive for engineers to pick up. As such, we typically use PowerAutomate for simpler flows (or if we need to integrate with another Microsoft product like Teams or Planner).

Recently, I was tackling a problem where we had an application that needed to post a message into a Teams channel. Back in the day, I would have use a webhook and published the message that way, however, Microsoft removed the ability to use Webhooks into Teams in late 2024. The main solution going forward is that you would have had to build a Teams application and install it for your tenant/channel. This can be a bit annoying if all you have is a simple workflow that you need to stand-up.

This got me thinking, I know I can use PowerAutomate to send a message to Teams, but can I trigger the flow programmatically (at this point, I was familiar with timers and when something happened).

Doing some digging, I found that PowerAutomate can trigger a workflow off of a HTTP Request.

Jackpot!

Even better, we can give the PowerAutomate flow a specific payload to expect as part of the request body, even better!

Combining these concepts, I can create a flow like the following:

First, let's set up a trigger that takes in a payload. In this case, the caller will give us the message content and a bit more info on which team and channel to post this message into.

Setting up the trigger

After setting up the trigger, we can then add a step for posting a message in Teams. Instead of using the pre-populated options that we get from our connection, we can use the values from the trigger instead.

Setting up the message step

After saving, our entire flow looks like the following

Full flow

From here, we can then use the URL that was created as part of the trigger and have our application invoke the flow.

Today I Learned: Configuring Git to Use A Specific Key for a Specific Repo

When working with a Git repository, there are two ways to authenticate yourself, either by using your name/password or by leveraging an SSH key, proving who you are. I like the SSH key the best since I can create the key for my machine, associate it to my profile, and then I'm good to go.

However, the problem becomes when I have to juggle different SSH keys. For example, I may have two different accounts (personal and a work one) and these accounts can't use the same SSH key (in fact, if you try to add the same SSH key to two different accounts, you'll get an error message).

In these cases, I'd like to be able to specify which SSH key to use for a given repository.

Doing some digging, I found the following configures the sshCommand that git uses

git config core.sshCommand 'ssh -i C:\\users\\<name>\\.ssh\\<name_of_private_key>'

Because we're not specifying a scope (like --global), this will only apply to the single repository.

Today I Learned: Configuring HttpClient via Service Registration

When integrating with an external service via an API call, it's common to create a class the encapsulates dealing with the API. For example, if I was interacting with the GitHub API, I might create a C# class that wraps the HttpClient, like the following:

public interface IGitHubService
{
    Task<string> GetCurrentUsername();
}

public class GitHubService : IGitHubService
{
    private readonly HttpClient _client;
    public GitHubService(HttpClient client)
    {
        _client = client;
    }
    public async Task<string> GetCurrentUsername()
    {
        // code implementation
    }
}

Repetition of Values

This is a great start, but over time, your class might end up like the following:

public class GitHubService
{
    private readonly HttpClient _client;
    public GitHubService(HttpClient client)
    {
        _client = client;
    }
    public async Task<string> GetCurrentUsername()
    {
        var result = _client.GetFromJsonAsync("https://api.github.com/user")
        return result.Login;
    }
    public async Task<List<string>> GetAllUsers()
    {
        var result = _client.GetFromJsonAsync("https://api.github.com/users");
        return result.Select(x => x.Login).ToList();
    }
    public async Task<List<string>> GetTeamNamesForOrg(string org)
    {
        var result = _client.GetFromJsonAsync($"https://api.github.com/orgs/{org}/teams");
        return result.Select(x => x.Name).ToList();
    }
}

Right off the bat, it seems like we're repeating the URL for each method call. To remove the repetition, we could extract to a constant.

public class GitHubService
{
    private readonly HttpClient _client;
    // Setting the base URL for later usage
    private const string _baseUrl = "https://api.github.com";
    public GitHubService(HttpClient client)
    {
        _client = client;
    }
    public async Task<string> GetCurrentUsername()
    {
        var result = _client.GetFromJsonAsync($"{_baseUrl}/user")
        return result.Login;
    }
    public async Task<List<string>> GetAllUsers()
    {
        var result = _client.GetFromJsonAsync($"{_baseUrl}/users");
        return result.Select(x => x.Login).ToList();
    }
    public async Task<List<string>> GetTeamNamesForOrg(string org)
    {
        var result = _client.GetFromJsonAsync($"{_baseUrl}/orgs/{org}/teams");
        return result.Select(x => x.Name).ToList();
    }
}

This helps remove the repetition, however, we're now keeping track of a new field, _baseUrl. Instead of using this, we could leverage the BaseAddress property and set that in the service's constructor.

public class GitHubService
{
    private readonly HttpClient _client;
    public GitHubService(HttpClient client)
    {
        _client = client;
        _client.BaseAddress = "https://api.github.com"; // Setting the base address for the other requests.
    }
    public async Task<string> GetCurrentUsername()
    {
        var result = _client.GetFromJsonAsync("/user")
        return result.Login;
    }
    public async Task<List<string>> GetAllUsers()
    {
        var result = _client.GetFromJsonAsync("/users");
        return result.Select(x => x.Login).ToList();
    }
    public async Task<List<string>> GetTeamNamesForOrg(string org)
    {
        var result = _client.GetFromJsonAsync($"/orgs/{org}/teams");
        return result.Select(x => x.Name).ToList();
    }
}

I like this refactor because we remove the field and we have our configuration in one spot. That being said, interacting with an API typically requires more information than just the URL. For example, setting up the API token or that we're always expecting JSON for the response. We could add the header setup in each method, but that seems quite duplicative.

Leveraging Default Request Headers

We can centralize our request headers by leveraging the DefaultRequestHeaders property and updating our constructor.

public class GitHubService
{
    private readonly HttpClient _client;
    public GitHubService(HttpClient client)
    {
        _client = client;
        _client.BaseAddress = "https://api.github.com";
        _client.DefaultRequestHeaders.Add("Accept", "application/vnd.github+json");
        _client.DefaultRequestHeaders.Add("Authentication", $"Bearer {yourTokenGoesHere}");
        _client.DefaultRequestHeaders.Add("X-GitHub-Api-Version", "2022-11-28");
    }
    public async Task<string> GetCurrentUsername()
    {
        var result = _client.GetFromJsonAsync("/user")
        return result.Login;
    }
    public async Task<List<string>> GetAllUsers()
    {
        var result = _client.GetFromJsonAsync("/users");
        return result.Select(x => x.Login).ToList();
    }
    public async Task<List<string>> GetTeamNamesForOrg(string org)
    {
        var result = _client.GetFromJsonAsync($"/orgs/{org}/teams");
        return result.Select(x => x.Name).ToList();
    }
}

I like this refactor because all of our configuration of the service is right next to how we're using it, so easy to troubleshoot. At this point, we would need to register our service in the Inversion of Control (IoC) container and then everything would work.

Generally, you'll find this in Startup.cs and would look like:

services.AddTransient<IGitHubService, GitHubService>();

An Alternative Approach for Service Registration

However, I learned that when you're building a service that's wrapping an HttpClient, there's another service registration method you could use, AddHttpClient with the Typed Client approach.

Let's take a look at what this would look like.

1
2
3
4
5
6
7
8
// In Startup.cs

services.AddHttpClient<IGitHubService, GitHubService>(client => {
    client.BaseAddress = new Uri("https://api.github.com");
    client.DefaultRequestHeaders.Add("Accept", "application/vnd.github+json");
    client.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiTokenGoesHere}");
    client.DefaultRequestHeaders.Add("X-GitHub-Api-Version", "2022-11-28");
});

We've essentially moved our configuration logic from the GitHubService to the IoC container, simplifying the service.

public class GitHubService : IGitHubService
{
    private readonly HttpClient _client;
    public GitHubService(HttpClient client)
    {
        _client = client;
    }
    public async Task<string> GetCurrentUsername()
    {
        var result = _client.GetFromJsonAsync("/user")
        return result.Login;
    }
    public async Task<List<string>> GetAllUsers()
    {
        var result = _client.GetFromJsonAsync("/users");
        return result.Select(x => x.Login).ToList();
    }
    public async Task<List<string>> GetTeamNamesForOrg(string org)
    {
        var result = _client.GetFromJsonAsync($"/orgs/{org}/teams");
        return result.Select(x => x.Name).ToList();
    }
}

My Thoughts

Even though this is a new approach, I'm kind of torn if I like it or not. On one hand, I appreciate that we can centralize the logic in one spot so that everything for the GitHubService is one spot. However, if we needed other dependencies to configure the service (for example, we needed to get the bearer token from AppSettings), I could see this getting a bit more complicated, though contained.

On the other hand, we could shift all that config to the IoC and let it deal with that. It definitely streamlines the GitHubService so we can focus on the endpoints and their logic, however, now I've got to look for two spots to see where the client is being configured.

Year in Review: 2024

As the year comes to a close, I like to take this time to reflect on the past year: what got done, how do I feel like things are progressing, and are there any changes that I should be working on for the next year.

With that in mind, I generally break my goals into three sections: what progress did I make for my career? what progress did I make for the community, and finally, what progress did I make for myself?

Professional

This year, I focused more on learning about consulting and what a successful engagement looks like. As such, this year was a good year as I've had experience working with both larger companies (Fortune 500) and smaller companies. All in all, I find consulting to be a good application of my skills and is a natural inclination for my process improvement skills.

  • Completed engagement with a Fortune 500 client to teach TypeScript and AWS (members were coming from COBOL)
  • Mentored two interns and helped get them hired full-time
  • Designed a system for custom form creation for an education company (which led to follow-up development work)
  • Started coaching engagement for an AgTech company focusing on delivery and process improvements
  • Designed a system for database health checks for a Database Administrator as a Service company

To help Lean TECHniques with their Microsoft partnership, I spent time over the summer working on obtaining various Microsoft certifications (I ran out of time before getting the Microsoft Certified: Azure Solutions Architect Expert certificate, but that's on my docket for 2025)

Last, but not least, I spent some time learning some (new to me tech) like GraphQL and basic Java.

Community

On the community side, 2024 was a year of firsts. For example, I hosted and organized Queen City Code Camp, a conference where attendees learned the fundamentals of Test Driven Development (TDD) by solving the Mars Rover kata. This was the first time since 2019 that I organized a conference (and absolutely the first time since I moved to North Carolina), so lots of good lessons learned here.

In another first, I created my first training video, Navigating Mars Using Functional Programming in TypeScript, a Udemy course where I walk you through how to solve the Mars Rover kata using functional programming concepts.

Lastly, I created two new talks this year: How to Make Your Own Automation Framework where I show you how to build your own framework using Deno, TypeScript, and GitHub Action; and How to Build More Resilient Teams where I show you how to use post-mortem and experiments to build high performing teams. In general, I shoot to create one new talk a year and I over-delivered on that goal this year (with 2025 already looking to be in good shape).

Outside of new talks, I authored 23 different blogs posts with my most common topics being Leadership and Functional Programming and presented at 11 different events.

Personal

  • Read 32 books in various genres (biographical, investing, leadership, history, consulting, process, science fiction, and general fiction)
  • Learned the basics of knitting, made scarves and baby blankets using both seed and garter stitches
  • Learned the basics of working with chisels and planes (including sharpening) for woodworking
  • Built a catio for our new cat, Trix. Catio for Tix

  • Learned how to make two different kinds of coffee drinks (Americano and Latte)

  • Built my first computer from components (major shout-out to Isaac for helping me figure out the components)
  • Picked up over 30 bags of trash around the neighborhood
  • Volunteered with Cub Scout Pack 58

What Does 2025 Have in Store?

Obtain Three Client Engagements

I've spent plenty of time at step 1 (collect information), but it's now time to start putting that information into use (for better or for worse) and get my own clients. If I'm going to be successful, then I need to apply what I learn, make mistakes, mess up, learn, and iterate. As such, I'm going to expand my connections here in Charlotte, honing in on my ideal client. From there, I can expand and see what makes sense.

Author Two Training Courses

I learned a lot about authoring a training course by going through the Udemy process. Though it has a lower barrier to entry to get started (and they handle a good amount of the marketing), the fact that the pricing can fluctuate so much is a bit bonkers. For example, I have the course listed at $99.99, with a coupon that can bring the price down to $49.99. However, Udemy can then cut the price down to $9.99. On one hand, it increases sales, however, the amount of money made (both for Udemy and myself) is not enough to to cover the cost.

As such, I'll be spending time finding a new platform for 2025 where I can host my training such that the pricing is a bit more in my control. (Udemy, if you could allow course authors to set a floor on their pricing, I'd be so much happier with the service!)

Build a Hope Chest

I come from a long line of makers. My dad worked in construction and manufacturing, various uncles worked as welders, fabricators, and construction, and my grandfather was a carpenter (and moonshiner, but that's a tale for a different day). I've always been fascinated about how things work and how to make them better, hence why I started my journey into woodworking during the pandemic (idle time and I had the space for it).

I spend my work hours building things that either won't be seen by the general public or I can't talk about the project at all, so it can be difficult to explain what I do. Plus, you can't really touch software or applications, you can use them.

Enter woodworking. I can build something with my own two hands and give it to a friend, keep it for myself, or as a lesson for me going forward. I've gotten some experience building small boxes using power tools last year, but I haven't built anything of a certain size and definitely not with hand tools.

My last major goal for 2025 is to build a hope chest as this is a solid piece of fine furniture and it can be something I can pass down to my children when it comes time.

Building this is would be tougher than anything I've built up to this point, however, it should be in the realm of my knowledge. The challenge with this build will be using strictly hand tools to build it out. It'll take me longer and it probably won't be as nice as if I had used machines, but at the end of the day, I can say that I made it.

Today I Learned - Leveraging Mock Names with Jest

I was working through the Mars Rover kata the other day and found myself in a predicament when trying to test one of the functions, the convertCommandToAction function.

The idea behind the function is that based on the Command you pass in, it'll return the right function to call. The code looks something like this.

type Command = 'MoveForward' | 'MoveBackward' | 'TurnLeft' | 'TurnRight' | 'Quit'
type Action = (r:Rover):Rover;

const moveForward:Action = (r:Rover):Rover => {
  // business rules
}
const moveBackward:Action = (r:Rover): Rover => {
  // business rules
}
const turnLeft:Action = (r:Rover):Rover => {
  // business rules
}
const turnRight:Action = (r:Rover): Rover => {
  // business rules
}
const quit:Action = (r:Rover):Rover => {
  // business rules
}

// Function that I'm wanting to write tests against.
function convertCommandToAction(c:Command): Action {
  switch (c) {
    case 'MoveForward': return moveForward;
    case 'MoveBackward': return moveBackward;
    case 'TurnLeft': return turnLeft;
    case 'TurnRight': return turnRight;
    case 'Quit': return quit;
  }
}

I'm able to write tests across all the other functions easily enough, but for the convertCommandToAction, I needed some way to know which function is being returned.

Since I don't want the real functions to be used, my mind went to leveraging Jest and mocking out the module that the actions were defined in, yielding the following test setup.

import { Command } from "./models";
import { convertCommandToAction, convertStringToCommand } from "./parsers";

jest.mock("./actions", () => ({
  moveForward: jest.fn(),
  moveBackward: jest.fn(),
  turnLeft: jest.fn(),
  turnRight: jest.fn(),
  quit: jest.fn(),
}));

describe("When converting a Command to an Action", () => {
  it("and the command is MoveForward, then the right action is returned", () => {
    const result = convertCommandToAction("MoveForward");

    // What should my expect be?
    expect(result);
  });
});

One approach that I have used in the past is jest's ability to test if a function is a mocked function, however, that approach doesn't work here because all of the functions are being mocked out. Meaning, that my test would pass, but if I returned moveBackward instead of moveForward, my test would still pass (but now for the wrong reason). I need a way to know which function was being returned.

Doing some digging, I found that the jest.fn() has a way of setting a name for a mock by leveraging the mockName function. This in turn allowed me to change my setup to look like this.

1
2
3
4
5
6
7
jest.mock("./actions", () => ({
  moveForward: jest.fn().mockName('moveForward'),
  moveBackward: jest.fn().mockName('moveBackward'),
  turnLeft: jest.fn().mockName('turnLeft'),
  turnRight: jest.fn().mockName('turnRight'),
  quit: jest.fn().mockName('quit'),
}));
Note: It turns out that the mockName function is part of a fluent interface, which allows it to return a jest.Mock as the result of the mockName call

With my setup updated, my tests can now check that the result has the right mockName.

1
2
3
4
5
6
7
8
9
describe("When converting a Command to an Action", () => {
  it("and the command is MoveForward, then the right action is returned", () => {

    // have to convert result as a jest.Mock to make TypeScript happy
    const result = convertCommandToAction("MoveForward") as unknown as Jest.Mock;

    expect(result.getMockName()).toBe("moveForward");
  });
});

Wrapping Up

If you find yourself writing functions that return other function (i.e., leveraging functional programming concepts), then you check out using mockName for keeping track of which functions are being returned.

Today I Learned: Changing the Entrypoint for a Docker Container

A common pattern for building software today is to develop using containers. This is a great strategy because it can ensure that everyone is building/deploying the same environment every time. However, like any other tooling, it can take a bit to create the file and make sure that you're setting it up correctly.

When I'm building a container, I typically will run bash in the container so I can inspect files, paths, permissions and more.

For example, let's say that I wanted to see what all is in the node image, I could run the following

docker run -it node:22 bash

And as long as the node image has bash installed, I can take a look.

This trick works just fine when I have access to the Dockerfile and can change it. But what if you didn't have access to the Dockerfile? How would you troubleshoot?

The Scenario

Let's say that I'm using a container that was created from another team, called deps. When I try to run it though, I get the following error:

docker run -it deps:latest

Node error stating that it could not find module '/app/hello.js'

Looking at the error message, it says that it couldn't find /app/hello.js. I could let the other team know and let them figure it out. However, I'd like to give them a bit more info and possible advice, so I could use the bash trick from before and see if I can spot the problem.

docker run -it deps:latest bash

Node error stating that it could not find module '/app/hello.js

Wait a minute! Why did I get the same error?

The reason is because the image has an ENTRYPOINT defined, which means that whenever the container starts, it's going to run that command. Since that command is the one that's failing, the container crashes before it executes bash or any other command.

The Solution

Since I don't have access to the Dockerfile, I need a way to change the entrypoint to allow it to run bash. Luckily, it turns out that the docker run command has a --entrypoint param that you can set to be whatever the command should be.

So let's run the container, except this time, specifying bash as the entrypoint.

docker run -it --entrypoint "bash" deps:latest

And if I run the ls command, I can see that the issue is that the file is called index.js, not hello.js.

There's not a file called hello.js, but it's called index.js

From here, I can give this information to the other team and they can make the necessary changes to their Dockerfile.

My Experience Preparing for the Azure Administrator Associate (AZ-104) Exam

There's been a bit of a lull the past couple of weeks on the blog as I've been focusing my time on studying and preparing for the AZ-104 exam. This was a particularly challenging certificate for me as I don't have a traditional IT Admin background so I had to not only shore up the gaps in that knowledge, but then also had to learn how to model similar concepts in Azure.

That being said, I was able to pass the exam on my first take and wanted to share some advice for those who are looking to take this or other Azure exams.

Expand your studying outside of the Microsoft Learn documentation

The Microsoft Learn docs are fine for doing a deep dive into a subject, but if it's the first time learning a concept, then they can be a bit rough as they assume you have knowledge that you might not. To help round out your learning, I recommend finding other resources like videos, books, or articles.

Build and Experiment in Azure

Given that most of these concepts are pretty abstract, I found that they stuck with me much more when I build out the resources. For example, when working with a Virtual Machine, all of its components need to be in the same region. You can either remember that text OR you know that has to be true because if you try building out the VM in Azure and try to change components, it's going to fail.

Don't Rely Solely on Practice Exams

Back in 2019, I was studying/preparing for the 483 (exam on C#) and the advice at the time was to go over the practice exams over and over again until things stuck. Following the same advice, I took tons of practice exams (through MS Learn and MeasureUp) and though they might have had the same format as the exam (multiple choice, drag-and-drop, etc...), none of them were a good stand-in for the real exam.

The reason being is that the exam questions likely won't ask you to define a term, but are more likely to be along the lines of how you'd solve a problem (which expects you to know the terminology inherently). So if you don't have the underlying knowledge, you're going to have a bad time trying to answer the questions.

Where the practice exams shone was helping me identify areas that I needed to focus more on. For example, if I struggled in the Networking section, then I know I needed to revisit concepts there. This helped me make the most of my studying time.

Using Generative AI To Help Understand Concepts

Given that I don't come from a networking/IT background, there were some concepts that were quite confusing to me. For example, I was trying to understand why I would need System routes if we already had Network Security Groups and Copilot was able to give me the following:

Asking Copilot why I would need system routes

To help make sure I didn't fall victim to hallucinations, I followed up on the links that Copilot provided to make sure that I understood the concepts, but given that I learn best by asking questions, this was a major win for me since you can't ask questions to books/videos.

Resources That Helped Me

For those looking to study up on this exam, I had success using these resources. Note: I do not receive compensation for these recommendations.

Today I Learned - Using TypeSpec to Generate OpenAPI Specs

Recently, I was doing analysis for a project where we needed to build out a set of APIs for consumers to use. Even though I'm a big believer of iterative design, we wanted to have a solid idea of what the routes and data models were going to look like.

In the past, I most likely would have generated a .NET Web API project, created the controllers/models, finally leveraging NSwag to generate Swagger documentation for the api. Even though this approach works, it does take more time on the implementation side (spinning up controllers, configuring ASP.NET, creating the models, adding attributes). In addition, if the actual API isn't being written in with .NET, then this code becomes throwaway pretty quickly.

Since tooling is always evolving, I stumbled across another tool, TypeSpec. Heavily influenced by TypeScript, this allows you to write your contracts and models that, when compiled, produces an OpenAPI compliant spec.

As a bonus, it's not restricted to just API spec as it has support for generating JSON schemas and gRPC's Protocol Buffers (protobuf)

Getting Started

All code for this post can be found on my GitHub.

Given that it's inspired by TypeScript, the tooling requires having Node installed (at least 20, but I'd recommend the long-term-supported (LTS) version).

From there, we can install the TypeSpec tooling with.

npm install @typespec/compiler

Even though this is all the tooling that's required, I'd recommend installing an extension for either Visual Studio or Visual Studio Code so that you can get Intellisense and other visual cues while you're writing the code.

Bootstrapping the project

Now that we've got the tooling squared away, let's create our project.

1
2
3
mkdir bookstore-api # let's make a directory to hold everything
cd bookstore-api
tsp init --template rest

Enter a project name and choose the defaults. Once it's finished bootstrapping, you can install necessary dependencies using tsp install.

Building Our First API

For our bookstore application, let's say that we want to have an inventory route where someone can retrieve information about a book.

For this work, I'm picturing the following

# Route -> GET api/inventory/{id}
# Returns 200 or 404

In the project, locate the main.tsp file and add the following

1
2
3
4
5
6
7
8
9
using TypeSpec.Http;
using TypeSpec.Rest;

@service({
    title: "Bookstore Service"
})
namespace Bookstore {

}

After adding this code, run tsp compile . (note the period). This will create a file in the tsp-output/@typespec/openapi3 folder, openapi.yaml.

We can open that file and see what our OpenAPI spec looks like

1
2
3
4
5
6
7
openapi: 3.0.0
info:
  title: Bookstore Service
  version: 0.0.0
tags: []
paths: {}
components: {}

So far, not much to look at. However, if we copy this code and render feed it to an online render (like https://editor.swagger.io/), we'll get a message about no operations.

Swagger.io saying there are no operations

Let's change that by building out our GET endpoint.

Back in main.tsp, let's add more code to our Bookstore namespace.

1
2
3
4
5
6
7
namespace Bookstore {
    @tag("Inventory")
    @route("inventory")
    namespace Inventory {
        @get op getBook(@path bookId:string): string
    }
}

After running tsp compile ., we'll see that our yaml has been updated and if we render it again, we'll have our first endpoint

Swagger.io rendering inventory by ID route

This is closer to what we want, however, we know that we're returning back a string, but a Book.

For this exercise, we'll say that a Book has the following:

  • an id (of number)
  • a title (of string)
  • a price (of number, minimum 1)
  • author name (of string)

Let's add this model to main.tsp

namespace Bookstore {
    // Note that we've added this to the Bookstore namespace
    model Book {
        id: string;
        title: string;

        @minValue(1)
        price: decimal;

        authorName: string;
    }
    @tag("Inventory")
    @route("inventory")
    namespace Inventory {

        // For our get, we're now returning a Book, instead of a string.
        @get op getBook(@path bookId: string): Book; 
    }
} 

After another run of tsp compile and rendering the yaml file, we see that we have a schema for our get method now.

Swagger showing the updated model

Refactoring a Model

Even though this works, the Book model is a bit lazy as it has the authorName as a property instead of an Author model which would have name (and a bit more information). Let's update Book to have an Author property.

model Author {
    id: string;

    @minLength(1)
    surname: string;

    @minLength(1)
    givenName: string;
}
model Book {
    id: string;
    title: string;

    @minValue(1)
    price: decimal;

    author: Author;
}

After making this change, we can see that we now have a nested model for Book.

Swagger showing both Book and Author model

Handling Failures

We're definitely a step in the right direction, however, our API definition isn't quite done. Right now, it says that we'll always return a 200 status code.

I don't know about you, but our bookstore isn't good enough to generate books with fictitious IDs, so we need to update our contract to say that it can also return 404s.

Back in main.tsp, we're going to change our return type of the @get operation to instead of being a Book, it's actually a union type.

1
2
3
4
5
6
7
8
@get op getBook(@path bookId: string): 
// Either it returns a 200 with a body of Book
{
    @statusCode statusCode: 200;
    @body book: Book;
} | { // Or it will return a 404 with an empty body
    @statusCode statusCode: 404;
};

With this final change, we can compile and render the yaml and see that route can return a 404 as well.

Swagger showing that the route can return a 404 as well

Next Steps

When I first started with TypeSpec, my first thought was that you could put this code under continuous integration (CI) and have it produce the OpenAPI format as an artifact for other teams to pull in and auto-generate clients from.

If you're interested in learning more about that approach, drop me a line at the Coaching Corner and I may write up my results in a future post.