# Typescript stories - Generics vs Union types

This will be a quick and more “back-to-basics” kind of article.

While doing some small refactoring, I stumbled across a situation where I had to pass an object, but the shape of the object is completely different depending on the component using that method.

Let me draw.io you the problem

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1730985855426/858426b1-fb56-4794-9b02-0e1813323d7c.png align="center")

**Objective** - Find who is **type** and not use the mighty foe - `any`.

If `any` was an accepted solution in my book, this article wouldn’t exist. This is why constraints make us creative.

# Marker interface

One of my first instincts was to use a marker interface. I don’t need functionality for the type; it’s just an object, but somehow I want to narrow down the type of objects I’m sending.

What’s a marker interface? It’s basically an interface with no methods or properties. You’re using it just for enforcing type checks.

```typescript
/// Marker interface
interface IVendorForm {}

interface Vendor0FormData {
    /// Doesn't extend FormData
}

interface Vendor1FormData extends IVendorForm {
    name: string,
    surname: string
}

interface Vendor2FormData extends IVendorForm {
    location: string
    coord_x: number
    coord_y: number
}

const sendData = (formData: IVendorForm) => {
    /// Send data
}
```

The university me would have said - Neat solution!

The present me is saying - **Why?**  
If it doesn’t have a functionality, why would it exist in the first place? (enter the existential crisis).

# Generics

What are **Generics**?

Generics allow you to define functions, classes, or interfaces with a type parameter that you can specify when you use them. This provides reusability and type checking with minimal drawbacks.

Let’s refactor the `sendData` method a bit:

```typescript
const sendData = <T>(formData: T): void => {
    /// Send data
}

sendData<Vendor1FormData>(form)
sendData<Vendor2FormData>(form)
```

Easy peasy! But wait a second…

Now I can send whatever I want to `sendData`. I’m back to square one. I have no guard and no way to narrow down the type I want to use. I just did `any` with extra steps!

# Union types

In TypeScript, we have what are called **union types**, which can hold a set of different types. In our scenario, we might have something like this:

```typescript
type VendorForm = Vendor1FormData | Vendor2FormData;

interface Vendor1FormData {
    type: 'vendor1'
    name: string;
    surname: string;
}

interface Vendor2FormData {
    type: 'vendor2'
    location: string;
    coord_x: number;
    coord_y: number;
}

const sendData = (formData: VendorForm) => {
  if (formData.type === "vendor1") {
    console.log(formData.name)
  }
  if (formData.type === "vendor2") {
    console.log(formData.location)
  }
};
```

Ok, we’re going places. Now I’ve narrowed the types that `sendData` can receive. It’s flexible enough for our scenario. The difference between the types is usually done through a discriminator.

I recommend having some sort of type discriminator instead of relying on parameters. Indeed, you have a parameter that you carry but then you have a simple logic.

If there’s another vendor form type, we can simply add a new interface for it and include it in the union type.

In most scenarios, union types should do the trick. They aren’t as flexible as generics but are more strict.

# Generics AND Union types?

Often, the best solution is found in the intersection. The same principle applies here.

There is a specific scenario where combining generics with union types creates a great developer experience when it comes to inference.

If, in our scenario, we would need to return the same type that we passed to the function, only the generics solution would maintain the inference correctly.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1730992328256/f47a2f4b-c086-41ae-8a06-7b70b16a44a9.png align="center")

If we used a union type, the returned type would be the union itself, not the specific interface that was passed in. This results in a loss of inference.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1730992442784/3ec32573-b9e8-4e49-9ee8-0b43eef30eea.png align="center")

Using both generics and union types is the sweet spot between the flexibility and type safety that we need.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1730992613325/bb630347-aa4c-492d-9ef1-3cc854d19471.png align="center")

# Summary

Let’s wrap up with a quick recap of what we’ve learned based on the scenarios discussed:

* If we’re dealing with a fixed set of types
    
* Reusability with strict type constrains
    
* Returning the exact input type
    

| **Scenario** | **Union Types** | **Generics** | **Generics + Union Types** |
| --- | --- | --- | --- |
| **Fixed set of types** | ✔️ Great for type narrowing | 🚫 Overkill for fixed types | ✔️ Does the job |
| **Reusable with strict type constraints** | 🚫 Limited to specified union | ✔️ Works for any type | ✔️ Flexible with constraints |
| **Returning exact input type** | 🚫 Returns general union | ✔️ Maintains exact type | ✔️ Maintains exact type |

Understanding when to use union types, generics, or a combination of both allows us to achieve the perfect balance of flexibility, reusability, and type safety.

PS: How about interface markers? Not today…
