WRITING // OCT 04, 2026

Don't Be Afraid of Generics

TypeScript generics can look intimidating at first, but they are one of the most useful tools for writing reusable and type-safe code. Let's break down the basics and look at a real-world example.

When I write software, I try to keep in mind that future me will eventually read my code and judge present me for it. Or, as Filipe Fortes once said:

Debugging is like being the detective in a crime movie where you are also the murderer.

So I try to write my code as reusable and well-crafted as reasonably possible. One of the tools in my box for doing that is generics.

Generics exist in many different languages such as TypeScript, Java, C++ or Go. Since I mainly write TypeScript and Java, that’s where most of my experience with them comes from. In this article, we’ll focus on TypeScript.

I also realized that although I use generics almost every day, I still had to revisit some of the basics while researching this post to properly explain them.

So follow along while we go through the basics of generics — and hopefully, by the end of this article, they won’t look quite as scary anymore.

What are generics?

What are generics anyway?

Generics are a programming feature that allows you to write classes, interfaces and functions using type placeholders. Instead of committing to one specific type up front, you can let the concrete type be determined when the code is actually used.

This allows you to write reusable code without simply giving up type safety.

But why do they look so scary?

Let’s start with a very simple example, inspired by the official TypeScript docs about generics.

const identity = (arg: number): number => {
  return arg;
};

console.log(identity(12)); // logs 12

The function takes an argument of type number and returns it.

Please do not ask about the real-world application of this function. We’ll just use it to understand the basics.

What do you do if you want exactly the same functionality for strings, booleans or any other type?

In a strongly typed language, you have to write the same function again for every possible type… right?

Wrong!

That’s where a generic can help. We can basically say:

I don’t know the concrete type yet, but whatever type goes in should also come out.

const genericIdentity = <T>(arg: T): T => {
  return arg;
};

console.log(genericIdentity(12)); // logs 12
console.log(genericIdentity("Generics are awesome!")); // logs "Generics are awesome!"
console.log(genericIdentity(true)); // logs true

Pretty awesome, right?

We no longer have to care about the concrete type when writing the function, but TypeScript still knows exactly which type is being used when we call it.

Generics look scarier than they are

I often hear that generics look scary because of all the weird symbols and letters.

<T>
<K extends keyof User>
User[K]

But once you get the hang of them, they become much less intimidating.

The simplest mental model I found is to think of a generic as another kind of variable.

A regular variable is a placeholder for a value.

A generic is a placeholder for a type.

And if you don’t like T, K, U or whatever letter someone chose, use another name. Especially for more complex generics, a meaningful name can make the code much easier to understand.

The letters themselves are not special.

But I don’t want to allow any type

A completely unconstrained generic can sometimes be more flexible than you actually need.

Often, you already know something about the types your function should accept — and TypeScript lets you express that.

Imagine we have the following interfaces:

interface SuperUser {
  name: string;
  age: number;
}

interface CustomerUser extends SuperUser {
  customerId: string;
}

interface ClientUser extends SuperUser {
  clientId: string;
}

In this scenario, we can tell our function that it should only accept values that are compatible with SuperUser.

const userIdentity = <U extends SuperUser>(arg: U): string => {
  return arg.name;
};

The important part here is:

U extends SuperUser

This does not mean that U has to be exactly SuperUser.

It means that whatever type is used for U has to contain at least the structure required by SuperUser.

That means both of our more specific user types work:

const customer: CustomerUser = {
  age: 23,
  customerId: "CUSTOMER-1",
  name: "Jon Doe",
};

const client: ClientUser = {
  age: 29,
  clientId: "CLIENT-1",
  name: "Jane Smith",
};

userIdentity(customer); // returns "Jon Doe"
userIdentity(client); // returns "Jane Smith"

So we still get the flexibility of a generic, while putting useful constraints around it.

Give me a real-world example

Examples like identity() are nice for understanding the syntax.

But where would I actually use generics in real code?

One of my favorite examples comes from React forms.

Imagine we have a user object like this:

interface User {
  name: string;
  age: number;
  active: boolean;
}

A typical form might have an input for each of these properties.

Every input needs to update one attribute of the user, so we could write a small helper function:

const updateUserField = <K extends keyof User>(
  key: K,
  value: User[K],
): void => {
  const updatedUser = {
    ...structuredClone(user),
    [key]: value,
  };

  setUser(updatedUser);
};

At first glance, this line can look pretty intimidating:

<K extends keyof User>

But let’s break it down.

keyof User gives us all valid property names of User.

For our example, that is essentially:

"name" | "age" | "active"

By writing:

K extends keyof User

we say that K must be one of those valid keys.

So this is allowed:

updateUserField("name", "Max");
updateUserField("age", 35);
updateUserField("active", true);

But this isn’t:

updateUserField("doesNotExist", true);

TypeScript already knows that "doesNotExist" is not a valid property of User.

But restricting the key is only half of what makes this useful.

The other interesting part is:

value: User[K]

User[K] means:

Give me the type of the property represented by K.

So if K is "name", the value has to be a string.

If K is "age", it has to be a number.

And if K is "active", it has to be a boolean.

That means TypeScript will also catch this:

updateUserField("age", "thirty-five");
// Type error: "age" expects a number

This is where I think generics become really useful.

We are not just making a function reusable. We are preserving a relationship between two types.

The selected key determines which type of value is allowed.

Without that relationship, we might be tempted to write something like:

const updateUserField = (
  key: keyof User,
  value: string | number | boolean,
): void => {
  // ...
};

This looks simpler, but we’ve lost an important piece of information.

TypeScript now knows that key has to be a valid user property and that value has to be one of our possible value types.

But it no longer knows that the two have to match.

So something like this could suddenly be accepted:

updateUserField("age", "definitely not a number");

And that’s exactly the kind of problem our generic version prevents.

As a nice bonus, this also gives us really useful autocomplete in the IDE.

When not to use generics

Generics are useful, but using them is not a goal by itself.

If adding a generic makes your code harder to understand without expressing a useful relationship between types, the simpler solution is probably the better one.

I especially like generics when they allow TypeScript to carry information from one part of an API to another — just like the relationship between key and value in the example above.

You don’t get extra points for making a type definition look complicated.

Where to go from here?

I really hope you can see why I think generics are awesome!

There is much more you can do with them than what we’ve covered here, but you don’t need to understand every advanced TypeScript feature to start benefiting from generics.

You are now well equipped to dive deeper into the world of generics either through the official TypeScript docs or simply by playing around with them in your own code.

And if something like this appears in front of you:

<K extends keyof T>

maybe it won’t look quite as scary anymore.

Either way, I’m sure you’ll get the hang of it quickly.

Thanks, and have fun using generics!

RELATED SIGNALS