Learning Typescript fundamentals... with cats 🐱

I'm working on learning Typescript! Let's dive into a demo where we will model some data about cats!

First, let's define some union types for our cat data.

type CatNames = 'Toby' | 'Beau' | 'Leo';
type CatColors = 'Tabby' | 'Tortoise Shell';
type CatBreeds = 'Persian' | 'Siamese' | 'Moggy';

We will then create an interface for our Cat attributes (or, Cattributes!).

interface Cattributes {
  age: number;
  breed: CatBreeds;
  color: CatColors;
}

Now lets define a type for an object where the cattributes are keys to the cats name. The shape of this object will be:

{
    catName: {
        age: number,
        breed: CatBreeds,
        color: CatColors
    }
}

To do so, we'll use the Record and Partial Utility Types.

type Cat = Partial<Record<CatNames, Cattributes>>; 

In the above, Record creates a type that can be used for objects. The first parameter is the key, and the second is the value. Above, CatNames can be our keys. The cat name key will itself take an object as its value - Cattributes.

We've pass this type to the Partial utility type so that we can add any 'part' (or N number) of CatNames.

Now lets define some cat objects, with Type safety.

const aGoodCat: Cat = {
  'Toby': {
    age: 5,
    breed: 'Persian',
    color: 'Tabby',
  }
} as const;

const anotherGoodCat: Cat = {
  'Leo': {
    age: 12,
    breed: "Persian",
    color: "Tortoise Shell"
  }
} as const;

const theBestCat: Cat = {
  'Beau': {
    age: 12,
    breed: "Siamese",
    color: "Tortoise Shell"
  }
} as const;

Lets pop our cats in a list. We'll define a type for our array of cats. Typescript will make sure we can only add objects of type Cat to our array.

type Cats = Cat[];
const catList: Cats = [aGoodCat, anotherGoodCat, theBestCat];

Next up, we'll compose a function that will lookup our catList by a particular search term, and returns all cats that match, sorted by age ascending.

For example, lets get all cats with the breed of Persian.

Our final function will have to responsibilities: 1. First, filter the cat list and return only those that match the searched term 2. Sort the results by age, ascending

First, lets define our sort function.

const sortCats = (cats: Cats): Cats => {
  return cats
    .sort((a, b) => {
      const aAge = Object.keys(a)[0] ?? undefined;
      const bAge = Object.keys(b)[0] ?? undefined;

      if (!aAge || !bAge) {
        return 0;
      }

      if (aAge < bAge) {
        return 1;
      }

      if (aAge > bAge) {
        return -1;
      }

      return 0;
    });
}

Our sort function reaches into our cat object, grabs the age, and returns a sorted array.

Now lets create our filter function. We'll start by defining types for our search term (eg. 'breed', 'color', 'age').

type SearchTerm = keyof Cattributes;
type SearchValue = CatBreeds | CatColors;

For the parameters of our function, we will use named parameters, and define an interface.

interface GetCatsParams {
  searchTerm: SearchTerm;
  searchValue: SearchValue;
  cats: Cats;
}

const filterCats = ({ searchTerm, searchValue, cats = [] }: GetCatsParams): Cats => {
  return cats
  .filter(cat => {
    const catName = Object.keys(cat)?.[0];
    const cattributes = cat[catName];

    if (cattributes[searchTerm] === searchValue) {
      return true;
    }
    
    return false;
  });
}

We've used our named parameters, and applied our interface. We've also set the return type of the function to be our Cats[].

Defining these two functions separately makes them easier to understand. It also makes writing unit tests easier, as we can test each logical concern separately.

Now, lets bring the two together.

const getCats = ({ searchTerm, searchValue, cats = [] }: GetCatsParams): Cats => {
  return sortCats(
    filterCats({ searchTerm, searchValue, cats })
  );
}

With our getCats function defined, we can now search our cat list for persians.

const persians = getCats({ searchTerm: 'breed', searchValue: 'Persian', cats: someCats });

Our search for Persians has the following results

[ 
    { 
        Toby: { 
            age: 5, 
            breed: 'Persian', 
            color: 'Tabby' 
        } 
    },
    { 
        Leo: { 
            age: 12, 
            breed: 'Persian', 
            color: 'Tortoise Shell' 
        } 
    } 
];

Woohoo! πŸŽ‰

Recap

We've used Typescript to provide type safety when creating objects. We've used utility types to create types from types. We've created 2 separate functions that use Typescript to create type safe parameters and return values.

You can see this demo working in the Typescript Playground.

To learn more about Typescript, checkout the Typescript Handbook.

πŸ‘¨πŸ»β€πŸ’» Happy coding!

Easter egg

One quirk of my above code is that it searches for any of the provided search terms. As age is one of the search terms, it's possible to search cat list by age. If you did, then it would find the relevant cats of that age. It would then try to sort them, by age.. and it wouldn't sort them, because they'd all be the same age πŸ˜‚