TypeScriptGeneric Interfaces & Types

Generic Interfaces & Types in TypeScript

Generic interfaces and type aliases let you define reusable data shapes that are parameterized by one or more types. Instead of duplicating nearly identical interfaces for every domain entity, you write one generic contract that the compiler specialises at each use site.

Generic Interfaces

Attach a type parameter list directly after the interface name to make it generic. Every property or method signature can reference those parameters.

TS
// A generic "box" that can hold any value
interface Box<T> {
  value: T;
  label: string;
}

const numberBox: Box<number>  = { value: 42,      label: 'age' };
const stringBox: Box<string>  = { value: 'hello', label: 'greeting' };
const boolBox:   Box<boolean> = { value: true,    label: 'active' };

// TypeScript catches type mismatches
// const bad: Box<number> = { value: 'oops', label: 'x' }; // ❌
Generic Type Aliases

Type aliases can be generic too, and they support features interfaces cannot — like union types and conditional types.

TS
// Generic type alias
type Pair<A, B> = {
  first: A;
  second: B;
};

type Nullable<T> = T | null;
type Maybe<T>    = T | null | undefined;

const p: Pair<string, number> = { first: 'Alice', second: 30 };
const n: Nullable<string>     = null; // or a string
const m: Maybe<boolean>       = undefined; // null, undefined, or boolean
Note
Prefer interface when describing object shapes that others may extend. Prefer type for unions, intersections, mapped types, and conditional types.
Generic API Response Pattern

One of the most useful applications of generic interfaces is modelling API responses. Instead of writing a separate response type for every endpoint, you define one generic wrapper.

TS
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
  timestamp: string;
}

interface User {
  id: number;
  name: string;
  email: string;
}

interface Post {
  id: number;
  title: string;
  body: string;
}

// Specialise for each endpoint
type UserResponse = ApiResponse<User>;
type PostResponse = ApiResponse<Post>;
type UserListResponse = ApiResponse<User[]>;

// Usage
const response: UserResponse = {
  data: { id: 1, name: 'Alice', email: 'alice@example.com' },
  status: 200,
  message: 'OK',
  timestamp: new Date().toISOString(),
};

console.log(response.data.name); // 'Alice' — fully typed
Generic Repository / Service Interfaces

Generic interfaces are ideal for defining contracts for data-access layers (repositories, services, DAOs) where the same operations apply to any entity.

TS
// Generic CRUD repository contract
interface Repository<TEntity, TId = number> {
  findById(id: TId): Promise<TEntity | null>;
  findAll(): Promise<TEntity[]>;
  create(entity: Omit<TEntity, 'id'>): Promise<TEntity>;
  update(id: TId, partial: Partial<TEntity>): Promise<TEntity>;
  delete(id: TId): Promise<void>;
}

// Implement once per entity
class UserRepository implements Repository<User> {
  async findById(id: number): Promise<User | null> {
    // fetch from DB
    return null;
  }
  async findAll(): Promise<User[]> { return []; }
  async create(entity: Omit<User, 'id'>): Promise<User> {
    return { id: Math.random(), ...entity };
  }
  async update(id: number, partial: Partial<User>): Promise<User> {
    return { id, name: '', email: '', ...partial };
  }
  async delete(_id: number): Promise<void> {}
}

class PostRepository implements Repository<Post> {
  async findById(id: number): Promise<Post | null> { return null; }
  async findAll(): Promise<Post[]> { return []; }
  async create(entity: Omit<Post, 'id'>): Promise<Post> {
    return { id: Math.random(), ...entity };
  }
  async update(id: number, partial: Partial<Post>): Promise<Post> {
    return { id, title: '', body: '', ...partial };
  }
  async delete(_id: number): Promise<void> {}
}
Generic Interfaces with Multiple Type Parameters

TS
// A generic key-value store
interface Store<TKey, TValue> {
  get(key: TKey): TValue | undefined;
  set(key: TKey, value: TValue): void;
  has(key: TKey): boolean;
  delete(key: TKey): boolean;
  keys(): Iterable<TKey>;
  values(): Iterable<TValue>;
}

// A transformer pipeline step
interface Transform<TInput, TOutput> {
  transform(input: TInput): TOutput;
  canTransform(input: unknown): input is TInput;
}

// Paginated list result
interface Page<T> {
  items: T[];
  total: number;
  page: number;
  pageSize: number;
  hasNext: boolean;
  hasPrev: boolean;
}

const userPage: Page<User> = {
  items: [],
  total: 0,
  page: 1,
  pageSize: 20,
  hasNext: false,
  hasPrev: false,
}
Extending Generic Interfaces

Generic interfaces can extend other interfaces — both generic and non-generic. The extending interface can forward, rebind, or add type parameters.

TS
interface Identifiable {
  id: number;
}

// Add an id to any generic entity
interface Entity<T> extends Identifiable {
  data: T;
  createdAt: Date;
}

// Further extend: add audit fields
interface AuditedEntity<T> extends Entity<T> {
  createdBy: string;
  updatedAt: Date;
  updatedBy: string;
}

const auditedUser: AuditedEntity<User> = {
  id: 1,
  data: { id: 1, name: 'Alice', email: 'alice@example.com' },
  createdAt: new Date(),
  createdBy: 'system',
  updatedAt: new Date(),
  updatedBy: 'admin',
};
Generic Function Types in Interfaces

Methods and properties of interface members can themselves be generic, independent of the interface's own type parameters.

TS
// Interface with a generic method (T is the interface param, U is local to the method)
interface Mapper<T> {
  map<U>(fn: (item: T) => U): U[];
  flatMap<U>(fn: (item: T) => U[]): U[];
  filter(predicate: (item: T) => boolean): T[];
}

// Event emitter with type-safe payloads
interface EventMap {
  click: { x: number; y: number };
  keydown: { key: string; code: string };
  resize: { width: number; height: number };
}

interface TypedEventEmitter<TEvents extends Record<string, unknown>> {
  on<K extends keyof TEvents>(event: K, handler: (payload: TEvents[K]) => void): void;
  off<K extends keyof TEvents>(event: K, handler: (payload: TEvents[K]) => void): void;
  emit<K extends keyof TEvents>(event: K, payload: TEvents[K]): void;
}

declare const emitter: TypedEventEmitter<EventMap>;

// Fully type-safe event handling
emitter.on('click', ({ x, y }) => {
  console.log(`Clicked at ${x}, ${y}`);
});

// emitter.emit('click', { key: 'a' }); // ❌ Error: wrong payload shape
Tip
The pattern above (TypedEventEmitter) is used by libraries like socket.io and typed-emitter. Understanding it helps you read and write professional TypeScript libraries.
Generic Type Aliases — Advanced Patterns

TS
// Recursive generic types
type Tree<T> = {
  value: T;
  children: Tree<T>[];
};

const tree: Tree<number> = {
  value: 1,
  children: [
    { value: 2, children: [] },
    { value: 3, children: [{ value: 4, children: [] }] },
  ],
};

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

function divide(a: number, b: number): Result<number, string> {
  if (b === 0) return { success: false, error: 'Division by zero' };
  return { success: true, data: a / b };
}

const result = divide(10, 2);
if (result.success) {
  console.log(result.data); // number — narrowed by discriminant
} else {
  console.error(result.error); // string
}
Interface vs Type Alias — Generic Context

Feature

interface

type alias

Generic parameters

Yes

Yes

Declaration merging

Yes

No

Extends other types

Yes (extends)

Yes (& intersection)

Unions

No

Yes

Conditional types

No

Yes

Recursive definitions

Yes (direct)

Yes (with workarounds)

Mapped types

No

Yes

Error messages

Shows interface name

Expands inline

Generic Utility Type Building Blocks

TS
// Building blocks for domain modelling
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
type Required<T, K extends keyof T> = Omit<T, K> & { [P in K]-?: T[P] };

// Make specific fields nullable
type Nullable<T, K extends keyof T> = Omit<T, K> & {
  [P in K]: T[P] | null;
};

// Form state wrapper
interface FormField<T> {
  value: T;
  error: string | null;
  touched: boolean;
  dirty: boolean;
}

type FormState<T> = {
  [K in keyof T]: FormField<T[K]>;
};

type UserForm = FormState<User>;
// { id: FormField<number>; name: FormField<string>; email: FormField<string> }
Generic Interfaces for State Management

State management libraries and patterns heavily rely on generic interfaces. Here is a simplified but realistic Redux-like store interface.

TS
// Generic action type
interface Action<TType extends string = string, TPayload = undefined> {
  type: TType;
  payload: TPayload;
}

// Generic reducer
type Reducer<TState, TAction extends Action> = (
  state: TState,
  action: TAction
) => TState;

// Generic store
interface Store<TState, TAction extends Action> {
  getState(): TState;
  dispatch(action: TAction): void;
  subscribe(listener: (state: TState) => void): () => void;
}

// Concrete usage
interface CounterState {
  count: number;
  lastUpdated: Date | null;
}

type IncrementAction = Action<'INCREMENT', { by: number }>;
type ResetAction     = Action<'RESET',     undefined>;
type CounterAction   = IncrementAction | ResetAction;

const counterReducer: Reducer<CounterState, CounterAction> = (state, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + action.payload.by, lastUpdated: new Date() };
    case 'RESET':
      return { count: 0, lastUpdated: new Date() };
    default:
      return state;
  }
};
Merging Generic Interfaces (Declaration Merging)

Unlike type aliases, interfaces support declaration merging — you can split or extend an interface across multiple declarations. This is particularly useful for augmenting third-party library types.

TS
// Base declaration (from a library, for example)
interface Theme<T = object> {
  colors: T;
  spacing: number[];
}

// Your extension — merges with the base
interface Theme<T = object> {
  fontFamily: string;
  borderRadius: number;
}

// The merged interface has all four properties
const myTheme: Theme<{ primary: string }> = {
  colors: { primary: '#3b82f6' },
  spacing: [4, 8, 16, 32],
  fontFamily: 'Inter',
  borderRadius: 4,
};
Note
Declaration merging only works with interface, not type. Use it sparingly — it can make types hard to trace when the declarations are in different files.
Quick Reference
  • interface Foo<T> — declare a generic interface

  • type Foo<T> — declare a generic type alias

  • Interfaces can extend generic interfaces: interface Bar<T> extends Foo<T>

  • Methods can be independently generic: doSomething<U>(fn: (t: T) => U): U

  • Generic interfaces model API responses, repositories, event emitters, and form state

  • type Result<T, E = Error> — default type params work on type aliases too

  • Only interfaces support declaration merging — use this for library augmentation

Success
You can now design flexible, reusable data contracts with generic interfaces and types. Next up: generic classes — bringing the same power to stateful object-oriented TypeScript.