Recently I learned something interesting about Typescript Generics. I have used Generics in Java and Kotlin but did not know Typescript is so powerful with Generics. The fantastic part about Typescript is sometimes the Type definitions feel like Magic. We can run conditions on types and whatnot. Here is a simple example that I came across and wanted to share in this blog post.
Let us assume we have this function.
// This is a function which takes 2 objects and updates the property last updated in the returning object
const updateLastUpdatedValue = (oldObject: any, newObj: any) : any => {
return {
...oldObject,
...newObj,
lastUpdated: Date.now().toString()
}
}
As you can see, this function takes 2 objects, old and new, oldObject
is updated with values from newObj
and lastUpdated
property is also updated.
Technically this object can take any type of oldObj and newObj, but we do not want that. We want this to be used by types
which need lastUpdated
properties.
Here are our different types
type UserInfo = {
name: string;
age: number;
lastUpdated: string;
}
type BookInfo = {
title: string;
author: string;
price: number;
lastUpdated: string;
}
type StoreInfo = {
pin: number;
address: string;
store_name: string;
lastUpdated: string;
}
type BuyerInfo = {
name: string
}
Each of these types has a last updated, except for BuyerInfo
, so we want to make our function updateLastUpdatedValue
to only take Objects with types UserInfo
, BookInfo
and StoreInfo
if we try to pass BuyerInfo
, it should not take and show an error.
Currently, our function updateLastUpdatedValue
can take any
object types as two params and return any
. We do not want to do that. Let's remove this any
.
We can use a UnionType
const updateLastUpdatedValue = (oldObject: UserInfo | BookInfo | StoreInfo, newObj: UserInfo | BookInfo | StoreInfo) : (UserInfo | BookInfo | StoreInfo) => {
return {
...oldObject,
...newObj,
lastUpdated: Date.now().toString()
}
}
Here is the updated function. We used |
(Pipe) to make a Union of all our types which the function can take.
But now we have a problem
console.log(updateLastUpdatedValue(user, {age: 31}))
If we do this, the second param will throw an error as it is expecting to pass a complete type, but we are only passing partial values to that object.
To fix this, we can make the second param as Partial
const updateLastUpdatedValue = (oldObject: UserInfo | BookInfo | StoreInfo, newObj: Partial<UserInfo> | Partial<BookInfo> | Partial<StoreInfo>) : (UserInfo | BookInfo | StoreInfo) => {
return {
...oldObject,
...newObj,
lastUpdated: Date.now().toString()
}
}
const updatedUserInfo : UserInfo = updateLastUpdatedValue(user, {age: 31}) as UserInfo
By doing this, the error is gone. We have another error on the variable updatedUserInfo
if we remove the as UserInfo
. I generally do not like putting these specific references. It breaks the complete TS. We are specifically removing the error because we are passing an as
if later another developer updates the main function updateLastUpdatedValue
to return some values which were never in UserInfo, we will not get any error, and all our typescript power is gone.
Ok, before fixing that, let's make a specific union type so our function does not look ugly.
type TUpdateUnion = UserInfo | BookInfo | StoreInfo;
const updateLastUpdatedValue = (oldObject: TUpdateUnion, newObj: Partial<TUpdateUnion>) : TUpdateUnion => {
return {
...oldObject,
...newObj,
lastUpdated: Date.now().toString()
}
}
const updatedUserInfo : UserInfo = updateLastUpdatedValue(user, {age: 31}) as UserInfo
Now the function looks a little neat and good to read. We moved the piped types into a separate type called TUpdateUnion
Ok, we are now removing the as
reference when using the function.
Here come the Generic function powers.
let's update our function to use generic
const updateLastUpdatedValue = <T extends TUpdateUnion>(oldObject: T, newObj: Partial<T>) : T => {
return {
...oldObject,
...newObj,
lastUpdated: Date.now().toString()
}
}
const updatedUserInfo : UserInfo = updateLastUpdatedValue(user, {age: 31})
We can now remove the reference from our variable assignment by updating our function with generics.
lets see whats happening
<T extends TUpdateUnion>
We added this before our param. Here, we are saying T
is a Generic, where we can name T anything, but as a generic, I usually name it T, and if we have to use another variable, I might call them J and K.
Ok, so here we are saying the function can take any object which is denoted by a generic name T
, the condition being, T
should extend TUpdateUnion
, so only UserInfo
, StoreInfo
, BookInfo
can be passed as T
.
Once this is passed into the params, for example, if we pass UserInfo
to the function, all the T
values will be considered as UserInfo
, so this function would act like this.
// when we pass T as UserInfo object
const updateLastUpdatedValue = (oldObject: UserInfo, newObj: Partial<UserInfo>) : UserInfo => {
return {
...oldObject,
...newObj,
lastUpdated: Date.now().toString()
}
}
So, where ever we use it, the return is automatically referred to as UserInfo
; hence no need to add as UserInfo
precisely.
This is the power of Union type and Generics; this is a straightforward example. Typescript has a lot of complex use cases, which I will update as I encounter such scenarios.
X
I'd appreciate your feedback so I can make my blog posts more helpful. Did this post help you learn something or fix an issue you were having?
Yes
No
X
If you'd like to support this blog by buying me a coffee I'd really appreciate it!
X
Subscribe to my newsletter
Join 107+ other developers and get free, weekly updates and code insights directly to your inbox.
Email Address
Powered by Buttondown
Divyanshu Negi is a VP of Engineering at Zaapi Pte.
X