any.1. The Core Problem: Reusability vs Type Safety#
Imagine you want to build an identity function that returns whatever value is passed into it.
Attempt 1: Separate Overloaded Functions (No Code Reuse)#
function identityString(val: string): string { return val; }
function identityNumber(val: number): number { return val; }Attempt 2: Using any (Type Erasure & Zero Safety)#
function identityAny(val: any): any { return val; }
// 'result' is typed as 'any', destroying type safety downstream!
const result = identityAny("Hello World");
result.nonExistentMethod(); // ❌ Runtime Error! No compiler protection.
2. The Solution: Type Parameters (<T>)#
Generics introduce Type Parameters inside angle brackets <T> placed before the function parameter list.
A Type Parameter acts as a variable that captures the type supplied by the caller (or inferred from the argument), allowing you to reuse that exact type signature for return values or internal logic:
// 'T' captures the type of 'val'
function identity<T>(val: T): T {
return val;
}
// 1. Explicit Generic Invocation (Passing <string> manually):
const strResult = identity<string>("Hello TypeScript"); // Return Type: string
// 2. Implicit Generic Inference (TypeScript automatically infers T = number):
const numResult = identity(42); // Return Type: number
// 🟢 Type safety is 100% preserved!
console.log(strResult.toUpperCase());
// ❌ Compiler Error: Property 'toUpperCase' does not exist on type 'number'.
// numResult.toUpperCase();
3. Generic Interfaces & Type Aliases#
Generics are not limited to functions. You can create generic data contracts for API responses, state wrappers, or storage containers.
Generic Interface Example (ApiResponse<T>)#
interface ApiResponse<TData> {
status: number;
message: string;
timestamp: number;
data: TData; // Type parameter passed from caller!
}
interface UserProfile {
id: string;
name: string;
email: string;
}
interface ProductItem {
sku: string;
price: number;
}
// 🟢 Reusing ApiResponse for User data:
const userResponse: ApiResponse<UserProfile> = {
status: 200,
message: "Success",
timestamp: Date.now(),
data: { id: "usr_101", name: "Alice", email: "[email protected]" },
};
// 🟢 Reusing ApiResponse for Product data:
const productResponse: ApiResponse<ProductItem> = {
status: 200,
message: "Success",
timestamp: Date.now(),
data: { sku: "LAPTOP_PRO", price: 1299.99 },
};
console.log(userResponse.data.email.toLowerCase());
console.log(productResponse.data.price.toFixed(2));Generic Type Alias Example (Result<T, E>)#
Generics can accept multiple type parameters:
// Algebraic Result pattern (Success or Failure)
type Result<TData, TError = Error> =
| { success: true; data: TData }
| { success: false; error: TError };
function parseInteger(input: string): Result<number, string> {
const parsed = parseInt(input, 10);
if (isNaN(parsed)) {
return { success: false, error: `Failed to parse '${input}' as integer.` };
}
return { success: true, data: parsed };
}
const res = parseInteger("100");
if (res.success) {
console.log(`Parsed Number: ${res.data.toFixed(0)}`);
} else {
console.error(`Error: ${res.error}`);
}4. Generic Classes#
Generics can also be applied to class definitions, allowing data structure containers (like Queues, Stacks, or Caches) to handle any data type while preserving instance type safety:
class DataQueue<TElement> {
private elements: TElement[] = [];
public enqueue(item: TElement): void {
this.elements.push(item);
}
public dequeue(): TElement | undefined {
return this.elements.shift();
}
public get size(): number {
return this.elements.length;
}
}
// 1. Create a queue strictly for numbers
const numberQueue = new DataQueue<number>();
numberQueue.enqueue(10);
numberQueue.enqueue(20);
const firstNum = numberQueue.dequeue(); // Inferred Type: number | undefined
// ❌ Compiler Error: Argument of type 'string' is not assignable to parameter of type 'number'.
// numberQueue.enqueue("hello");
// 2. Create a queue strictly for string tuples
const commandQueue = new DataQueue<[cmd: string, payload: string]>();
commandQueue.enqueue(["SAVE", "file.txt"]);5. Naming Conventions for Type Parameters#
While <T> (Type) is the standard default name for single generic parameters, using descriptive names is recommended when a signature contains multiple parameters or complex domain logic:
| Short Name | Descriptive Name | Common Convention |
|---|---|---|
T | TItem, TData | Primary Type Parameter |
U, V | TOutput, TResult | Secondary / Output Type Parameters |
E | TError | Exception / Error Type Parameter |
K | TKey | Object Key Type Parameter |
V | TValue | Object Value Type Parameter |
Summary & Next Steps#
In this episode:
- We defined Generics as compile-time type parameters (
<T>). - We proved how generics eliminate repetitive functions and replace unsafe
anytypes. - We constructed generic interfaces (
ApiResponse<TData>), generic type aliases (Result<T, E>), and generic classes (DataQueue<T>). - We explored type parameter inference and naming conventions.
In Episode 30: Generic Constraints (extends), we will learn how to restrict type parameters using the extends keyword (<T extends HasID>)!

