Skip to main content
Cover image for TypeScript Generics
Back to Work Blog
Work

TypeScript Generics

Learn how to use TypeScript Generics to create reusable functions while preserving type safety.

#typescript #generics #programming

Hello! Generics in TypeScript are an absolutely powerful tool. They allow us to create reusable functions that still keep type properties! Instead of having to rely on any, we can tell the compiler that we will interpret the type during compile time with whatever property we are passing in. Rather than talking about it, generics can be best explained through example. Here, we can see a basic function:

function logArgument(value: any): any {
  console.log('The value is: ', value);
  return value;
}

logArgument('cool'); // Once this function runs, we do not know the return type as it is returning "any"!

This function will accept any sort of value, log it out, and return the value. Now, what if we need to know the actual return value type to continue on, but still want to pass in any parameter? In this situation, that is where generics come into play. You can assign generics by appending <T> where T can be any name, usually you will see this written as either <T> or <Type>, but it can be named anything. Here is an example of what our function would look like now using a generic!

function logArgument<Type>(value: Type): Type {
  console.log('The value is: ', value);
  return value;
}

const result1 = logArgument('cool');
// result1 would then be of type "string"
const result2 = logArgument(123);
// result2 would then be of type "number"

Here is an example of using a generic with the shorthand function:

// Note: if you are using a .ts file, this is fine, but in a .tsx file you require a comma after the generic name!
const logArgument = <T>(value: T): T => {
  console.log('The value is: ', value);
  return value;
};

As you can see, it looks fairly similar, just we are using the generic to define the parameter type and the return type. Now when we invoke this function, When we pass the parameter in, we will get the value of whatever the parameter is as the return type!

Generic Constraints

Sometimes, even though we are using generics, we still only want properties that exist on the type passed in, lets say for example you want the property to also include a .count property. You can do this by adding a constraint to the generic type!

interface Count {
  count: number;
}

function giveMeCount<Type extends Count>(value: Type): Type {
  console.log(value.count); // We now know that value will always contain count now!
  return value;
}

giveMeCount({ count: 1, apples: true }); // This will work just fine!

As you can see above, we first need to define the constraint as a type, so we made interface Count and supplied it with the count property. We then use the extends keyword on our generic, constraining it to only allow arguments that have the count property! Passing in anything that doesn’t have the property will result in a type error. It is also good to note here that when you pass in the extra properties, they do not drop off, so you keep all the extra properties beyond .count!

keyof Parameter

Sometimes, you need to access a specific key in a property from a generic type. TypeScript has a property named keyof that helps with this. keyof lets you grab the object keys and not allowing any other key that is not on the original type.

function getEnemyData<Type, Key extends keyof Type>(enemy: Type, key: Key): Type[Key] {
  return enemy[key];
}

let enemy = { hp: 10, isAttacking: false, name: 'slime' };

getEnemyData(enemy, 'hp'); // This will return the enemy key!
getEnemyData(enemy, 'loot'); // This will throw an error, "loot" does not exist!

When to use Generics

Use generics when:

  • You need a reusable function or class that works over multiple types.
  • You want compile-time safety: the return type adapts to the type you pass in.
  • Your logic applies to many shapes of data (e.g. a calculateDamage<T>(attacker: T, defender: T): number used for both Player and Enemy).
  • You’re writing a helper that accesses arbitrary keys (e.g. getProperty<T, K extends keyof T>(obj: T, key: K): T[K]).

Avoid generics when:

  • Your function or class is tightly bound to a single type (no reuse in sight).
  • The added complexity reduces readability more than it increases flexibility.
  • You don’t need a variable return type—plain overloads or union types might suffice.