Common Generic Type Mistakes in TypeScript

Avoid the most frequent generic type mistakes in TypeScript. Learn to spot unnecessary type parameters, missing constraints, runtime type checks, and overly complex generics.

5 min read

Generics are a powerful TypeScript feature, but they are also easy to misuse. The most common TypeScript generic mistakes come from reaching for generics when a simpler solution exists, or from misunderstanding what type parameters can and cannot do at runtime. Here are the mistakes to watch for.

Adding a Type Parameter When You Do Not Need One

The most frequent mistake is adding a type parameter to a function that only works with one concrete type. Here is an example where T serves no purpose:

typescripttypescript
function greet<T>(name: string): string {
  return `Hello, ${name}`;
}

T appears in the angle brackets but nowhere in the function signature. The parameter name is always a string, and the return is always a string, so there is no relationship between T and anything else.

The fix is simple: remove the type parameter and write the function with concrete types instead.

The same applies when a union is clearer than a generic:

typescripttypescript
function logValue(value: string | number): void {
  console.log(value);
}

A generic version with a constraint would work, but it adds complexity without benefit. The reader immediately sees which types are accepted with a union.

Use generics when the return type depends on the input type, or when multiple parameters share a type that the caller chooses. If you are not sure whether you need a generic, start with a concrete type or union and only upgrade to generics when the need becomes clear.

Trying to Check a Type Parameter at Runtime

Type parameters are a compile-time concept. They do not exist in the emitted JavaScript:

typescripttypescript
function createArray<T>(value: T, count: number): T[] {
  if (typeof T === "string") {
    return Array(count).fill(value);
  }
  return Array(count).fill(value);
}

This does not even compile, because T is a compile-time placeholder with no runtime value to check. TypeScript rejects the comparison itself:

texttext
'T' only refers to a type, but is being used as a value here.

If you need runtime type information, pass it as a separate argument or use a discriminated union instead of a generic.

This is the same reason you cannot use instanceof with a type parameter. Both typeof and instanceof operate on runtime values, not compile-time types. For runtime validation patterns, see runtime validation in TypeScript.

Forgetting to Add a Constraint When You Need One

Without a constraint, a type parameter can be anything. You cannot access properties that only exist on specific types:

typescripttypescript
function getLength<T>(value: T): number {
  return value.length;
}

This produces a compiler error because not every type has a length property. The fix is to add a constraint that requires the property:

typescripttypescript
function getLength<T extends { length: number }>(value: T): number {
  return value.length;
}

Now TypeScript knows the property exists, and callers must pass a value that satisfies the constraint. For the full explanation, see generic constraints in TypeScript.

Over-Constraining a Type Parameter

The opposite mistake is making the constraint so narrow that only one type can satisfy it:

typescripttypescript
function process<T extends string>(value: T): T {
  return value.toUpperCase();
}

If T can only be string, just write string. A constraint this narrow defeats the purpose of generics. The constraint should represent a meaningful set of types that share a capability, like "has a length property" or "has a name field."

Nesting Generics Too Deeply

Complex nested generics make code hard to read and debug:

typescripttypescript
function transform<T, U extends Record<keyof T, V>, V>(
  input: T,
  mapping: U
): { [K in keyof T]: U[K] } {
  return mapping;
}

Three type parameters with interdependent constraints in one line are hard to parse. Break complex generics into named intermediate types:

typescripttypescript
type MappingFor<T, V> = Record<keyof T, V>;
 
function transform<T, V>(
  input: T,
  mapping: MappingFor<T, V>
): MappingFor<T, V> {
  return mapping;
}

The named type makes the intent clearer, and the function signature is easier to scan. If a generic type expression takes more than a line to read, extract it into a type alias.

Not Providing a Default When One Type Is Dominant

When one type argument is used far more often than others, provide a default:

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

Now callers can write Container without angle brackets for the common case, and still override when needed. Without the default, every use site must supply the type argument even when it is obvious. For more on this, see default generic type parameters.

Ignoring Inference and Writing Explicit Type Arguments Everywhere

TypeScript infers type arguments from the values you pass. Writing them explicitly everywhere adds noise without providing any additional safety:

typescripttypescript
// Unnecessary: TypeScript already knows T is string
const result = identity<string>("hello");
 
// Cleaner: let inference do the work
const same = identity("hello");

Only write type arguments explicitly when inference fails or when you need to override the inferred type. In most cases, letting TypeScript do the work keeps your code cleaner and less repetitive.

Rune AI

Rune AI

Key Insights

  • Do not add a type parameter when a concrete type or union works fine.
  • Only constrain a type parameter when the function body needs specific properties.
  • Never use typeof on a type parameter at runtime, it does not exist there.
  • Do not nest generics deeply when a named intermediate type would be clearer.
  • Break complex generic types into smaller, named pieces for readability.
RunePowered by Rune AI

Frequently Asked Questions

How do I know if my generic is too complex?

If a new team member cannot understand the type signature in under a minute, it is probably too complex. Look for deeply nested conditional types, more than three type parameters, or type parameters that only appear once. Simplify to a concrete type, a union, or multiple smaller generics.

Should I always add a constraint to my generic type parameter?

No. Only add a constraint when the function body needs to access specific properties or methods on the type. An unconstrained parameter is perfectly valid when the function only passes the value through or wraps it in a container.

Can I use a generic just to avoid writing a union?

Technically yes, but it is usually a mistake. If your function body does not relate multiple type parameters or return a type derived from the input, a union parameter is clearer and simpler.

Conclusion

Most generic type mistakes come from using generics when something simpler would work. Before adding a type parameter, ask: does the return type depend on the input type? Do multiple parameters share a type? If not, a concrete type or union is probably clearer. When you do use generics, add constraints only when needed, never check type parameters at runtime, and keep the complexity low enough for teammates to understand quickly.