1

Let's say I have this AnalyticsService class

export class AnalyticsService {
  static sendAnalytics(eventName: string) {
    console.log(eventName);
    // logic here...
  }

  static EVENTS = {
    Header: {
      LogoClicked: "Header: Logo Clicked",
    },
    UserMenu: {
      LoginButtonClicked: "User Menu: Login Button Clicked",
      LogoutButtonClicked: "User Menu: Logout Button Clicked",
    }
  };
}

And I use this class to send analytics like:

AnalyticsService.sendAnalytics(AnalyticsService.EVENTS.Header.LogoClicked)

I want to extract all values of EVENTS to a union type to make sure that sendAnalytics function gets only existing event names

for example, the results here should be: "Header: Logo Clicked" | "User Menu: Login Button Clicked" | "User Menu: Logout Button Clicked"

Is it even possible with typescript?

If it is, is it going to significantly reduce the typescript performance when it's a pretty big object?

Edit: just to clarify the EVENTS object can be really nested (I gave a tiny example just for simplicity )

Israel kusayev
  • 833
  • 8
  • 20
  • Why not use an `enum`? – Bergi Dec 29 '21 at 14:51
  • Btw, you shouldn't use a `class` if it contains only `static` members. – Bergi Dec 29 '21 at 14:52
  • @Bergi I took it from a real class so in reality we have tons of things there. also, can you explain why I shouldn't use class with static members? it's convenient – Israel kusayev Dec 29 '21 at 15:02
  • It's less convenient, less efficient, and less concise than [just using an object literal or named module exports](https://stackoverflow.com/q/29893591/1048572). – Bergi Dec 29 '21 at 15:08
  • @Bergi I don't use enum because enum is flat and we want the EVENTS to be very nested – Israel kusayev Dec 30 '21 at 07:29
  • The `eventName: string` parameter to your function is very flat. – Bergi Dec 30 '21 at 14:15
  • @Bergi right, but when I pass the string to the function I pass it like `AnalyticsService.EVENTS.Header.LogoClicked` not like "someEvent" – Israel kusayev Dec 30 '21 at 16:18
  • `sendAnalytics` doesn't know that, and TypeScript won't check for it. You might as well pass a string literal, it works the same - and you ask for that union type so that only the right strings are passed, right? Because if you already had a rule that ensures you pass `EVENTS.Header.LogoClicked`, you wouldn't need this type check at all. – Bergi Dec 30 '21 at 16:30

1 Answers1

0

Here is how you could do to correctly type sendAnalytics:

type Keys = typeof AnalyticsService.EVENTS[keyof typeof AnalyticsService.EVENTS]

type UnionToIntersection<U> =
  (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never

type FlattenedEvents = UnionToIntersection<Keys>;

type EventNames = FlattenedEvents[keyof FlattenedEvents];

export class AnalyticsService {
  static sendAnalytics(eventName: EventNames) {
    console.log(eventName);
    // logic here...
  }

  static EVENTS = {
    Header: {
      LogoClicked: "Header: Logo Clicked",
    },
    UserMenu: {
      LoginButtonClicked: "User Menu: Login Button Clicked",
      LogoutButtonClicked: "User Menu: Logout Button Clicked",
    }
  } as const;
}

AnalyticsService.sendAnalytics(AnalyticsService.EVENTS.Header.LogoClicked); // OK
AnalyticsService.sendAnalytics('test'); // KO

TypeScript playground

Guerric P
  • 30,447
  • 6
  • 48
  • 86
  • It's nice, but my object can be really nested, and I don't want to add `keyof typeof AnalyticsService.EVENTS.SOMETHING_ELSE` every time I add something. (I don't want to hard-coded the Keys) – Israel kusayev Dec 29 '21 at 15:05