58

How do i create a customEvent Typescript and use it? I found this link on javascript on Mozilla site (https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent)

I am just doing some testing on custom Event but Typescript see it as an error. what i am planning on doing is add some extra data into the details property to use later: here is my code.

let div:any=document.getElementById("my_div");

let c_event = new CustomEvent("build",{'details':3 });

div.addEventListener("build",function(e:Event){
    console.log(e.details);
}.bind(this));

div.dispatchEvent(c_event);
2
  • what error do you see? that looks like valid js code thus most likely valid ts as well. Commented Mar 24, 2017 at 14:15
  • let c_event = new CustomEvent("build",{'details':3 }); should be let c_event = new CustomEvent("build",{detail:3 }); Commented Mar 24, 2017 at 16:09

9 Answers 9

56

2024-11-28: Hijacking top answer to show use of CustomEventInit for preserving the detail property type:

// Note trailing "!" here is the non-null assertion operator.
// This removes the `null` from the `HTMLElement | null` type
// that getElementByID returns.
//
// Source: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#non-null-assertion-operator
const div: HTMLElement = document.getElementById("my_div")!;

// Note: Property name is "detail", not "details"
//
// Source: https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/detail
const c_event = new CustomEvent("build", { detail: 3 });

div.addEventListener("build",(e:CustomEventInit<number>) => {
  // `detail` is properly typed as `number` here!
  console.log(e.detail);
});

div.dispatchEvent(c_event);

[Original answer, kept for posterity:]

Simplest way is like so:

window.addEventListener("beforeinstallprompt", ((event: CustomEvent) => {
   console.log("Whoop!");
}) as EventListener);
Sign up to request clarification or add additional context in comments.

5 Comments

this should be the go-to answer for many reasons
Could you elaborate? What are some of the many reasons that this should be the go-to answer?
@FlavioWuensche - Because it is much more brief than the top answer, and contains less irrelevant code than the accepted answer?
If your goal is to just get it "working" than yes this works, but it doesn't actually give you much benefit outside of compiling since CustomEvent.detail will be any. The other solution by Jurijs will map the type to it's detail type in a way that makes the consuming code more aware of the underlying type of each event.
Easiest solution. In order to give detail a type, just use CustomEvent<{ x: number... }>.
51

The solution described in the accepted answer gets the job done but for the cost of losing type safety.

If you want to keep the type-safety going I would suggest the following:

Create dom.d.ts file in @types folder in your sources (or configure typeRoots to make sure that TS compiler will look there) with the following:

interface CustomEventMap {
    "customnumberevent": CustomEvent<number>;
    "anothercustomevent": CustomEvent<CustomParams>;
}
declare global {
    interface Document { //adds definition to Document, but you can do the same with HTMLElement
        addEventListener<K extends keyof CustomEventMap>(type: K,
           listener: (this: Document, ev: CustomEventMap[K]) => void): void;
        dispatchEvent<K extends keyof CustomEventMap>(ev: CustomEventMap[K]): void;
    }
}
export { }; //keep that for TS compiler.

This will augment global definition of document's addEventListener function to accept your synthetic event and its typed params.

now you can do:

function onCustomEvent(event: CustomEvent<CustomParams>){
  this.[...] // this is Document
  event.detail ... //is your CustomParams type.
}
document.addEventListener('anothercustomevent', onCustomEvent);

This way you will have everything typed and under control.

3 Comments

Three small changes for completeness sake. 1) The addEventListener declaration should include the third options parameter: options?: boolean | AddEventListenerOptions. 2) The return type of the listener param should be any to match the one in lib.dom.d.ts 3) You should include the declaration for removeEventListener (including the above points): removeEventListener<K extends keyof CustomEventMap>( type: K, listener: (this: HTMLElement, ev: CustomEventMap[K]) => any, options?: boolean | EventListenerOptions, ): void;
Also, if you move the CustomEventMap to the global definition, you don't have to redefine the params every time. You can then just use it like: const customHandler = (event: CustomEventMap['mycustomevent']) => console.log(event.detail.every.defined.prop.will.be.here)
This was close to what I needed, but using GlobalEventHandlersEventMap was way shorter and needed no further definition. Found in this github comment: github.com/microsoft/TypeScript/issues/…
34

The property name is detail and not details. The correct code needs to be:

let div: any = document.getElementById("my_div");

let c_event = new CustomEvent("build",{detail: 3});

div.addEventListener("build", function(e: CustomEvent) { // change here Event to CustomEvent
    console.log(e.detail);
}.bind(this));

div.dispatchEvent(c_event);

5 Comments

At first I thought this didn't work, but then i figured out that JSON.stringify() on a CustomEvent doesn't show the detail member ... sigh.
Seeing this: has no properties in common with type 'CustomEventInit<unknown>'
It worked for me, only when i defined the type of the event-parameter in the addEventListener as CustomEventInit
CustomEvent is a generic type, so in this example, with the type of detail being number, you can use CustomEvent<number>. See answer stackoverflow.com/a/62568282/685715
I don't know if this is something with TS version but CustomEvent won't work, CustomEventInit does as stated earlier, the generic being the type of your custom "detail".
15

CustomEvent is a generic type. You can pass the type of the detail property as a parameter (it defaults to any). Here is how it is defined in lib.dom.d.ts (which is in the lib directory of your npm typescript installation):

interface CustomEvent<T = any> extends Event {
    /**
     * Returns any custom data event was created with. Typically used for synthetic events.
     */
    readonly detail: T;
    initCustomEvent(typeArg: string, canBubbleArg: boolean, cancelableArg: boolean, detailArg: T): void;
}

In the OP's example, the type of detail is number. So, building on the answer from @Diullei:

let div: HTMLElement | null = document.getElementById("my_div");

let c_event = new CustomEvent<number>("build", {detail: 3});

div.addEventListener("build", function(e: CustomEvent<number>) { // change here Event to CustomEvent
    console.log(e.detail);
}.bind(this));

div.dispatchEvent(c_event);

Above, I've also used the HTMLElement type from lib.dom.d.ts. As a newcomer to TypeScript, I found it very useful to scan this file, and search it for 'obvious' types.

2 Comments

if you add index.d.ts to your root it picks up automatically in REACT. I also added the declare CustomEvent and interface CustomEventInit
The link is 404 now. I have found github.com/microsoft/TypeScript-DOM-lib-generator/blob/… instead
13

Jurijs Kovzels' answer is technically correct - all you have to do is extend the global DOM types.

But the implementation is a bit tedious: you will have to extend type of addEventListener(), removeEventListener() and dispatchEvent() for all types of elements (HTMLInputElement, HTMLDivElement, etc.)

Instead, I'd suggest to

Extend GlobalEventHandlersEventMap.

There are a couple of ways you can do it:

A) Declare it globally in a file of interest

This is a fast way to make sure everything works but I would not keep it in a production codebase.

declare global {
  interface GlobalEventHandlersEventMap {
    build: CustomEvent<number>;
  }
}

B) Create your own .d.ts declaration file

You can do the same as above by writing your own file with global declarations

  1. Create a file declarations/dom.d.ts and add the following in it
interface GlobalEventHandlersEventMap {
  build: CustomEvent<number>;
}
  1. Then make sure it is included in your tsconfig.json
{
  "include": [
    "declarations/*.d.ts",
  ],
}
  1. And it works! Now your listeners are properly typed typed-custom-event-listener

You can go further and add util for creating strictly typed events like this:

export const createTypedCustomEvent = <T extends CustomEventType>(
  type: T,
  eventInitDict: CustomEventInit<CustomEventHandlersMap[T]>,
) => new CustomEvent(type, eventInitDict);

Then you will be getting type hints and checks for the detail field:

Non existent event typed-custom-event-key-error

Incorrect type of event detail typed-custom-event-detail-type-error

Comments

6

I ended up taking a different approach. Instead, I made a wrapper class that extended EventTarget.

type AudioEvent = {bytes: Uint8Array};

interface IAudioEventTarget {
  addListener(callback: (evt: CustomEvent<AudioEvent>) => void): void
  dispatch(event: AudioEvent): boolean;
  removeListener(callback: (evt: CustomEvent<AudioEvent>) => void): void
}

class AudioEventTarget extends EventTarget implements IAudioEventTarget {
  private readonly targetType = 'audio-event';

  addListener(callback: (evt: CustomEvent<AudioEvent>) => void): void {
    return this.addEventListener(this.targetType, callback as (evt: Event) => void);
  }

  dispatch(event: AudioEvent): boolean {
    return this.dispatchEvent(new CustomEvent(this.targetType, { detail: event }));
  }

  removeListener(callback: (evt: CustomEvent<AudioEvent>) => void): void {
    return this.removeEventListener(this.targetType, callback as (evt: Event) => void);
  }
};

And usage is as follows:

const audioEventTarget = new AudioEventTarget();
const listener = (audioEvent: CustomEvent<AudioEvent>) => {
    console.log(`Received ${audioEvent.detail.bytes.length} bytes`);
}

audioEventTarget.addListener(listener);
audioEventTarget.dispatch({bytes: new Uint8Array(10)});
audioEventTarget.removeListener(listener);

Comments

1

maybe overly complicated but typesafe?

interface FizzInfo {
  amount: string;
}

interface BuzzInfo {
  level: number;
}

interface FizzBuzzEventMap {
  fizz: CustomEvent<FizzInfo>;
  buzz: CustomEvent<BuzzInfo>;
}

interface FizzerBuzzer extends EventTarget {
  addEventListener<K extends keyof FizzBuzzEventMap>(type: K, listener: (this: FizzerBuzzer, ev: FizzBuzzEventMap[K]) => void, options?: boolean | AddEventListenerOptions): void;
  addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
  removeEventListener<K extends keyof FizzBuzzEventMap>(type: K, listener: (this: FizzerBuzzer, ev: FizzBuzzEventMap[K]) => void, options?: boolean | EventListenerOptions): void;
  removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}

class FizzerBuzzer extends EventTarget {
  numFizz: number = 0;
  numBuzz: number = 0;

  start(): void {
    setInterval(() => this.emitFizz(), 3000);
    setInterval(() => this.emitBuzz(), 5000);
  }
  emitFizz(): void {
    ++this.numFizz;
    this.dispatchEvent(new CustomEvent<FizzInfo>('fizz', {
      detail: { amount: this.numFizz.toString() },
    }));
  }
  emitBuzz(): void {
    ++this.numBuzz;
    this.dispatchEvent(new CustomEvent<BuzzInfo>('buzz', {
      detail: { level: this.numBuzz },
    }));
  }
}


const fb = new FizzerBuzzer();
fb.addEventListener('fizz', (ev) => {
  console.assert(typeof ev.detail.amount === 'string', 'bad!');
  console.log(ev.detail.amount);
});
fb.addEventListener('buzz', (ev) => {
  console.assert(typeof ev.detail.level === 'number', 'bad');
  console.log(ev.detail.level);
});

Comments

0

Here you go. Credits to @kirill-taletski

declare global {
    interface GlobalEventHandlersEventMap {
        'your-custom-event-name': CustomEvent<{}>;
    }
}

export const createCustomEvent = <T extends keyof GlobalEventHandlersEventMap>(
    type: T,
    eventInitDict: CustomEventInit<
        GlobalEventHandlersEventMap[T] extends CustomEvent<infer T> ? T : never
    >,
) => new CustomEvent(type, eventInitDict);

2 Comments

{} is not what you think it is. This will accept any value.
@BartLouwers of course, this is an example. You must specify any type of data you need.
0

I wanted not to just type custom events but to also make an API for them that is separate from the usual DOM events so extending/overriding the built-in types didn't work for me. So I went with Bryan McGrane's answer but instead of extending EventTarget , I instantiated it and provided typed methods that just call the EventTarget 's methods.

interface EventMap {
  delete_colorset: CustomEvent<{ id: string }>;
}

class EventManager {
  #eventTarget = new EventTarget();

  addEventListener<K extends keyof EventMap>(
    type: K,
    listener: (event: EventMap[K]) => void,
    options?: boolean | AddEventListenerOptions
  ): void {
    this.#eventTarget.addEventListener(type, listener as EventListener, options);
  }

  removeEventListener<K extends keyof EventMap>(
    type: K,
    listener: (event: EventMap[K]) => void,
    options?: boolean | EventListenerOptions
  ): void {
    this.#eventTarget.removeEventListener(type, listener as EventListener, options);
  }

  dispatch<K extends keyof EventMap>(type: K, detail: EventMap[K]['detail']): boolean {
    return this.#eventTarget.dispatchEvent(new CustomEvent(type, { detail }));
  }
}

const eventManager = new EventManager();

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.