TypeScript Generic with some validation

Taufan
Taufan
Mar 5 · 3 min read

Ever since learning about it, I have always thought that generic type is awesome. But now I know that it can be even more awesome!

With generic, a single type can be reused with a variety of other types. Think of Promise, it can resolve to just about anything. For example, in one case it would resolve a string, while another case expect it to resolve a boolean. As Promise type is a generic, we can assert the resolved value by passing the type into its generic, like so Promise<string> or Promise<boolean>.

Alright, enough of the basic. What if you want to have a generic which can only accept a certain type or interface?

To answer, let’s imagine we are building a very simple bookkeeper app which record transactions. A transaction can be an expense, or an income. You may even add a transfer as another form of transaction, but let’s keep it simple for now.

An expense and an income have a lot in common, the former means cash flowing out while the latter in. Since the use case would be quite similar for both, it would be a proper use of generic type. In this case, we will create an interface to implement the business use case of creating and updating the transactions.

Generic with union

Let’s first see how we can create this by using union type.

It might look like by providing the union type, the generic would validate the type passed in when implementing the interface. But this is incorrect as the union types are just for default. When used, the generic type use the type passed in instead, just like in our example with Promise.

Hence, the class below would work without any problem, which is not what we want as we want the generic to only accept a specific kind of types.

Generic with extends

Instead of providing default types, we should use extends to ensure TypeScript to validate the use of this generic.

By using extends, we tell TypeScript that this generic should only receive types that implements a certain type, or extends from it. Hence, the previous implementation would not work now as it does not adhere to the required type.

As such, we can be sure that the user who implements this interface would be adhering to the basic types that we have defined.

By doing this, we are guaranteed of two things.

First, any client implementing the contract would work with the constraints of the types we defined. This mean another class depending on this contract can be rest assured that params and result of the methods would be as expected. I.e., dependency for expense transaction would accept expense params and resolve an expense data, and so on.

Second, any changes to the transaction shape itself would have alerted the concrete classes as they have to update their implementation to follow the new contract. So if we require a transaction to also record the location it took place, now all concrete classes would have to adhere to this new type. Although in this example, it would probably better be introduced with some backward compatibility, or as a new contract altogether.

Cheers!

Nerd For Tech

From Confusion to Clarification

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store