TypeScript Generics Explained for Beginners: A Simple Guide

TypeScript generics aren't about clever code—they're a placeholder for a type you choose at call time, so passing a string returns a string, not `any`. Here's how they work, and when to skip them.

TypeScript Generics Explained for Beginners: A Simple Guide

You copy a function from one part of your codebase because you need it somewhere else. It works. Two weeks later, you notice it returns any. You don't remember writing any. But there it is, quietly eating your autocomplete and letting a typo slide through to production.

That scenario is why TypeScript generics exist. Not to make your code look clever. To make the compiler actually help you when you reuse code across different types.

I'll admit something: the first time I saw <T> in a function signature, I closed the file and worked on something else. It looked like algebra notation someone smuggled into JavaScript. It took me a genuinely embarrassing amount of time to realize the whole concept boils down to one sentence you'll read in a minute.

Key takeaways

  • A generic is a placeholder for a type — you decide the actual type when you call the function, not when you write it.
  • Generics beat any because they preserve the link between input and output: pass a string, get a string back.
  • The angle-bracket syntax <T> appears on functions, interfaces, classes, and type aliases — same idea every time.
  • Constraints (<T extends Something>) let you say "any type, but it has to have this shape."
  • Beginners overuse generics. If your function only ever handles one type, you don't need them.
  • TypeScript generics borrow heavily from C# and Java, so tutorials in those languages apply reasonably well.

What are TypeScript generics, really?

Think about a function that takes a value and gives it back unchanged. The "identity function." It sounds useless, but it's the cleanest way to see what generics do.

Write it with a fixed type and you've solved nothing:

function identity(value: string): string {
  return value;
}

That works for strings. If you need the same thing for numbers, you either copy the function or you reach for any:

function identity(value: any): any {
  return value;
}

And now you've lost the plot. The compiler no longer knows what came out. Call identity("hello").toFixed(2) and TypeScript shrugs. You get a runtime crash instead of a red squiggle.

How do I understand generics in TypeScript?

Here's the sentence I promised. A generic is a variable that holds a type instead of a value.

That's it. When you write let x = 5, x is a variable holding a number. When you write <T>, T is a variable holding a type — and TypeScript fills in what T is when you actually call the function.

function identity<T>(value: T): T {
  return value;
}

const a = identity("hello");  // T is string
const b = identity(42);       // T is number
const c = identity({ id: 1 }); // T is { id: number }

No any. No copies. One function, three types, full autocomplete in every case. Hover over a in your editor and it says string, not any.

How do generics work?

TypeScript decides what T is through type inference. You pass 42, the compiler sees a number, and T becomes number for that call. You can also be explicit when inference gets confused:

const d = identity<number>(42);

Both styles are valid. I used to write the explicit version everywhere because it felt safer. Now I let inference do the work unless the compiler genuinely can't figure it out — usually when you pass an empty array or an object literal with optional fields.

The important part: this all disappears at runtime. Generics are erased when TypeScript compiles. There's no generic machinery in the JavaScript that ships to the browser. It's pure developer tooling.

Why not just use any and move on?

Because any is a switch that turns the compiler off for that piece of code. And it doesn't stay in one place. any spreads: a function returns any, the variable holding it becomes any, that variable goes into another function, and suddenly half your file has no type checking at all.

Why not just use any and move on?

I once traced a bug for two hours — a property name I'd misspelled in an API response handler. The type was any. TypeScript had known nothing and said nothing. Adding one generic wrapper around the fetch call would have caught it in ten seconds.

Here's the difference in practice:

ApproachReusable across typesType safetyAutocompleteRefactor confidence
Fixed type (string)No — you duplicate the functionFull, but for one type onlyWorksHigh for that type
anyYesNoneNoneYou're on your own
Generic <T>YesFull, per callWorks, derived from the argumentHigh everywhere

The generic column is the only one where you get flexibility and safety at the same time. That's the entire value proposition.

Generics on interfaces, classes, and arrows

The function is where most people meet generics. But the same syntax shows up in four other places, and beginners panic when they see it because the shape changes.

Generics on interfaces, classes, and arrows

A generic interface

You'll build this one constantly. Any time you wrap a value with metadata — API responses, pagination, cache entries — you want the wrapper to describe the payload, not be a mystery box:

interface ApiResponse<T> {
  data: T;
  status: number;
}

const userResponse: ApiResponse<{ name: string }> = {
  data: { name: "Ada" },
  status: 200,
};

Notice you must supply the type when you use it. ApiResponse<{ name: string }> tells the compiler exactly what data holds. Try to access userResponse.data.email and the compiler stops you.

A generic class

class Box<T> {
  constructor(private value: T) {}

  get(): T {
    return this.value;
  }
}

const stringBox = new Box("hello");
const numberBox = new Box(42);

Two instances, two types, one class definition. If you've used Array<string> or Map<string, number>, you've been using generic classes all along — those are built into TypeScript.

A generic arrow function

This one bites people. In a .ts file this is fine:

const identity = <T>(value: T): T => value;

In a .tsx file, the compiler reads <T> as the start of a JSX element and throws a confusing error. The fix is a trailing comma:

const identity = <T,>(value: T): T => value;

Two characters. I spent forty minutes on this the first time it happened, convinced my whole project config was broken.

Two type parameters at once

Generics aren't limited to one. A key-value pair is the classic case, and TypeScript's own Map uses it:

function toPair<K, V>(key: K, value: V): [K, V] {
  return [key, value];
}

Convention matters here: T for a general type, K for a key, V for a value, U when you need a second general type. Nothing enforces it, but deviating makes your code harder to read for everyone else.

Constraints: "any type, as long as it has this"

Here's where a plain <T> runs out of road. Suppose you want to log the length of whatever gets passed in. Writing value.length inside a generic function fails, because T could be a number, and numbers have no length.

Constraints: "any type, as long as it has this"

You fix that with extends:

function logLength<T extends { length: number }>(value: T): T {
  console.log(value.length);
  return value;
}

logLength("hello");   // fine
logLength([1, 2, 3]); // fine
logLength(42);        // error: number has no length

Read extends here as "has at least this shape," not as class inheritance. It's a filter on what T is allowed to be. This single keyword fixes most of the "Property does not exist on type T" errors you'll hit in your first week.

TypeScript also supports defaults, which help when a generic has an obvious common case:

interface Container<T = string> {
  value: T;
}

const c: Container = { value: "default is string" };

When should you actually use generics?

Three signals, and I'd argue you should wait for at least one of them:

  • You're about to write the same function body twice with different type annotations. That's a generic asking to be born.
  • The return type should depend on the argument type — arrays, wrappers, pick-and-omit helpers, anything that passes a value through.
  • You're writing a library or shared utility that other people will call with types you can't predict.

And the counter-signal: if you can name the type concretely, do that. A function that only ever handles User objects should say User. Wrapping it in <T> makes it harder to read and buys nothing. I've reviewed code where every helper was generic "for flexibility," and the effect was that nobody could tell what any of it was for.

Is TypeScript still relevant in 2026?

Yes, and generics are a big reason why. The language's role has only grown — it's the default for new frontend projects, it's standard in most Node backends, and it's increasingly the layer that validates data coming in from APIs and AI services, where you genuinely don't control the shape of what arrives.

What's changed is the pressure to write generics by hand. Modern editors infer far more than they did a few years ago, and AI coding assistants are decent at filling in the boilerplate. But they're also confidently wrong about type parameters surprisingly often — they'll write <T> where a concrete type was correct, or miss a constraint and hand you three cascading errors. Being able to read a generic signature and know whether it's right is now a bigger skill than being able to write one from scratch.

How can you learn the basics of TypeScript?

Skip the video courses that spend ninety minutes on setup. Start with four things, in this order:

  1. Basic types and interfaces. A day, maybe two. Object shapes, optional properties, union types.
  2. Union types and narrowing. string | number, and how typeof and in checks narrow things down inside an if. This is where TypeScript starts feeling useful rather than annoying.
  3. Generic functions. Just the identity function and one array helper. Write them, don't read them. The concept is genuinely small once your fingers have typed <T> a few times.
  4. Constraints and built-in utility types. Partial<T>, Pick<T, K>, Record<K, V>. These are generics someone already wrote for you, and reading their signatures is the best practice available.

The mistake I made early on was reading about generics for a week before writing any. TypeScript is a language you learn by watching the error messages change. Turn on strict mode, break things on purpose, and read what the compiler says. It's more patient with you than any tutorial.

There's one habit worth building immediately: whenever you reach for any, pause and ask whether the function actually needs to work with more than one type. If it does, that's a generic. If it doesn't, name the type and move on. That single question separates code that ages well from code you'll be untangling six months from now.

Naomi Kingsley

Naomi Kingsley

Naomi Kingsley is a machine learning and data visualization expert who specializes in Python and big data analytics. She is known for making complex analytical concepts accessible to a wide range of audiences. Her work focuses on helping organizations turn large, messy datasets into clear, actionable insights.

See all articles →

Related articles