I have been using TypeScript for some time now and have always faced challenges with this particular issue. My goal is to create an Event system for our application, where I can ensure type safety when creating objects that group events related to a specific context.
Summary
Before diving into the details of what I am looking for, let me first outline the end result that I am aiming for:
type DashboardGroups = "home" | "settings" | "profile";
// events in `DashboardEventsMap` should belong to one of these groups only
type DashboardEvent = IMetricsEvent<DashboardGroups>;
// structure enforcement for events: `{ [event:string]: DashboardEvent }`
const DashboardEventsMap = createEventMapping<DashboardEvent>({
validEvent: {
name: "valid event",
group: "home" // ✅ this should work
},
invalidEvent: {
name: "invalid event",
group: "invalid group", // ❌ This should give a type error
}
})
// BUT MOST IMPORTANTLY
// Maintain shape and intellisense while accessing the map object
DashboardEventsMap.validEvent // ✅ Should work and show in autocomplete
DashboardEventsMap.eventWhichDoesntExist // ❌ Should give a type error
Details
Events follow a certain structure and must be able to accept a custom group to ensure they belong to specific groups within different parts of the application.
export interface IMetricsEvent<
TGroup extends string = string,
> {
name: string;
group?: TGroup;
}
Currently facing some challenges with my createEventMapping
function:
type MetricsEventMapType<TEvent extends IMetricsEvent> = {
[event: string]: TEvent;
};
export const createMetricsEventMapping = <
TMetricsEvent extends IMetricsEvent,
T extends MetricsEventMapType<TMetricsEvent> = MetricsEventMapType<TMetricsEvent>,
>(
arg: T,
) => arg;
1. No type
const map = createMetricsEventMapping({event: {name:"event", group:"any group"});
map.event // ✅ autocompletion works but there is no typechecking on the groups
2. Passing in event type
If I pass in an event type, I get type checking on the groups but lose autocompletion:
type DashboardEvent = IMetricsEvent<"home">;
const map = createMetricsEventMapping<DashboardEvent>({event: {name:"event", group:"any group"});
map.event // ❌ type checking works on the groups above ☝️ but there's no autocompletion anymore
3. Removing the optional default = MetricsEventMapType<TMetricsEvent>
= MetricsEventMapType<TMetricsEvent>
The issue here arises because when no types are passed, TypeScript infers everything correctly. However, when the first type argument TMetricsEvent
is passed, TypeScript expects T to also be passed and defaults to
MetricsEventMapType<TMetricsEvent>
instead of being inferred.
When I remove the optional default value
= MetricsEventMapType<TMetricsEvent>
, TypeScript raises an error when passing the TMetricsEventType
:
export const createMetricsEventMapping = <
TMetricsEvent extends IMetricsEvent,
T extends MetricsEventMapType<TMetricsEvent>, // removed default here
>(
arg: T,
) => arg;
type DashboardEvent = IMetricsEvent<"home">;
// ❌ Now gives a type error saying expected 2 type arguments but got 1
const map = createMetricsEventMapping<DashboardEvent>({event: {name:"event", group:"any group"});
If you have any insights on better ways to handle this scenario with more robust types or alternatives like function inference, nested functions, etc., please share your thoughts. Any assistance is greatly appreciated!