Skip to content

TypeScript Generics Basics — Complete Guide

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about TypeScript Generics Basics. We cover key concepts, practical examples, and best practices to help you master this topic.

TypeScript generics let you write functions, classes, and types that work with any data type while preserving strict Type Checking — like a reusable machine that adapts its interface to whatever you feed it.

What You'll Learn

  • Generic functions and type parameters
  • Type parameter naming conventions
  • Constraints with extends
  • Generic interfaces and type aliases
  • Default type parameters

Why It Matters

Without generics, you'd write the same function multiple times for different types (a numberIdentity, a stringIdentity, etc.) or use any and lose type safety. Generics give you the best of both — one function, full type safety, zero duplication.

Real-World Use

React's useState<T>() hook is a generic function — you provide the type, and it returns a properly typed state value and setter. Without generics, every state value would be any. Durga Antivirus Pro uses a generic ApiClient<T> class to type API responses — one class handles User, Scan, Threat, and Report resources with full type safety.

Learning Path

flowchart LR
  A[Type Assertions] --> B[Generics Basics]
  B --> C[Advanced Generics]
  B --> D[You Are Here]
  C --> E[Conditional Types]
  E --> F[Mapped Types]

The Problem Generics Solve

// Without generics: one function per type
function identityNumber(arg: number): number { return arg; }
function identityString(arg: string): string { return arg; }

// With any: loses type safety
function identityAny(arg: any): any { return arg; }
const result = identityAny("hello"); // result type is 'any'

Generic Functions

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

const num = identity(42);    // type: number
const str = identity("hi");  // type: string
const bool = identity(true); // type: boolean

The <T> is a type parameter — a placeholder for the actual type that gets filled in when you call the function. TypeScript infers it from the argument.

Explicit Type Annotation

const num = identity<number>(42);
const str = identity<string>("hello");

Usually you don't need the explicit annotation — inference works. Use it when inference fails or for documentation.

Naming Conventions

Parameter Convention When
<T> Type General purpose
<K> Key Object keys
<V> Value Object values
<E> Element Array elements
<R> Return Return type

Working with Generic Arrays

function firstElement<T>(arr: T[]): T | undefined {
  return arr[0];
}

const first = firstElement([1, 2, 3]);    // type: number | undefined
const str = firstElement(["a", "b", "c"]); // type: string | undefined

console.log(firstElement([10, 20, 30])); // 10
console.log(firstElement([]));            // undefined

Constraints with extends

Sometimes you need to restrict what types a generic can accept:

interface HasLength {
  length: number;
}

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

logLength("hello");      // 5 — string has length
logLength([1, 2, 3]);    // 3 — array has length
// logLength(42);        // Error: number doesn't have length

Think of constraints as saying: "T must be a type that satisfies this interface." This gives you access to properties of the constrained type inside the function.

Constraining to Keys

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

const user = { name: "Alice", age: 30, email: "a@b.com" };
const name = getProperty(user, "name"); // type: string
const age = getProperty(user, "age");   // type: number
// getProperty(user, "ssn"); // Error: "ssn" is not in keyof

Generic Interfaces

interface Repository<T> {
  getById(id: string): T | undefined;
  getAll(): T[];
  create(item: T): void;
  update(id: string, item: Partial<T>): void;
  delete(id: string): void;
}

class UserRepository implements Repository<User> {
  private users: User[] = [];

  getById(id: string): User | undefined {
    return this.users.find(u => u.id === id);
  }

  getAll(): User[] { return this.users; }
  create(item: User): void { this.users.push(item); }
  update(id: string, item: Partial<User>): void {
    const index = this.users.findIndex(u => u.id === id);
    if (index >= 0) this.users[index] = { ...this.users[index], ...item };
  }
  delete(id: string): void {
    this.users = this.users.filter(u => u.id !== id);
  }
}

Generic Type Aliases

type Result<T> = { success: true; data: T } | { success: false; error: string };

function fetchData<T>(url: string): Result<T> {
  try {
    const data = JSON.parse(localStorage.getItem(url) || "null") as T;
    return { success: true, data };
  } catch {
    return { success: false, error: "Failed to fetch" };
  }
}

const result = fetchData<{ name: string }>("/api/user");
if (result.success) {
  console.log(result.data.name); // Narrowed
}

Default Type Parameters

function createArray<T = string>(length: number, value: T): T[] {
  return Array(length).fill(value);
}

const arr1 = createArray(3, "hello"); // type: string[]
const arr2 = createArray<number>(3, 42); // type: number[]

Default types are used when TypeScript cannot infer the type parameter.

Multiple Type Parameters

function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}

const p1 = pair("Alice", 30); // type: [string, number]
const p2 = pair(1, true);     // type: [number, boolean]

Common Mistakes

1. Not Using Constraints When You Need Them

function getLength<T>(arg: T): number {
  // return arg.length; // Error: Property 'length' doesn't exist on T
}

function getLength<T extends { length: number }>(arg: T): number {
  return arg.length; // OK
}

2. Overusing Generics

// Overkill
function identity<T>(arg: T): T { return arg; }

// Just use the specific type
function identity(arg: string): string { return arg; }

Add generics only when you genuinely need the type to vary.

3. Forgetting to Type Generic Constraints Properly

// Too loose
function process<T>(items: T[]) { }

// More specific
function process<T extends { id: string }>(items: T[]) { }

4. Not Providing Type Arguments When Inference Fails

Sometimes TypeScript cannot infer the type parameter. Provide it explicitly:

const data = await api.get<ScanResult>("/scans/123");

5. Using any Instead of a Generic

// Bad
function clone(obj: any): any { return { ...obj }; }

// Good
function clone<T>(obj: T): T { return { ...obj }; }

Practice Questions

  1. What is the purpose of the <T> syntax in a generic function? It introduces a type parameter — a placeholder for a type that will be determined at call time.

  2. How do you constrain a generic parameter to only accept types with certain properties? Use extends: function fn<T extends { length: number }>(arg: T)

  3. What is a default type parameter and when is it used? A fallback type used when TypeScript cannot infer the type argument: function fn<T = string>(arg: T)

  4. Can a generic function have multiple type parameters? Yes: function pair<T, U>(a: T, b: U): [T, U]

Challenge: Write a generic map function that takes an array of type T and a transform function (T) => U, and returns an array of type U. Use it with a string array to produce a number array (lengths).

FAQ

Do generics exist at runtime?

No. Generics are erased during compilation. The JavaScript output has no type parameters.

What is the difference between `T` and `any`?

T preserves the type relationship between input and output. any loses that relationship entirely.

Can I use a generic with arrow functions?

Yes: const identity = <T>(arg: T): T => arg; In TSX files, add a trailing comma: <T,> to avoid confusion with JSX.

What are the naming conventions for type parameters?

Single letters are conventional: T, K, V, E. Use descriptive names for complex cases: TypeParam, KeyType.

Can interfaces use generics?

Yes: interface Container<T> { value: T; }

Mini Project: Generic Cache

// src/cache.ts

interface CacheEntry<T> {
  value: T;
  expiresAt: number;
}

class Cache<T> {
  private store = new Map<string, CacheEntry<T>>();
  private defaultTTL: number;

  constructor(defaultTTLMs: number = 60_000) {
    this.defaultTTL = defaultTTLMs;
  }

  set(key: string, value: T, ttlMs?: number): void {
    this.store.set(key, {
      value,
      expiresAt: Date.now() + (ttlMs ?? this.defaultTTL),
    });
  }

  get(key: string): T | undefined {
    const entry = this.store.get(key);
    if (!entry) return undefined;
    if (Date.now() > entry.expiresAt) {
      this.store.delete(key);
      return undefined;
    }
    return entry.value;
  }

  has(key: string): boolean {
    return this.get(key) !== undefined;
  }

  clear(): void {
    this.store.clear();
  }
}

const scanCache = new Cache<{ id: string; threats: string[] }>(30_000);

scanCache.set("scan-001", { id: "scan-001", threats: ["Trojan"] });
console.log(scanCache.get("scan-001")?.threats); // ["Trojan"]
console.log(scanCache.has("scan-001")); // true

What's Next

Now level up with advanced generic patterns:

Lesson Description
{{< ref "/programming-languages/typescript/08-type-assertions" >}} Review type assertions
{{< ref "/programming-languages/typescript/10-generics-advanced" >}} Generic classes, conditional types, mapped types
{{< ref "/programming-languages/typescript/11-keyof-typeof" >}} Keyof, typeof, indexed access types

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro