DevToolBoxGRATIS
Blogg

TypeScript Generics förklarade: Praktisk guide med exempel

14 min läsningby DevToolBox

TypeScript generics are one of the most powerful features of the type system. They allow you to write flexible, reusable, and type-safe code by parameterizing types. Instead of duplicating functions or classes for every data type, generics let you write once and use with any type while maintaining full type safety. This guide covers everything from basics to advanced patterns.

Your First Generic Function

The classic starting point is the identity function — a function that returns exactly what it receives. Without generics, you would either lose type information by using any, or you would have to write separate functions for each type.

With generics, you define a type parameter (conventionally named T) that acts as a placeholder. When you call the function, TypeScript infers the actual type automatically.

// Without generics — loses type info
function identityAny(value: any): any {
  return value;
}
const result1 = identityAny("hello"); // type: any (not string!)

// With generics — preserves type info
function identity<T>(value: T): T {
  return value;
}
const result2 = identity("hello");    // type: string ✓
const result3 = identity(42);         // type: number ✓
const result4 = identity(true);       // type: boolean ✓

// Explicit type argument (rarely needed)
const result5 = identity<string>("hello"); // type: string

Generic Functions

Generic functions can accept multiple type parameters and even have default types. This makes them incredibly versatile for building utility functions.

// Multiple type parameters
function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}
const p = pair("hello", 42); // type: [string, number]

// Swap function with two type params
function swap<A, B>(tuple: [A, B]): [B, A] {
  return [tuple[1], tuple[0]];
}
const swapped = swap(["hello", 42]); // type: [number, string]

// Default type parameters
function createArray<T = string>(length: number, value: T): T[] {
  return Array(length).fill(value);
}
const strings = createArray(3, "x");   // T inferred as string
const numbers = createArray(3, 0);     // T inferred as number
const defaults = createArray<>(3, ""); // T defaults to string

Default type parameters work just like default function parameters — if the caller does not specify a type, the default is used.

Generic Interfaces and Types

Generics are not limited to functions. You can parameterize interfaces and type aliases to create flexible data structures that work with any type.

// Generic interface
interface Box<T> {
  value: T;
  label: string;
}

const stringBox: Box<string> = { value: "hello", label: "greeting" };
const numberBox: Box<number> = { value: 42, label: "answer" };

// Generic interface with multiple params
interface KeyValuePair<K, V> {
  key: K;
  value: V;
}

// Generic type alias
type Result<T, E = Error> =
  | { success: true; data: T }
  | { success: false; error: E };

const ok: Result<string> = { success: true, data: "hello" };
const fail: Result<string> = { success: false, error: new Error("oops") };

// Generic type for callbacks
type AsyncCallback<T> = (error: Error | null, data: T | null) => void;

// Generic type for nullable values
type Nullable<T> = T | null | undefined;

Generic interfaces are especially useful for defining shapes of API responses, collections, and data containers.

Generic Classes

Classes can also be generic. This is useful for building data structures like stacks, queues, linked lists, or any container that should work with multiple types.

// Generic Stack class
class Stack<T> {
  private items: T[] = [];

  push(item: T): void {
    this.items.push(item);
  }

  pop(): T | undefined {
    return this.items.pop();
  }

  peek(): T | undefined {
    return this.items[this.items.length - 1];
  }

  get size(): number {
    return this.items.length;
  }
}

const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
numberStack.pop(); // type: number | undefined

const stringStack = new Stack<string>();
stringStack.push("hello");
stringStack.peek(); // type: string | undefined

// Generic class with multiple type params
class HashMap<K, V> {
  private map = new Map<K, V>();

  set(key: K, value: V): void {
    this.map.set(key, value);
  }

  get(key: K): V | undefined {
    return this.map.get(key);
  }

  has(key: K): boolean {
    return this.map.has(key);
  }
}

The type parameter is available throughout the class — in properties, method parameters, and return types.

Generic Constraints with extends

Unconstrained generics accept any type, but sometimes you need to narrow down what types are allowed. The extends keyword lets you add constraints.

Constraining to Object Keys with keyof

A very common pattern is constraining a type parameter to be a valid key of another type. This ensures type-safe property access.

// getProperty with keyof constraint
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: "Alice", age: 30, email: "alice@example.com" };

getProperty(user, "name");  // type: string ✓
getProperty(user, "age");   // type: number ✓
// getProperty(user, "foo"); // Error: "foo" is not assignable to "name" | "age" | "email"

// Pick specific keys from an object
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
  const result = {} as Pick<T, K>;
  keys.forEach(key => { result[key] = obj[key]; });
  return result;
}

const subset = pick(user, ["name", "email"]);
// type: { name: string; email: string }

Constraining to Specific Shapes

You can require that a type parameter has certain properties by extending an interface or object type.

// Constraint to objects with a length property
interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(value: T): T {
  console.log(`Length: ${value.length}`);
  return value;
}

logLength("hello");     // OK — string has length
logLength([1, 2, 3]);   // OK — array has length
logLength({ length: 5, name: "test" }); // OK — has length
// logLength(42);       // Error — number has no length property

// Constraint to objects with an id
interface HasId {
  id: number | string;
}

function findById<T extends HasId>(items: T[], id: T["id"]): T | undefined {
  return items.find(item => item.id === id);
}

const users = [
  { id: 1, name: "Alice" },
  { id: 2, name: "Bob" },
];

const found = findById(users, 1);
// type: { id: number; name: string } | undefined

Conditional Types

Conditional types let you choose between two types based on a condition, using the syntax T extends U ? X : Y. They are the foundation of many advanced type-level computations.

// Basic conditional type
type IsString<T> = T extends string ? true : false;

type A = IsString<string>;  // true
type B = IsString<number>;  // false
type C = IsString<"hello">; // true

// Flatten arrays, leave other types alone
type Flatten<T> = T extends Array<infer Item> ? Item : T;

type D = Flatten<string[]>;    // string
type E = Flatten<number[][]>;  // number[]
type F = Flatten<string>;      // string (not an array, returned as-is)

// Extract return type (simplified ReturnType)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type G = MyReturnType<() => string>;           // string
type H = MyReturnType<(x: number) => boolean>; // boolean

// Extract promise value
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type I = UnwrapPromise<Promise<string>>; // string
type J = UnwrapPromise<number>;          // number

Conditional types become especially powerful when combined with infer, which lets you extract types from within other types.

Mapped Types

Mapped types let you transform the properties of an existing type to create a new type. They iterate over keys using [K in keyof T] syntax.

// Make all properties optional (like Partial<T>)
type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

// Make all properties required (like Required<T>)
type MyRequired<T> = {
  [K in keyof T]-?: T[K];
};

// Make all properties readonly (like Readonly<T>)
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

// Make all properties nullable
type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

// Transform property types
type Stringify<T> = {
  [K in keyof T]: string;
};

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

type StringifiedUser = Stringify<User>;
// { name: string; age: string; active: string }

// Mapped type with conditional
type OptionalNullable<T> = {
  [K in keyof T]: undefined extends T[K] ? T[K] | null : T[K];
};

You can add or remove modifiers like readonly and ? (optional) in mapped types.

Built-in Utility Types

TypeScript ships with many utility types built using generics. Here are the most commonly used ones:

Utility TypeDescriptionExample
Partial<T>Makes all properties optionalPartial<User>
Required<T>Makes all properties requiredRequired<Partial<User>>
Pick<T, K>Picks specified propertiesPick<User, "name" | "email">
Omit<T, K>Omits specified propertiesOmit<User, "password">
Record<K, V>Creates a key-value type mappingRecord<string, number>
Readonly<T>Makes all properties readonlyReadonly<Config>
ReturnType<T>Extracts function return typeReturnType<typeof fn>
Parameters<T>Extracts function parameter types tupleParameters<typeof fn>
// Utility types in action
interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  createdAt: Date;
}

// For update forms — all fields optional
type UpdateUser = Partial<User>;

// Public user — omit password
type PublicUser = Omit<User, "password">;

// User creation — omit auto-generated fields
type CreateUser = Omit<User, "id" | "createdAt">;

// Lookup table
type UserLookup = Record<number, User>;

// Immutable user from API
type FrozenUser = Readonly<User>;

// Extract function types
function fetchUser(id: number): Promise<User> { /* ... */ }
type FetchReturn = ReturnType<typeof fetchUser>;     // Promise<User>
type FetchParams = Parameters<typeof fetchUser>;     // [number]

Real-World Patterns

Generic API Response Wrapper

One of the most common uses of generics is wrapping API responses in a consistent structure. This ensures every endpoint returns data in the same format while preserving the specific data type.

// Generic API response wrapper
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
  timestamp: string;
}

interface PaginatedResponse<T> extends ApiResponse<T[]> {
  pagination: {
    page: number;
    pageSize: number;
    totalItems: number;
    totalPages: number;
  };
}

// Usage with specific types
interface User {
  id: number;
  name: string;
  email: string;
}

interface Product {
  id: number;
  title: string;
  price: number;
}

async function fetchApi<T>(url: string): Promise<ApiResponse<T>> {
  const res = await fetch(url);
  return res.json();
}

// TypeScript knows the exact data type
const userRes = await fetchApi<User>("/api/users/1");
console.log(userRes.data.name); // type: string ✓

const productsRes = await fetchApi<Product[]>("/api/products");
console.log(productsRes.data[0].price); // type: number ✓

Generic React Component Props

Generics let you build React components that work with different data types while maintaining type safety on props.

// Generic list component props
interface ListProps<T> {
  items: T[];
  renderItem: (item: T, index: number) => React.ReactNode;
  keyExtractor: (item: T) => string | number;
  onItemClick?: (item: T) => void;
  emptyMessage?: string;
}

function GenericList<T>({
  items,
  renderItem,
  keyExtractor,
  onItemClick,
  emptyMessage = "No items",
}: ListProps<T>) {
  if (items.length === 0) return <p>{emptyMessage}</p>;
  return (
    <ul>
      {items.map((item, index) => (
        <li
          key={keyExtractor(item)}
          onClick={() => onItemClick?.(item)}
        >
          {renderItem(item, index)}
        </li>
      ))}
    </ul>
  );
}

// Usage — TypeScript infers T from items
<GenericList
  items={users}
  keyExtractor={(user) => user.id}
  renderItem={(user) => <span>{user.name}</span>}
  onItemClick={(user) => console.log(user.email)}
/>

Generic Repository Pattern

The repository pattern abstracts data access behind a generic interface. This allows you to swap data sources without changing business logic.

// Generic repository interface
interface Entity {
  id: number | string;
}

interface Repository<T extends Entity> {
  findAll(): Promise<T[]>;
  findById(id: T["id"]): Promise<T | null>;
  create(data: Omit<T, "id">): Promise<T>;
  update(id: T["id"], data: Partial<T>): Promise<T>;
  delete(id: T["id"]): Promise<boolean>;
}

// Concrete implementation
class ApiRepository<T extends Entity> implements Repository<T> {
  constructor(private baseUrl: string) {}

  async findAll(): Promise<T[]> {
    const res = await fetch(this.baseUrl);
    return res.json();
  }

  async findById(id: T["id"]): Promise<T | null> {
    const res = await fetch(`${this.baseUrl}/${id}`);
    if (!res.ok) return null;
    return res.json();
  }

  async create(data: Omit<T, "id">): Promise<T> {
    const res = await fetch(this.baseUrl, {
      method: "POST",
      body: JSON.stringify(data),
    });
    return res.json();
  }

  async update(id: T["id"], data: Partial<T>): Promise<T> {
    const res = await fetch(`${this.baseUrl}/${id}`, {
      method: "PATCH",
      body: JSON.stringify(data),
    });
    return res.json();
  }

  async delete(id: T["id"]): Promise<boolean> {
    const res = await fetch(`${this.baseUrl}/${id}`, { method: "DELETE" });
    return res.ok;
  }
}

// Usage
interface User extends Entity { id: number; name: string; email: string; }
const userRepo = new ApiRepository<User>("/api/users");
const users = await userRepo.findAll(); // type: User[]

Generic Event Emitter

A type-safe event emitter ensures that event names and their payloads are correctly typed. No more string-based events with unknown data.

// Type-safe event emitter
type EventMap = Record<string, any>;
type EventCallback<T> = (payload: T) => void;

class TypedEmitter<Events extends EventMap> {
  private listeners: {
    [K in keyof Events]?: EventCallback<Events[K]>[];
  } = {};

  on<K extends keyof Events>(event: K, callback: EventCallback<Events[K]>): void {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event]!.push(callback);
  }

  off<K extends keyof Events>(event: K, callback: EventCallback<Events[K]>): void {
    const cbs = this.listeners[event];
    if (cbs) {
      this.listeners[event] = cbs.filter(cb => cb !== callback);
    }
  }

  emit<K extends keyof Events>(event: K, payload: Events[K]): void {
    const cbs = this.listeners[event];
    if (cbs) {
      cbs.forEach(cb => cb(payload));
    }
  }
}

// Define your event types
interface AppEvents {
  "user:login": { userId: string; timestamp: number };
  "user:logout": { userId: string };
  "item:added": { itemId: string; quantity: number };
  "error": { message: string; code: number };
}

const emitter = new TypedEmitter<AppEvents>();

// Fully typed — payload type is inferred
emitter.on("user:login", (payload) => {
  console.log(payload.userId);    // type: string ✓
  console.log(payload.timestamp); // type: number ✓
});

// Error: "unknown:event" is not assignable to keyof AppEvents
// emitter.on("unknown:event", () => {});

Common Mistakes

Here are the most frequent mistakes developers make with generics, along with corrections:

MistakeProblemCorrection
Using any instead of genericsLoses all type informationUse a type parameter T instead of any
Over-constraining type paramsMakes the generic unusable for valid typesOnly constrain to what you actually need
Unnecessary type parametersAdding T that is used only once adds complexityIf T appears only in return type, just use the concrete type
Not using constraints when accessing propertiesTypeScript error: Property does not exist on type TAdd extends constraint: T extends { prop: type }
Confusing generic defaults with constraintsDefault type does not restrict — callers can still pass other typesUse extends for restriction, = for default
Not inferring when possibleExplicitly passing types when TS can infer themLet TypeScript infer types from arguments when possible
Using T[] when you mean readonly T[]Allows mutation of arrays that should be read-onlyUse readonly T[] for immutable data from APIs
// ❌ Mistake: Using any
function badParse(json: string): any {
  return JSON.parse(json);
}
const data = badParse('{"name":"Alice"}');
data.whatever; // No error — but might crash at runtime!

// ✓ Correction: Using generics with validation
function safeParse<T>(json: string, validator: (data: unknown) => data is T): T | null {
  try {
    const parsed = JSON.parse(json);
    return validator(parsed) ? parsed : null;
  } catch {
    return null;
  }
}

// ❌ Mistake: Unnecessary type parameter
function badLength<T>(arr: T[]): number {
  return arr.length; // T is never meaningfully used
}

// ✓ Correction: Just use the concrete type
function goodLength(arr: unknown[]): number {
  return arr.length;
}

// ❌ Mistake: Missing constraint
function badGetName<T>(obj: T): string {
  return obj.name; // Error: Property 'name' does not exist on type 'T'
}

// ✓ Correction: Add constraint
function goodGetName<T extends { name: string }>(obj: T): string {
  return obj.name; // OK ✓
}

Frequently Asked Questions

What is the difference between generics and the any type?

Generics preserve type information throughout the function or class, while any discards it. With generics, if you pass a string, TypeScript knows the return type is also string. With any, all type information is lost.

When should I use generics vs function overloads?

Use generics when the logic is the same for all types and you just need to preserve type relationships. Use overloads when the implementation differs based on input types, or when you need to map specific input types to specific output types.

Can I use generics with arrow functions in TSX files?

Yes, but you need to add a trailing comma to avoid JSX ambiguity: const fn = <T,>(arg: T): T => arg. The comma tells the parser this is a generic, not a JSX tag.

How many type parameters should a generic have?

As few as possible. One or two is common. Three is sometimes necessary. If you need more than three, consider refactoring — your abstraction may be too complex. Each type parameter should serve a clear purpose.

Do generics affect runtime performance?

No. Generics are purely a compile-time feature. They are completely erased during compilation and produce no runtime overhead. The generated JavaScript has no trace of generic type parameters.

What is the naming convention for type parameters?

Single uppercase letters are conventional: T for type, K for key, V for value, E for element, R for return type. For complex generics with multiple parameters, descriptive names like TData, TError are acceptable and improve readability.

𝕏 Twitterin LinkedIn
Var detta hjälpsamt?

Håll dig uppdaterad

Få veckovisa dev-tips och nya verktyg.

Ingen spam. Avsluta när som helst.

Try These Related Tools

TSJSON to TypeScriptJSTypeScript to JavaScriptGTGraphQL to TypeScript

Related Articles

JSON till TypeScript: Komplett guide med exempel

Lär dig konvertera JSON-data till TypeScript-interfaces automatiskt. Täcker nästlade objekt, arrayer, valfria fält och bästa praxis.

TypeScript vs JavaScript: När och hur man konverterar

Praktisk guide om när man ska konvertera TypeScript till JavaScript och vice versa. Migreringsstrategier, verktyg, påverkan på bundelstorlek och teamöverväganden.

JSON to Zod Schema: Type-Safe Runtime Validation in TypeScript

Learn how to convert JSON to Zod schemas for type-safe runtime validation in TypeScript. Covers basic types, objects, arrays, unions, z.infer, and comparison with JSON Schema.

GraphQL to TypeScript: Code Generation & Type-Safe Development Guide

Generate TypeScript types from GraphQL schemas automatically. Covers manual conversion, graphql-codegen setup, React Query/Apollo integration, and best practices.