Skip to content

Generic Interfaces and Type Aliases

Generic type parameters are not limited to functions. Interfaces and type aliases can be parameterized too, producing reusable shapes that adapt to any type that fills the parameter.

You have already written type Nullable<T> = T | null as a pattern — here it is as a named generic alias:

type Nullable<T> = T | null;
type NullableHighScore = Nullable<HighScore>; // HighScore | null
type NullableQuestion = Nullable<TriviaQuestion>; // TriviaQuestion | null

Nullable<T> captures a recurring pattern — “this value or null” — and gives it a name. Every use is consistent: change the definition once and every alias updates.

An interface can declare a type parameter the same way a function does:

interface ApiResult<T> {
data: T;
timestamp: number;
source: string;
}
const questionResult: ApiResult<TriviaQuestion[]> = {
data: questions,
timestamp: Date.now(),
source: 'opentdb.com',
};
const scoreResult: ApiResult<HighScore> = {
data: { score: 8, total: 10, date: 'Jan 15, 2024' },
timestamp: Date.now(),
source: 'localStorage',
};

ApiResult<T> describes any wrapped response — the wrapping structure (timestamp, source) stays the same while the data property changes type.

Promise<T> and Array<T> are generic interfaces

Section titled “Promise<T> and Array<T> are generic interfaces”

You have been using generic interfaces throughout the course:

const p: Promise<TriviaQuestion[]> = fetchQuestions(10);
// Promise<T> with T = TriviaQuestion[]
const arr: Array<string> = ['Mercury', 'Venus'];
// Array<T> with T = string — identical to string[]

Promise<T> declares that the resolved value will be of type T. Array<T> declares that every element will be of type T. Both are built into TypeScript’s standard library as generic interfaces — you supply the type argument and the library does the rest.

Generic type aliases work well for reusable callback patterns:

type Handler<T> = (value: T) => void;
type QuestionHandler = Handler<TriviaQuestion>;
// (value: TriviaQuestion) => void
type ScoreHandler = Handler<number>;
// (value: number) => void

Handler<T> names the pattern “a function that receives a T and returns nothing”. Specializing it with <TriviaQuestion> or <number> produces readable, consistent callback types.

AceIt’s QuizState is a discriminated union. You could generalize the active branch to carry any data type:

type LoadingState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'ready'; data: T }
| { status: 'error'; message: string };
type QuizLoadingState = LoadingState<TriviaQuestion[]>;
type ScoreLoadingState = LoadingState<HighScore>;

LoadingState<T> captures a common async data pattern — idle, loading, ready with data, or error — and can be applied to any data type. AceIt’s QuizState is a specialized version of this pattern.

Not every type benefits from a parameter. Generalize when:

  • The same structure appears with different data types in multiple places.
  • The relationship between the type parameter and the shape is meaningful (not just coincidental).

Do not generalize speculatively — three similar lines of code is not automatically a sign to abstract. AceIt’s TriviaQuestion and HighScore are intentionally not merged into a generic Question<T> because they describe genuinely different things.

In types.ts:

  1. Add type Nullable<T> = T | null. Replace HighScore | null wherever it appears with Nullable<HighScore>.
  2. Write type Handler<T> = (value: T) => void. Define type ScoreHandler = Handler<{ score: number; total: number }>.
  3. Write interface Wrapped<T> { value: T; timestamp: number }. Create a Wrapped<HighScore> variable.
  4. Review QuizState — identify which branches carry data and what the data type is. Note how this maps to the LoadingState<T> pattern above, even though AceIt keeps the concrete version.
  • Interfaces and type aliases can declare type parameters just like functions do.
  • Promise<T> and Array<T> are generic interfaces you have been using throughout the course.
  • Generic type aliases like Nullable<T> and Handler<T> name recurring patterns and keep them consistent.
  • Generalize when the same structure recurs with different data types — not speculatively.
  • A generic interface describes a shape that adapts to any type that fills the parameter.