AniUI Academy

Generic Interfaces and Classes

Apply generics beyond standalone functions — to interfaces, type aliases, and classes — and read the generic types you already use every day, like Array<T> and Promise<T>.

9 min read

Generics aren't limited to standalone functions — the same idea, "a placeholder type filled in per use," applies to interfaces, type aliases, and classes too. You've actually been using generic types since your first array in this track; this lesson makes that explicit.

Generic interfaces

An interface can declare a type parameter exactly like a function:

interface Box<T> {
  value: T;
}
 
const numberBox: Box<number> = { value: 42 };
const stringBox: Box<string> = { value: "hello" };
 
numberBox.value = "oops";
// Type 'string' is not assignable to type 'number'.

Box<number> and Box<string> are two distinct, fully separate types built from the same template — assigning one where the other is expected is an error, the same as assigning string where number is expected anywhere else.

You already know this: Array<T>

Array in TypeScript's own standard library is a generic interface, and T[] has always been shorthand syntax for Array<T> — the two are identical:

let scores: number[] = [10, 20, 30];
let sameThing: Array<number> = [10, 20, 30]; // exactly the same type

This is worth internalising: nothing about generics is a separate, advanced feature you're opting into. Every array you've written in this track has been a generic type, filled in with whatever element type you gave it.

Generic type aliases

The same works for type:

type Result<T> = { success: true; data: T } | { success: false; error: string };
 
function parseNumber(input: string): Result<number> {
  const parsed = Number(input);
  return Number.isNaN(parsed)
    ? { success: false, error: "Not a number" }
    : { success: true, data: parsed };
}

This combines two ideas from earlier lessons — a discriminated union, and a generic — into a pattern you'll recognise everywhere once you've seen it once: a reusable "this either succeeded with data, or failed with a reason" shape.

Generic classes

Classes take a type parameter the same way, applied after the class name and usable anywhere inside — constructor parameters, methods, properties:

class Box<T> {
  constructor(public value: T) {}
 
  map<U>(transform: (value: T) => U): Box<U> {
    return new Box(transform(this.value));
  }
}
 
const box = new Box(5);           // T inferred as number
const doubled = box.map((n) => n * 2); // Box<number>
const asString = box.map((n) => n.toString()); // Box<string>

T is inferred from the constructor argument, exactly like a generic function infers from its parameters. map introduces its own additional type parameter, U, for the transformed value's type — a method can add type parameters beyond whatever the class itself declared.

Promise<T>: a generic you already use

Promise is generic over what it eventually resolves to — this is precisely why await gives back a correctly typed value instead of something generic or untyped:

function fetchUser(id: string): Promise<{ name: string }> {
  return fetch(`/api/users/${id}`).then((response) => response.json());
}
 
async function run() {
  const user = await fetchUser("1"); // user: { name: string }
  console.log(user.name);            // fine — TypeScript knows the shape
}

Without the <{ name: string }> type parameter, Promise<any> would leave user untyped after await, silently losing checking on everything downstream — the same problem as any from the previous lesson, just arriving through a promise instead of a bare variable.

Try it yourself

Try it yourself
Loading playground...

What to remember

  • Interfaces, type aliases, and classes can all take type parameters, using exactly the same syntax and reasoning as a generic function.
  • T[], Array<T>, Promise<T>, and Map<K, V> are ordinary generic types from the standard library, not separate built-in features.
  • Applying a generic type with different type arguments (Box<string> vs Box<number>) produces different, incompatible types.
  • A method on a generic class can introduce its own additional type parameters beyond the class's own.

Check yourself

4 questions · pass 3/4 to unlock Utility Types, Part 1: Partial, Required, Readonly, Pick, Omit

up to 50
  1. 1.What does Array<T> actually mean, given that arrays are typically written as string[]?

  2. 2.Given class Box<T> { constructor(public value: T) {} }, what is the type of new Box(5).value?

  3. 3.Why does Promise<T> need a type parameter at all?

  4. 4.A generic interface interface Box<T> { value: T } is used as Box<string> in one place and Box<number> in another. Are these the same type?

4 left to answer