trusktr
trusktr

Reputation: 45484

How can we map an array of strings to a TypeScript type map of keys to types?

I've the following working code, with no type errors:

type Events = { SOME_EVENT: number; OTHER_EVENT: string }

interface EventEmitter<EventTypes> {
  on<K extends keyof EventTypes>(s: K, listener: (v: EventTypes[K]) => void);
}

declare const emitter: EventEmitter<Events>;

emitter.on('SOME_EVENT', (payload) => testNumber(payload));
emitter.on('OTHER_EVENT', (payload) => testString(payload));

function testNumber( value: number ) {}
function testString( value: string ) {}

(example on TS Playground)

However, I'd like to use something similar to an enum in order to have autocompletion of the type names, so that I can write something like the following instead of using string literals:

emitter.on(EventNames.SOME_EVENT, (payload) => testNumber(payload));
emitter.on(EventNames.OTHER_EVENT, (payload) => testString(payload));

I'm trying to keep things DRY, so I'm wondering if there's a way I can make this work without repeating all the event names in a new type.

In plain JavaScript, I can easily do the following:

const EventNames = [
    'SOME_EVENT',
    'OTHER_EVENT',
]

const Events = {}

for (const eventName of Events) {
    Events[eventName] = eventName
}

// then use it:

emitter.on(Events.SOME_EVENT, (payload) => testNumber(payload));
emitter.on(Events.OTHER_EVENT, (payload) => testString(payload));

So in plain JS I can create the enum-like objects without having to resort to something a little less DRY like

const Events = {
    SOME_EVENT: 'SOME_EVENT', // repeats the names twice
    OTHER_EVENT: 'OTHER_EVENT',
}

emitter.on(Events.SOME_EVENT, (payload) => testNumber(payload));
emitter.on(Events.OTHER_EVENT, (payload) => testString(payload));

I'm wondering how I can keep things as DRY as possible in TypeScript, only ever defining the name of each event just once, but also have a type associated with the event payloads.

I can do it the following less-DRY way, repeating the event names three times each:

type Events = { SOME_EVENT: number; OTHER_EVENT: string }

const EventNames: {[k in keyof Events]: k} = {
  SOME_EVENT: 'SOME_EVENT', OTHER_EVENT: 'OTHER_EVENT'
}

interface EventEmitter<EventTypes> {
  on<K extends keyof EventTypes>(s: K, listener: (v: EventTypes[K]) => void);
}

declare const emitter: EventEmitter<Events>;

emitter.on(EventNames.SOME_EVENT, (payload) => testNumber(payload));
emitter.on(EventNames.OTHER_EVENT, (payload) => testString(payload));

function testNumber( value: number ) {}
function testString( value: string ) {}

(Playground link)

So my question is: Is there a way that I can write this so that I specify the events only one time each, as well as the payload types?

If it isn't possible to do it with only one instance of each event name, what's the best way?

Upvotes: 0

Views: 3104

Answers (1)

trusktr
trusktr

Reputation: 45484

I found a way to keep things DRY so that each event name is defined only once, using a dummy class that holds type information AND generates JS output that we can iterate on in order to create the enum-like with proper typing:

class EventTypes {
    constructor(
        // define a map of event names to event payload types here.
        public SOME_EVENT: number,
        public OTHER_EVENT: string
    ) {}
}

const EventNames = {} as { [k in keyof EventTypes]: k }

for (const key in new (EventTypes as any)) {
    EventNames[key] = key
}

console.log(EventNames)

interface EventEmitter<EventTypes> {
  on<K extends keyof EventTypes>(s: K, listener: (v: EventTypes[K]) => void);
}

declare const emitter: EventEmitter<EventTypes>;

emitter.on(EventNames.SOME_EVENT, (payload) => testNumber(payload));
emitter.on(EventNames.OTHER_EVENT, (payload) => testString(payload));

function testNumber( value: number ) { console.assert(typeof value === 'number') }
function testString( value: string ) { console.assert(typeof value === 'string') }

playground link

Upvotes: 1

Related Questions