In the development of scalable applications, code reusability without losing type safety plays a crucial role. Without generic programming, developers are forced to either duplicate identical structures for each data type or resort to the any type, which completely destroys static verification.
Generics are parameters for types. Just as regular functions accept value arguments to compute a result at runtime, type constructors accept type parameters (<T>) to compute the resulting static type at compile time.
Based on generics, TypeScript's standard library provides a family of built-in Utility Types: Partial<T>, Required<T>, Readonly<T>, Pick<T, K>, Omit<T, K>, Record<K, T>, ReturnType<T>, and others. These utilities allow transforming existing data models without manual interface duplication.
Let's look at the correspondence between the world of values and the world of types:
+-------------------------------------------------------------------------+
| ANALOGY: VALUES vs TYPES |
+-------------------------------------------------------------------------+
| VALUE WORLD (Runtime): |
| function wrapValue(val) { |
| return { data: val }; |
| } |
| wrapValue(42) ---> { data: 42 } |
+-------------------------------------------------------------------------+
| TYPE WORLD (Compilation): |
| type ApiResponse<T> = { |
| data: T; |
| status: number; |
| }; |
| ApiResponse<User> ---> { data: User; status: number } |
| ApiResponse<Post> ---> { data: Post; status: number } |
+-------------------------------------------------------------------------+
| STANDARD TRANSFORMATIONS (Utility Types): |
| Partial<User> ---> All fields become optional (?) |
| Pick<User, 'id'> ---> Keeps ONLY the specified keys |
| Omit<User, 'pwd'> ---> Excludes specified keys from the type |
+-------------------------------------------------------------------------+<T>)
The developer calls the `identity(42)` function without explicitly specifying `identity<number>(42)`.
The compiler inspects the argument's position and associates the value `42` with the type variable `T`.
If `T extends Entity` is declared, the compiler verifies the inferred type against the base constraint.
Type `T` is fixed, and all dependent positions (return type, other arguments) receive the precise type.
What role does the keyword `extends` play in the declaration `function getEntityId<T extends { id: string }>(entity: T): string`?
apiClient.get<UserProfile>('/me'), ensuring the correct response data type.const [user, setUser] = useState<User | null>(null).Partial<User> for PATCH request endpoints.Record<string, CacheEntry> or Record<Locale, Dictionary>.The compiler automatically infers type T based on the passed argument:
// Generic wrapper function
function wrapResult<T>(value: T): { success: true; payload: T } {
return { success: true, payload: value };
}
const numResult = wrapResult(100); // Type: { success: true; payload: number }
const strResult = wrapResult('OK'); // Type: { success: true; payload: string }
// Generic container interface
interface PaginatedList<TItem> {
items: TItem[];
total: number;
page: number;
hasMore: boolean;
}
interface Article {
slug: string;
title: string;
}
type ArticleFeed = PaginatedList<Article>;Partial, Pick, Omit, Record#interface UserProfile {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
createdAt: Date;
}
// 1. Partial<T>: makes all fields optional (for PATCH requests)
type UpdateUserDto = Partial<UserProfile>;
// Equivalent to: { id?: string; name?: string; email?: string; role?: 'admin' | 'user'; createdAt?: Date }
// 2. Pick<T, K>: selects only the specified fields
type UserPreview = Pick<UserProfile, 'id' | 'name'>;
// Equivalent to: { id: string; name: string }
// 3. Omit<T, K>: excludes specified fields (e.g., removing id and createdAt during creation)
type CreateUserDto = Omit<UserProfile, 'id' | 'createdAt'>;
// Equivalent to: { name: string; email: string; role: 'admin' | 'user' }
// 4. Record<K, T>: creates a typed key-value dictionary
type UserRegistry = Record<string, UserProfile>;
type RolePermissions = Record<'admin' | 'user', string[]>;
const permissions: RolePermissions = {
admin: ['read', 'write', 'delete'],
user: ['read'],
};keyof#Binding two type parameters allows creating absolutely type-safe property access helpers:
// K must be a key of object T!
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const profile: UserProfile = {
id: 'usr_1',
name: 'Anna',
email: '[email protected]',
role: 'admin',
createdAt: new Date(),
};
const userEmail = getProperty(profile, 'email'); // Type of userEmail is strictly string!
const creationDate = getProperty(profile, 'createdAt'); // Type is strictly Date!
// COMPILATION ERROR:
// getProperty(profile, 'nonExistentProperty');
// TS2345: Argument of type '"nonExistentProperty"' is not assignable to parameter of type 'keyof UserProfile'.What type will result from applying `Omit<UserProfile, 'id' | 'role'>` if `UserProfile` contains the fields `id`, `name`, `email`, `role`, `createdAt`?
Do not create generics for the sake of generics:
// REDUNDANT AND POOR PRACTICE:
function printId<T extends string>(id: T): void {
console.log(id);
}
// CORRECT AND CONCISE:
function printId(id: string): void {
console.log(id);
}A generic is only necessary when there is a logical relationship between the types of multiple arguments or the return type.
A dangerous pattern that disables type checks.
Solution: Use Record<string, unknown> for unknown values with subsequent validation, or strict types.