Enforce Type Safety in TypeScript: Using Enums as Restricted Key Types
In TypeScript, enums (enumerations) are a way to define a set of named constants. They improve code readability and maintainability by using meaningful names instead of raw numbers. But enums can also be powerful for restricting the types of keys you can use in objects.
The in
Keyword
The in
keyword allows you to specify that an object's keys must be from a particular enum. Here's how it works:
enum Color {
Red = "red",
Green = "green",
Blue = "blue",
}
let colorStatus: { [key in Color]: string } = {
Red: "active",
Green: "inactive",
};
// This is valid because "Yellow" is not a member of the Color enum
// colorStatus.Yellow = "unknown"; // TypeScript error
In this example:
- The
Color
enum defines three color constants. - The object
colorStatus
is typed to have keys that are members of theColor
enum, and the values are strings. - You can only assign properties to
colorStatus
using the enum members (Red
,Green
, orBlue
). Trying to use a non-existent color like"Yellow"
will result in a TypeScript error during compilation.
Optional Keys with ?
You can make some or all of the enum-based keys optional by adding a question mark (?
) after the closing square bracket:
let colorStatus: { [key in Color]?: string } = {
Red: "active",
};
// Now this is valid because Green is optional
colorStatus.Green = "unknown";
Using keyof typeof
While the in
syntax is convenient, it has a limitation: you need to explicitly list all enum members in the object type. For larger enums, this can become cumbersome.
The keyof typeof
construct provides a more dynamic solution. It gets the literal type of all the keys from the enum:
enum Size {
Small = "small",
Medium = "medium",
Large = "large",
}
type SizeStatus = { [key in keyof typeof Size]: string };
let sizeStatus: SizeStatus = {
Small: "in stock",
Medium: "out of stock",
};
// This is still valid because the key comes from the Size enum
sizeStatus.Large = "available soon";
Here's a breakdown:
keyof typeof Size
gets the literal type of allSize
enum members (e.g.,"small"
,"medium"
,"large"
).- The object type
SizeStatus
uses this type to restrict its keys.
Key Points
- Using enums as restricted key types enforces type safety, preventing typos and ensuring consistency.
- The
in
keyword is simple but requires listing enum members explicitly. keyof typeof
is more dynamic, especially for large enums, but might feel less readable at first.- Choose the approach that best suits your project's needs and coding style.
enum UserRole {
Admin = "admin",
Editor = "editor",
Reader = "reader",
}
let userPermissions: { [key in UserRole]: string[] } = {
Admin: ["create", "edit", "delete"],
Editor: ["create", "edit"],
Reader: ["view"],
};
// This will cause a TypeScript error because "Author" is not a UserRole
// userPermissions.Author = ["comment"];
console.log(userPermissions.Admin); // Output: ["create", "edit", "delete"]
This example defines a UserRole
enum and an object userPermissions
with keys restricted to the enum members. It ensures that permissions are assigned only to valid user roles.
enum ProductStatus {
Available = "available",
OutOfStock = "out_of_stock",
Discontinued = "discontinued",
}
let productInventory: { [key in ProductStatus]?: number } = {
Available: 10,
OutOfStock: 0,
};
// This is valid because ProductStatus.Discontinued is optional
productInventory.Discontinued = 5; // Setting a discontinued product's quantity
console.log(productInventory); // Output: { Available: 10, OutOfStock: 0, Discontinued: 5 }
Here, some keys in the productInventory
object are optional using the ?
syntax. This allows flexibility for discontinued products that might not have a quantity.
Dynamic Keys with keyof typeof:
enum OrderType {
Online = "online",
Phone = "phone",
WalkIn = "walk_in",
}
type OrderDetails = {
[key in keyof typeof OrderType]: {
items: string[];
customer: string;
};
};
let orders: OrderDetails = {
Online: {
items: ["Book", "Pen"],
customer: "John Doe",
},
Phone: {
items: ["Shirt", "Hat"],
customer: "Jane Smith",
},
};
// Valid because OrderType.WalkIn is a valid key
orders.WalkIn = {
items: ["Headphones"],
customer: "Alice Johnson",
};
console.log(orders.Online.customer); // Output: John Doe
This example uses keyof typeof
to dynamically extract the enum keys and create a more generic OrderDetails
type. It demonstrates how this approach can be useful for handling different order types with similar structures.
If your keys are a fixed set of well-defined strings and you don't need the benefits of enums (like named constants), you can use a union of string literals:
type UserRole = "admin" | "editor" | "reader";
let userPermissions: { [key in UserRole]: string[] } = {
admin: ["create", "edit", "delete"],
editor: ["create", "edit"],
reader: ["view"],
};
This approach offers type safety similar to enums but is less flexible for adding new roles later.
Literal Types with as const
If you have a constant object with known string keys, you can use the as const
assertion to create a literal type for its keys:
const productStatuses = {
available: "available",
outOfStock: "out_of_stock",
discontinued: "discontinued",
} as const;
type ProductStatus = keyof typeof productStatuses;
let productInventory: { [key in ProductStatus]: number } = {
available: 10,
outOfStock: 0,
};
This method is useful when working with existing constant objects and provides type safety for the keys. However, it requires an initial constant object and doesn't allow dynamic modification of the keys.
Choosing the Right Method
Here's a quick guide to help you decide when to use each approach:
- Enums: Best for named constants with potential for adding new ones later.
- Union of String Literals: Suitable for a fixed set of well-defined string keys without the need for named constants.
- Literal Types with
as const
: Useful for working with existing constant objects where the keys are known and won't change.
typescript