How Union Types and Generics work in Typescript

Divyanshu Negi

🚀🚀 3 min read

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

Did this post help you ?

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.

  • No Spam
  • Unsubscribe whenever
  • Email Address

    Powered by Buttondown

    Picture of Divyanshu Negi

    Divyanshu Negi is a VP of Engineering at Zaapi Pte.

    X