Define TS Types for your Laravel Echo Events

Seth ChenSeth Chen
3 min read

Hey guys,

I've been working on a Laravel project (personal) for fun, focusing on the latest cool stuff: Laravel 11 & Reverb (from Broadcasting) โญ๏ธ.

Using Laravel Broadcasting means you should use the client library provided by Laravel - Laravel Echo, to save a lot of your time setting up stuff ๐Ÿ˜Ž.

Even though Laravel Echo has been written in TypeScript, however, it doesn't give us any opportunity or battery to "type" our events ๐Ÿ’ฃ.

So that's why we have this topic and I'll show you how to define the types for your events ๐Ÿ˜‰.

Before going deeper

I suppose you already have some knowledge about:

๐Ÿฅน๐Ÿฅน๐Ÿฅน

The Define Types for Laravel Echo

Note: I'm demonstrating on the PresenceChannel (private channel with auth & more features).

Extending the "PresenceChannel"

I'll create a file called echo.ts and extend the PresenceChannel interface

type BaseEvent = {
  type: string;
  payload: unknown;
};

export interface StrictPresenceChannel<Event extends BaseEvent>
  extends PresenceChannel {
  listen<EventType extends Event['type']>(
    event: EventType,
    callback: (data: Extract<Event, { type: EventType }>['payload']) => void
  ): this;
}

This would be our base interface wrapper for the PresenceChannel.

In the real-life app, we would have multiple channels for communicating between client & server.

Each channel should extend the base interface and have its own Events declaration.

Defining Events

Following the BaseEvent structure, we need to define all of the Events that would be sent to the client.

For a sample chat app, I've added these events:

type ChatMessageAddedEvent = {
    type: 'ChatMessageAdded';
    payload: {
        fromUser: {
            id: string; // ULID
        },
        chat: {
            id: string;
            message: string;
        }
    }
}

type ChatImageAddedEvent = {
    type: 'ChatImageAdded';
    payload: {
        fromUser: {
            id: string; // ULID
        },
        chat: {
            id: string;
            imageUrl: string;
        }
    }
}

type ChatEvent = ChatMessageAddedEvent | ChatImageAddedEvent;

Don't forget to add a final type - a union of all events ๐Ÿ˜‰, we'll use it in the part below.

Create a Channel and Extend the StrictPresenceChannel

I created a ChatChannel interface and added the ChatEvent union type.

export interface ChatChannel extends StrictPresenceChannel<ChatEvent> {}

A function to join the Typed-Channel

// getEchoInstance() is simply returning the new Echo({...})

export const getChatChannel = (channelId: string):  => {
  return getEchoInstance().join(channelId) as ChatChannel;
};

Here we have to hack a bit - using as to force the type.

This is the shortcut for a quick win & hassle-free while it won't result in any error or failure.

Use the Typed-Channel instance ๐Ÿ˜Ž

const channel = getRoomChannel(myChatChannel.id);

channel.listen('ChatMessageAdded', (data) => {
    // access data. & IDE will suggest the fields for you 
}).listen('ChatImageAdded', (data) => {
    // access data. & IDE will suggest the fields for you 
});

Then you're all set ๐Ÿ˜Ž IDEs now will handle auto-suggestion, eslint/tsc can do the type check stuff in build time ๐Ÿ’ช

An example from my project:

Notes

Note: this is purely defining types, it won't guarantee that your data is 100% valid and it could go wrong from the end user ๐Ÿ™€.

I'll make another topic for schemas definition & data validation for Laravel Echo soon, ensuring the data is legit to use โญ๏ธ.

Stay tuned ๐Ÿš€

Conclusion

Well, that's basically it for defining types for your Broadcast Events ๐Ÿ˜Ž.

Have fun guys!

15
Subscribe to my newsletter

Read articles from Seth Chen directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Seth Chen
Seth Chen

develops awesome software, contributes to OSS, writes tech tips, and loves Vietnamese milk coffee!