Explain Codes LogoExplain Codes Logo

An index signature parameter type cannot be a union type. Consider using a mapped object type instead

javascript
mapped-types
enum
type-aliases
Alex KataevbyAlex Kataev·Oct 23, 2024
TLDR

To overcome the "index signature cannot be a union" error, make use of a mapped type. Define a type where each key in your union maps to an identical value type:

type KeyUnion = 'foo' | 'bar'; type MappedType = { [K in KeyUnion]: any }; // `MappedType` is your go-to for index-safe operations

This strategy creates a type with separate properties for each union member, skillfully sidestepping the restriction.

Implementing index signature with enums

In a situation where you're integrating enums and intend to create index signatures, a mapped type offers a cleaner solution compared to traditional index signatures.

enum EmployeeRole { Admin = 'admin', User = 'user', Guest = 'guest' // Here just to taste the coffee ☕ } type RolePermissions = { [K in EmployeeRole]: string[] }; // Now we've got a neat schedule of permissions for each role. Fancy, isn't it? 🎩

The in keyword in this scenario helps us iterate over enum keys to avoid the union type error. An efficient pattern to create types indexed with enum values is utilizing the Record built-in utility type:

type UserAccess = Record<EmployeeRole, string[]>;

Optional properties made easy

There are times when you'd prefer an index signature where it is not mandatory for all keys in the union to be present. Here, you can call upon a PartialRecord type to save the day:

type PartialRecord<K extends keyof any, T> = { [P in K]?: T }; type EasierPermissions = PartialRecord<EmployeeRole, string[]>; // Now each property is optional, creating a lot more elbow room 🕺

This tip defines a type where each property is optional, giving you a much-needed dose of flexibility.

Type aliases overthrow interfaces

While interfaces sit on the throne when it comes to declaring the shape of an object, type aliases usurp the power when it comes to power and flexibility for mapped types:

// Harness the power of type alias for intricate structures type IntricatePermission = { [K in EmployeeRole as `can${Capitalize<K>}`]: boolean // Suddenly, we can. 💪 };

Type aliases grant you the power to extend or modify the property keys using features like template literal types.

Expert scenarios and advanced mapping solutions

Mastering key transformations with mapped types

Mapped types don't limit you to merely mapping over keys, they also empower you to transform them as per your whim:

// Prefixing our permissions with 'can' type PrefixedPermissions = { [K in EmployeeRole as `can${Capitalize<K>}`]: boolean // Unleash the can-do spirit! 🚀 };

Tackling complex property types with conditional property types

By harnessing the might of conditional types, you can set off to create detailed type definitions that respond differently based on input types:

type ConditionalPermissions<K extends keyof any> = { [P in K]: P extends 'admin' ? string[] : boolean // Admins get the fancy list because... they're admins! };

Managing key complexities with ease

As your application bursts at the seams, you'll probably witness an explosive increase in the size of key sets you need to handle. Mapped types are ever at your service, providing the flexibility you need:

type EnormousPermissionsSet<K extends string> = { [P in K]: boolean // Because booleans don't elicit as many questions 😉 };

Show, don't tell: Practical examples

Type-safe API response handler? Check!

Suppose you're dealing with an API that returns different shapes of data depending on user roles. Mapped types can come to your rescue to guarantee type safety:

type ServerResponse<T> = { data: T }; function manageResponse<T extends EmployeeRole>(role: T, response: ServerResponse<ResponsibilitiesByRole[T]>) { // Response is managed based on role. No mess, no stress. 🧘‍♂️ }

The manageResponse(T extends EmployeeRole)(role: T, response: ServerResponse[ResponsibilitiesByRole[T]]) function has a set signature that forces the response to conform to the structure defined by our mapped type ResponsibilitiesByRole.

Molding to changing data structures, hassle-free

The ever-evolving landscape of real-world applications often results in changes in data structures. Mapped types allow you to adapt to these changes in a less error-prone manner:

enum UpdatedRoles { Manager = 'manager', // More roles piling up... } type EnhancedPermissions = { [K in keyof typeof UpdatedRoles]: string[]; }; // Update the `UpdatedRoles` enum. Done and dusted! ✔️

Ready for the unknown? With mapped types, absolutely!

As TypeScript advances, the ability to create more advanced types is likely to follow suit. Bearing in mind TypeScript's development path, mapped types are anticipated to increase in capabilities, bracing your codebase for future advancements.