
The Way I Approach Code Reviews
Cassie, Tech Lead here at Cleo, outlines how she approaches code reviews.
This is some text inside of a div block with Cleo CTA
CTASigning up takes 2 minutes. Scan this QR code to send the app to your phone.
Tara outlines how to use TypeScript generics to keep your code robust and reliable.
Let’s face it — TypeScript is amazing, but sometimes it feels like you need a crystal ball to predict what data types your code might need down the road. Enter generics, your new best friend for writing code that’s both flexible and future-proof.
So, what exactly is a generic? In TypeScript, generics are like the ultimate multi-tool: they let you create reusable components that adapt to any data type without sacrificing TypeScript’s signature type safety.
Think of it this way: instead of locking yourself into a specific type from the start, generics act as placeholders that you can fill in when you actually know what your code will need to deal with. The result? Functions, classes, interfaces, and methods that are type-agnostic and ready to handle whatever data comes their way:
To truly appreciate generics, let’s take a moment to imagine life without them.
Without generics, you can find yourself writing repetitive code — components that look almost identical but differ just slightly in their type annotations. It’s tedious, inefficient, and a nightmare to maintain.
The alternative isn’t much better. Sure, you could create reusable shared components and methods, but if you resort to typing everything as any
, you might as well toss the benefits of a typed language like TypeScript’s out the window.
And worst-case scenario? Overly complex type annotations that are as fragile as they are unreadable. Good luck trying to maintain those six months down the line!
To make this more relatable, let’s take a trip to the farm. Yes, the farm—because nothing illustrates TypeScript generics better than cows, chickens, and sheep.
Problem 1: Repetitive code
Imagine you’re a farmer managing different types of animals. You need to track milk for cows, eggs for chickens, and wool for sheep. Without generics, you’d be stuck writing separate, nearly identical code for each animal — tedious and error-prone. Generics let you create a single, reusable solution for all your farm friends.
Problem 2: Lack of type safety
Now imagine if you threw all your farm data into a single bucket, regardless of type. Milk, eggs, and wool all mixed up — chaotic, right? That’s what happens when you sacrifice type safety. Generics keep things organised, ensuring each animal’s data stays neatly typed and manageable.
Problem 3: Hard-to-maintain code
As your farm grows and you add more animals, managing separate, specific code for each becomes unsustainable. Generics make your code flexible, so you can easily expand without a complete rewrite.
Now, let’s take a look at the before—what your farm looks like without generics.
You’ve got three different functions, all doing essentially the same thing, but each tailored to a specific type of animal. One function to handle cows, another for chickens, and yet another for sheep. It works, but it’s repetitive, clunky, and cries out for a better solution.
Here’s an example:
Three functions, one problem: they’re practically identical, except for the type of the data they deal with.
So what if we tried to combine these functions into one unified method?
At first glance, it seems like progress. You’ve got a single function that adapts based on the endpoint and can handle multiple types of farm data. But there are still major drawbacks:
Promise<any>
. This means you lose the ability to leverage TypeScript’s type checking, leaving you vulnerable to runtime errors.if
branch, making it harder to maintain and much more prone to bugs.While this approach is functional, it’s far from ideal. Clearly, we need a more robust solution. Enter generics.
Ready for the after?
It’s time to give our code a much-needed makeover — courtesy of our friend, generics.
This updated version uses a generic type parameter <T>
to do all the heavy lifting. Let’s break down why this approach is a game changer:
<T>
, you ensure the function can work with any data type while maintaining strong type safety. When calling the function, you pass the exact type you expect (like Cow
), and TypeScript ensures you can’t mix up your data types.Pig
) and call the same function with <Pig>
. The function adapts seamlessly, without any code duplication or complex conditionals.Thanks to generics, your code is now more concise, maintainable, and ready to scale. This is the power of TypeScript at its finest.
Setting a default type
When we specify <T = FarmAnimal>
, we make it so that if no specific type is passed to the fetchFarmData
function, T
will default to FarmAnimal
. The function has a return type of Promise<T[]>
, which means the resolved value will be an array of type T
. In practice, this means we’ll eventually end up with a list of objects with at least a name
property.
But it’s important to remember that setting a default type does not enforce any type constraints. The method is still flexible enough that you can override the default by passing a specific animal type. Here’s what happens if, unlike our default FarmAnimal
type, we call our method with a type that doesn’t have a name
property:
Generic constraints
If you want to limit the types that a generic accepts, you can specify constraints. In our imaginary farm, we want to ensure the fetchFarmData
function accepts types that satisfy the structure defined by FarmAnimal
. We achieve this by using the extends
key word: <T extends FarmAnimal>
ensures that fetchFarmData
will only accept animal types that have at least a name
property. As a result, we know that animal.name
is guaranteed to be safe, even when we don’t know what animal we’re dealing with.
So now if we try to call fetchFarmData
with an invalid type, TypeScript will recognise this straightaway and let us know:
Conditional types
We can leverage conditional types to adapt the type of the data based on the endpoint string:
T
is constrained to string
, representing the endpointAnimalType<T>
checks the endpoint and returns the appropriate array type'api/cows'
, the type resolves to an array of Cow
.'api/chickens'
, it resolves to an array of Chicken
.'api/sheep'
, it resolves to an array of Sheep
.never
is returned, preventing invalid calls at compile time.This use of conditional types allows the function to be highly adaptable based on the properties available for each type, leading to more flexible and type-safe code. It’s especially useful in scenarios where different animal types share some common properties but also have unique ones, allowing TypeScript to enforce the correct structure for each data type.
e.g.
While conditional types can ensure type safety at compile time, they don’t actually exist at runtime. This means that if the API response doesn’t match expectations (whether that’s because of a bug on the backend or simply unexpected data), things can break.
To solve this, we can do some client-side validation before returning the data. We’re already using TypeScript types for compile-time safety, so let’s add some runtime type guards to our fetchFarmData
function ensure the fetched data is in the expected format, and gracefully handle any unexpected data:
Generics shine in scenarios where you need flexibility, reusability, and type safety across different parts of your codebase. Here are some key situations where they are particularly beneficial:
Using generics in these contexts not only enhances type safety but also makes your codebase more adaptable and maintainable. They simplify how you manage data across different parts of your application and help you avoid common pitfalls related to type mismatches and runtime errors.
Whether you’re wrangling cows, chickens, or any new critter that joins your “farm,” generics help keep your code robust and reliable. It's one of those more advanced concepts that definitely broadened my understanding of TypeScript — and it all started with a PR comment from a colleague less than a year ago. She asked if one of my components should be a generic, and in all honesty I had no idea what she meant. But now, I can see how they make code more flexible and type-safe. While I don’t use them all the time, I think they’re incredibly useful in the right places.
So if you didn’t know what generics were before, or if you’ve been curious about how to incorporate them into your projects, I hope this post gives you some ideas. Next time you’re building a reusable component, or building out an API layer, spare a thought for how generics could sprinkle a little magic into your code ✨
Happy coding — and happy farming!
Cassie, Tech Lead here at Cleo, outlines how she approaches code reviews.
Fell wants you to disagree with him more. Why? Give this a read.
You didn’t hear it from me, but Cleo Engineers aren’t perfect. We have bugs. We have quite a few of them. Our relentless focus at Cleo is to make it as simple and joyful as possible for our users to level up their relationship with money.