TypeScript Generics Overview
Let’s break down each piece to clarify what’s happening with T
, extends
, and why these constructs are often used, and what generic type parameter are in TypeScript.
DISCLAIMER: The majority of this article was generated from ChatGPT prompts, and curated/edited by a human being.
What Does ‘T’ Mean?
What exactly does the T
in something like <T extends { [key: string]: any }>
do or mean? This T
is a generic type parameter. Generics allow you to create reusable, type-safe code that works with different types while preserving type information.
- When you see
<T>
, it means the code is introducing a placeholder for a type, namedT
. - The type of
T
will be determined when the generic is used (either explicitly or inferred by TypeScript).
Example:
function identity<T>(value: T): T {
return value;
}
identity(42); // T is inferred as number
identity("hello"); // T is inferred as string
What is the ‘extends’ keyword for?
The extends
keyword in a generic constraint means that T
must satisfy the condition of being assignable to the specified type, which in this case is { [key: string]: any }
.
{ [key: string]: any }
is a type that represents an object with string keys and any values.- By saying
T extends { [key: string]: any }
, you’re constrainingT
to be an object-like type with string keys and values of any type.
This prevents T
from being a primitive type like number
, string
, or boolean
, which are not assignable to an object type.
Example:
function logObjectKeys<T extends { [key: string]: any }>(obj: T) {
console.log(Object.keys(obj));
}
logObjectKeys({ a: 1, b: 2 }); // Works
logObjectKeys(42); // Error: number is not assignable to { [key: string]: any }
What does ‘Record’ mean exactly?
The Record
type indicates that the variable should be a kind of object literal (see this StackOverflow post). Also, Record<string, any>
is a utility type provided by TypeScript. It is equivalent to { [key: string]: any }
, but more succinct and standardized.
It means an object with keys of type string
and values of any type:
type MyObject = Record<string, any>;
const example: MyObject = { a: 1, b: "hello" }; // Works
The key differences between <T extends { [key: string]: any }>
and Record<string, any>
are:
Aspect | <T extends { [key: string]: any }> | Record<string, any> |
---|---|---|
Generic | Yes, defines a generic type placeholder T . | No, Record<string, any> is a specific type. |
Flexibility | You can add additional constraints or combine with other types. | Fixed type definition. |
Use Case | Used when defining generic functions or classes. | Used for a concrete type definition. |
Why do we see ‘T extends …’ in function parameters?
The main reasons are:
-
Type Constraints: To ensure the function parameter
T
satisfies certain requirements. For example, ensuringT
is an object type or has specific keys. -
Type Inference: When
T
is a generic type, it allows TypeScript to infer the type based on the function’s usage, which makes the code more flexible and reusable.
Example:
function getValue<T extends { [key: string]: any }>(obj: T, key: string): any {
return obj[key];
}
const user = { id: 1, name: "John" };
getValue(user, "name"); // Works
getValue(42, "id"); // Error: Argument of type 'number' is not assignable to ...
Summary of Typescript Generics
To summarize what generics in TypeScript do:
- T: Is a generic placeholder that makes your code flexible and reusable.
- extends: Is a constraint that ensures
T
adheres to a specific shape or type. - Record<string, any>: Is a predefined TypeScript type for objects with string keys and
any
values, useful for concrete type definitions. - Use Case:
<T extends { [key: string]: any }>
is used when defining generic, type-safe functions.Record<string, any>
, however, is used for fixed object type definitions.
Examples of Generics and Record
Generics like T
can work with Record<string, any>
just as they work with { [key: string]: any }
. In fact, using Record<string, any>
is often cleaner because it’s a standard utility type provided by TypeScript.
TypeScript Examples Using Extends
Basic Example with <T extends { [key: string]: any }>
:
function printKeys<T extends { [key: string]: any }>(obj: T): void {
console.log(Object.keys(obj));
}
printKeys({ name: "Alice", age: 30 }); // Logs: ['name', 'age']
printKeys({ id: 123, active: true }); // Logs: ['id', 'active']
// printKeys(42); // Error: Type 'number' is not assignable to '{ [key: string]: any }'
Here, T
is constrained to be object-like (an object with string keys and any values). If you pass a non-object (like a number), TypeScript gives an error.
The same behavior can be implemented using Record<string, any>
:
function printValues<T extends Record<string, any>>(obj: T): void {
console.log(Object.values(obj));
}
printValues({ firstName: "Bob", lastName: "Smith" }); // Logs: ['Bob', 'Smith']
printValues({ x: 1, y: 2 }); // Logs: [1, 2]
// printValues(42); // Error: Type 'number' is not assignable to 'Record<string, any>'
This is functionally equivalent to the first example, but uses Record<string, any>
for the constraint.
Adding Specificity to Generics
You can further constrain T
by adding more specific requirements. Here’s an example that require specific keys:
function getProperty<T extends Record<string, any>>(obj: T, key: keyof T): any {
return obj[key];
}
const person = { name: "Alice", age: 25 };
console.log(getProperty(person, "name")); // Logs: Alice
console.log(getProperty(person, "age")); // Logs: 25
// console.log(getProperty(person, "height")); // Error: Argument of type '"height"' is not assignable to 'keyof T'
In the above example, keyof T
ensures the key
parameter is valid for the given object obj
.
Combining Multiple Constraints
You can use extends
to create more complex constraints. For example, requiring T
to have specific properties in addition to being a Record:
function hasRequiredFields<T extends Record<string, any> & { id: string; isActive: boolean }>(obj: T): boolean {
return obj.id !== "" && obj.isActive;
}
const user = { id: "123", isActive: true, name: "Alice" };
const product = { id: "", isActive: false, price: 100 };
console.log(hasRequiredFields(user)); // Logs: true
console.log(hasRequiredFields(product)); // Logs: false
// hasRequiredFields(42); // Error: Type 'number' is not assignable to required type
The above example shows how T
must be an object with string keys (Record<string, any>
), and must also have id
and isActive
as properties.
Using with Default Type Values
You can set a default type for a generic, which can also use Record<string, any>
:
function mergeObjects<T extends Record<string, any> = {}>(obj1: T, obj2: T): T {
return { ...obj1, ...obj2 };
}
const merged = mergeObjects({ a: 1 }, { b: 2 });
console.log(merged); // Logs: { a: 1, b: 2 }
const defaultMerged = mergeObjects({}, { name: "Bob" });
console.log(defaultMerged); // Logs: { name: "Bob" }
This example shows how the default type for T
falls back to an empty object ({}
) if none is provided.
Dynamic Typing in Real-Life Use Cases
You might use <T extends Record<string, any>>
when parsing API responses, where you want to ensure flexibility for unknown keys but still have type safety:
function parseApiResponse<T extends Record<string, any>>(response: T): T {
console.log("Keys:", Object.keys(response));
console.log("Values:", Object.values(response));
return response;
}
const apiResponse = { status: "success", data: { id: 1, name: "Alice" } };
parseApiResponse(apiResponse);
Dynamic Form Validation
You could validate dynamic forms where fields are not predefined but need to follow a certain structure:
function validateForm<T extends Record<string, string>>(formData: T): boolean {
return Object.values(formData).every((value) => value.trim() !== "");
}
const form = { username: "user1", email: "user@example.com" };
console.log(validateForm(form)); // Logs: true
const invalidForm = { username: "", email: "user@example.com" };
console.log(validateForm(invalidForm)); // Logs: false
Conclusion
Here’s a final summary of the T extends Record<string, any>
benefits:
- Ensures the parameter is object-like: Prevents passing primitive types like
number
orboolean
. - Provides flexibility: You can combine it with additional constraints to create more specific types.
- Reusable and Clean: By using
Record<string, any>
, you avoid boilerplate definitions like{ [key: string]: any }
. - Type Safety: Allows TypeScript to infer types while maintaining checks like
keyof T
.