TypeScript Index Types and the keyof Keyword
Index types and keyof are powerful tools in TypeScript for manipulating object types.
They allow us to dynamically access object properties and create flexible type mappings.
SVG Diagram: How Index Types and keyof Work
Why Do We Need Index Types?
In JavaScript, we often need to dynamically access object properties.
For example, a function may need to get all the keys of an object, or access the corresponding value type based on a key.
Index types and keyof allow us to express this dynamism in the type system while maintaining type safety.
Concept:Index types are a class of types that allow dynamic access to object properties, and keyof is used to obtain a union type composed of all keys of an object type.
The keyof Operator
The keyof Operator
Example
interface User {
id: number; // User ID
name: string; // Username
email: string; // Email
age?: number; // Age (optional)
}
// Use keyof to get the union type of all keys
// Result: "id" | "name" | "email" | "age"
type UserKeys = keyof User;
// Test keyof
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user: User = {
id: 1,
name: "Alice",
email: "[email protected]"
};
// Get the name property
const userName: string = getProperty(user, "name");
console.log("Username: " + userName);
Output:
用户名: Alice
Explanation:keyof returns a union type of key name literals, not a string type.
Index Access Types
Using Index Access Types, you can get the type of an object property.
Example
interface User {
id: number;
name: string;
email: string;
}
// Use index access types to get property types
type UserId = User["id"]; // number
type UserName = User["name"]; // string
// You can use a union type to access multiple keys
type UserIdAndName = User["id" | "name"]; // number | string
// Use keyof to get the union of all property types
type AllUserValues = User[keyof User]; // number | string
// Practical usage example
function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user: User = { id: 1, name: "Bob", email: "[email protected]" };
// Get the id type
const idValue: number = getValue(user, "id");
console.log("ID: " + idValue);
// Get the name type
const nameValue: string = getValue(user, "name");
console.log("Name: " + nameValue);
Index access:Using square brackets [] you can access the type of a specific property of an object type; it is very powerful and flexible.
Mapped Types Basics
Mapped types allow you to create new types based on existing types by iterating over all their keys.
Example
interface User {
id: number;
name: string;
email: string;
age: number;
}
// Make all properties optional
type PartialUser = Partial<User>;
// Make all properties readonly
type ReadonlyUser = Readonly<User>;
// Custom mapped type: make all properties optional and stringify them
type Stringify<T> = {
[P in keyof T]: string;
};
type StringifiedUser = Stringify<User>;
// Practical usage example
const partialUser: PartialUser = {
id: 1,
name: "Alice"
// email and age are optional
};
const readonlyUser: ReadonlyUser = {
id: 1,
name: "Bob",
email: "[email protected]",
age: 25
};
// readonlyUser.name = "Charlie"; // Error: readonly
console.log("Partial user: " + partialUser.name);
console.log("Readonly user: " + readonlyUser.name);
Mapped types:Using the [P in keyof T] syntax to iterate over all keys of a type is the foundation for creating utility types.
Constraining the Type of Keys
Use keyof and generic constraints to restrict the keys a function accepts.
Example
interface Config {
apiUrl: string;
timeout: number;
retry: boolean;
}
// Can only get existing keys
function getConfigValue<T, K extends keyof T>(
config: T,
key: K
): T[K] {
return config[key];
}
// Define the configuration
const config: Config = {
apiUrl: "https://api.example.com",
timeout: 5000,
retry: true
};
// Correct: the key exists
const url: string = getConfigValue(config, "apiUrl");
const timeoutVal: number = getConfigValue(config, "timeout");
// Error: the key does not exist (TypeScript will report an error)
// const invalid = getConfigValue(config, "unknown");
console.log("API URL: " + url);
console.log("Timeout: " + timeoutVal);
Constraint:K extends keyof T ensures that the passed key must be a key that exists on the object, preventing runtime errors.
Getting Only Properties of a Specific Type
Extract property keys of a specific type from an object type.
Example
interface Mixed {
id: number;
name: string;
age: number;
email: string;
active: boolean;
}
// Extract all keys of string type
type StringKeys<T> = {
[K in keyof T]: T[K] extends string ? K : never;
}[keyof T];
// Extract all keys of number type
type NumberKeys<T> = {
[K in keyof T]: T[K] extends number ? K : never;
}[keyof T];
// Test
type StringProps = StringKeys<Mixed>; // "name" | "email"
type NumberProps = NumberKeys<Mixed>; // "id" | "age"
// Practical application: get the value of a string property
function getStringProps<T, K extends StringKeys<T>>(
obj: T,
keys: K[]
): T[K][] {
return keys.map(key => obj[key]);
}
const mixed: Mixed = {
id: 1,
name: "Alice",
age: 25,
email: "[email protected]",
active: true
};
const strings = getStringProps(mixed, ["name", "email"]);
console.log("String properties: " + strings.join(", "));
Conditional types:Combining conditional types and mapped types enables complex type filtering and extraction.
Iterating Over Array Types
Use index types to manipulate arrays and tuples.
Example
type Tuple = [string, number, boolean];
// Get the tuple element type
type First = Tuple[0]; // string
type Second = Tuple[1]; // number
type Third = Tuple[2]; // boolean
// Use number to get the type of all elements
type AllElements = Tuple[number]; // string | number | boolean
// Get the array element type
type StringArray = string[];
type ArrayElement = StringArray[number]; // string
// Practical application: function overloads
function getElement<T extends any[]>(
arr: T,
index: number
): T[indexof T] | undefined {
return index < arr.length ? arr[index] : undefined;
}
const tuple: Tuple = ["hello", 123, true];
const arr: string[] = ["a", "b", "c"];
console.log("Tuple element + getElement(tuple, 0));
console.log("Array element + getElement(arr, 1));
Tuple indexing:Tuples can use numeric indices, and each index corresponds to a specific element type, which arrays cannot do.
Notes
- keyof returns a union type:keyof returns a union type of key name literals
- Index access safety:Ensure the accessed key exists in the target type
- Generic constraints:Use extends keyof to constrain generic parameters
- Mapped types require the in keyword:Mapped types use the [P in keyof T] syntax
Best practices:Index types are the cornerstone of creating generic utility types; mastering them can greatly improve your type manipulation abilities.
Summary
Index types and keyof are important components of the TypeScript type system.
- keyof:Get all keys of an object type
- Index access:Get property types through keys
- Mapped types:Create new types based on existing types
- Type safety:Ensure type safety for dynamic property access
Other extensionsRecommendation:When you need to manipulate object properties, prefer using index types to maintain type safety.