Indexed Types and the keyof Keyword in TypeScript |
Indexed types and keyof are powerful tools for manipulating object types in TypeScript.
They allow us to dynamically access object properties and create flexible type mappings.
Why Indexed Types Are Needed
In JavaScript, we often need to dynamically access object properties.
For example, a function might need to get all keys of an object or access the value type based on a key.
Indexed types and keyof enable us to express this dynamism within the type system while maintaining type safety.
Concept: Indexed 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 is used to get a union type composed of all keys of an object type.
Example
// Define user type
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{
return obj;
}
const user: User ={
id:1,
name:"Alice",
email:"alice@example.com"
};
// Get name property
const userName: string = getProperty(user,"name");
console.log("Username: "+ userName);
Output:
Username: Alice
Note: keyof returns a literal union type of key names, not a string type.
Index Access Types
Using index access types allows you to retrieve the type of an object property.
Example
// Define user type
interface User {
id: number;
name: string;
email: string;
}
// Use index access types to get property types
type UserId = User;// number
type UserName = User;// string
// Can use union types for multiple key access
type UserIdAndName = User["id"|"name"];// number | string
// Use keyof to get a union of all property types
type AllUserValues = User;// number | string
// Practical usage example
function getValue<T, K extends keyof T>(obj: T, key: K): T{
return obj;
}
const user: User ={ id:1, name:"Bob", email:"bob@test.com"};
// Get id type
const idValue: number = getValue(user,"id");
console.log("ID: "+ idValue);
// Get name type
const nameValue: string = getValue(user,"name");
console.log("Name: "+ nameValue);
Index Access: Using square brackets [] allows accessing specific property types of an object type, which is very powerful and flexible.
Basic Mapped Types
Mapped types allow creating new types based on existing ones by iterating over their keys.
Example
// Define user type
interface User {
id: number;
name: string;
email: string;
age: number;
}
// Make all properties optional
type PartialUser = Partial;
// Make all properties read-only
type ReadonlyUser = Readonly;
// Custom mapped type: make all properties optional and stringified
type Stringify<T>={
: string;
};
type StringifiedUser = Stringify;
// Practical usage example
const partialUser: PartialUser ={
id:1,
name:"Alice"
// email and age are optional
};
const readonlyUser: ReadonlyUser ={
id:1,
name:"Bob",
email:"bob@test.com",
age:25
};
// readonlyUser.name = "Charlie"; // Error: read-only
console.log("Partial user: "+ partialUser.name);
console.log("Read-only user: "+ readonlyUser.name);
Mapped Types: The syntax iterates through all keys of a type, forming the basis for creating utility types.
Constraining Key Types
Use keyof with generic constraints to limit the keys accepted by a function.
Example
// Define configuration type
interface Config {
apiUrl: string;
timeout: number;
retry:boolean;
}
// Only allow getting existing keys
function getConfigValue<T, K extends keyof T>(
config: T,
key: K
): T{
return config;
}
// Define configuration
const config: Config ={
apiUrl:"https://api.example.com",
timeout:5000,
retry:true
};
// Correct: key exists
const url: string = getConfigValue(config,"apiUrl");
const timeoutVal: number = getConfigValue(config,"timeout");
// Incorrect: 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 exist on the object, preventing runtime errors.
Extracting Properties of Specific Types
Extract property keys of a specific type from an object type.
Example
// Define mixed type
interface Mixed {
id: number;
name: string;
age: number;
email: string;
active:boolean;
}
// Extract all string-type keys
type StringKeys<T>={
: Textends string ? K : never;
};
// Extract all number-type keys
type NumberKeys<T>={
: Textends number ? K : never;
};
// Test
type StringProps = StringKeys;// "name" | "email"
type NumberProps = NumberKeys;// "id" | "age"
// Practical application: get values of string properties
function getStringProps<T, K extends StringKeys<T>>(obj: T, keys: K[]): T[]{
return keys.map(key => obj);
}
const mixed: Mixed ={
id:1,
name:"Alice",
age:25,
email:"alice@test.com",
active:true
};
const strings = getStringProps(mixed,["name","email"]);
console.log("String properties: "+ strings.join(", "));
Conditional Types: Combining conditional types with mapped types enables complex type filtering and extraction.
Iterating Over Array Types
Use indexed types to operate on arrays and tuples.
Example
// Define tuple type
type Tuple =[string, number,boolean];
// Get element types of tuple
type First = Tuple;// string
type Second = Tuple;// number
type Third = Tuple;// boolean
// Use number to get all element types
type AllElements = Tuple;// string | number | boolean
// Get array element types
type StringArray = string[];
type ArrayElement = StringArray;// string
// Practical application: function overloading
function getElement<T extends any[]>(arr: T, index: number): T|undefined{
return index < arr.length? arr: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 be indexed using numeric indices, where each index corresponds to a specific element type β something arrays cannot do.
Notes
- keyof returns a union type: keyof returns a literal union type of key names
- Safe index access: Ensure accessed keys exist in the target type
- Generic constraints: Use extends keyof to constrain generic parameters
- Mapped types require the in keyword: Mapped types use the syntax
Best Practice: Indexed types are the foundation for creating generic utility types. Mastering them significantly enhances your ability to work with types.
Summary
Indexed types and keyof are important components of TypeScript's type system.
- keyof: Gets all keys of an object type
- Index Access: Retrieves property types via keys
- Mapped Types: Creates new types based on existing ones
- Type Safety: Ensures type safety during dynamic property access
Suggestion: When working with object properties, prefer indexed types to maintain type safety.
YouTip