You can give types to object keys in TypeScript using index signatures, the Record utility type, keyof with typeof, and Mapped Types. Each method addresses different scenarios, from handling dynamic key names to creating new types by transforming existing ones.
Using index signatures
Index signatures are ideal for objects with keys that are not known ahead of time but follow a consistent pattern. They define the type for the object's keys and the type for its values.
Usage:
interface UserScores {
[key: string]: number;
}
const userScores: UserScores = {
alice: 100,
bob: 120,
carol: 90,
};
console.log(userScores.alice); // No error, 'alice' is a string key
// console.log(userScores.dave); // No error at compile-time, but returns undefined at runtime
// Adding a new key is allowed
userScores.dave = 80;
Use code with caution.
Benefits:
- Allows flexible property names that aren't known statically.
- Enforces that all values for the specified key type conform to a single value type.
Limitations:
- You cannot specify different value types for different keys, as the signature applies to all keys.
- TypeScript treats all keys as the index signature type (e.g.,
string), which can lead to runtime errors if you try to access a non-existent key without checking.
Using the Record utility type
The Record<Keys, Type> utility type constructs an object type with a specific set of keys (Keys) and a uniform value type (Type). It provides stronger type safety than index signatures for a known set of keys.
Usage:
// Define a union of literal types for the keys
type Course = "Computer Science" | "Mathematics" | "Literature";
// Define the shape of the values
interface CourseInfo {
professor: string;
cfu: number;
}
// Use `Record` to create a new type where keys are from `Course`
const courses: Record<Course, CourseInfo> = {
"Computer Science": { professor: "Mary Jane", cfu: 12 },
"Mathematics": { professor: "John Doe", cfu: 12 },
"Literature": { professor: "Frank Purple", cfu: 12 },
};
// This would cause a compile-time error because 'History' is not a valid key
// courses.History = { professor: "Jane Doe", cfu: 10 };
Use code with caution.
Benefits:
- Strong type safety, as it guarantees that all specified keys exist on the object.
- Clearly communicates the intent of a dictionary-like object with known keys.
- Protects against adding keys that are not part of the
Keysunion.
Limitations:
- Not suitable for objects where the keys are completely dynamic and not known ahead of time.
Combining keyof with typeof
For situations where you have an existing object and want to derive a type based on its keys, you can combine the keyof and typeof operators.
Usage:
// Existing object with predefined keys and values
const colorPalette = {
primary: "blue",
secondary: "green",
tertiary: "yellow",
};
// Use `typeof` to get the object's type, then `keyof` to get a union of its keys
type ColorKey = keyof typeof colorPalette; // "primary" | "secondary" | "tertiary"
function getColor(key: ColorKey): string {
return colorPalette[key];
}
console.log(getColor("primary")); // Valid, returns "blue"
// console.log(getColor("invalidKey")); // Compile-time error
Use code with caution.
Benefits:
- Allows you to create a type from an existing object, ensuring consistency if the object is updated.
- Ideal for configuration objects or other constants where you need to reference keys in a type-safe way.
Limitations:
- Only works for objects that have already been declared.
- Does not enforce uniform value types across all keys.
Using mapped types
Mapped types are a more advanced feature that allows you to transform an existing object type's properties into a new type. They are useful for creating reusable utility types, such as making all properties of an object readonly or optional.
Usage:
interface User {
name: string;
age: number;
}
// Mapped type that makes all properties of `User` optional
type PartialUser = {
[K in keyof User]?: User[K];
};
const user1: PartialUser = {
name: "Alice", // 'age' is now optional
};
// Mapped type that makes all properties of `User` readonly
type ReadonlyUser = {
readonly [K in keyof User]: User[K];
};
const user2: ReadonlyUser = {
name: "Bob",
age: 30,
};
// user2.name = "Charlie"; // Compile-time error: Cannot assign to 'name' because it is a read-only property.
Use code with caution.
Benefits:
- Extremely flexible and powerful for creating complex type transformations.
- Enables reusable generic types that can operate on any object type.
Limitations:
- More complex syntax can have a higher learning curve.
- Best suited for advanced use cases where you need to modify or iterate over an existing type's keys.