Broadcast Events
The Laravel TypeScript Publisher generates one TypeScript interface per ShouldBroadcast / ShouldBroadcastNow event class, plus a combined broadcast-events.ts index file with a BroadcastEvent union type and a flat BroadcastEvents const of every Echo event name — and, optionally, a module-augmentation file that makes Laravel Echo's Events interface fully typed.
As mentioned in Installation & Usage, broadcast events don't need the @tolki/ts runtime package — the output is plain TypeScript interfaces and a plain const object.
How Broadcast Event Types Are Generated
Unlike broadcast channels, broadcast events use the same modular, per-class pipeline as enums, models, and form requests:
BroadcastEventsCollectordiscovers every class implementingShouldBroadcastorShouldBroadcastNow, by default inapp/Events(configurable, and it extends the sharedCoreCollector, so it supportsincluded/excluded/additional_directoriesand#[TsExclude]— see Filtering & Excluding).- Each event class is statically analyzed by the package's own analyzer to resolve its payload shape — see Property Resolution.
- One
.tsfile is written per event, at a namespace-derived path mirroring the event's FQCN (just like models and enums). - After every event file is generated,
BroadcastEventsIndexWritercombines them into a singlebroadcast-events.tsindex — see The Combined Index File. - Optionally,
BroadcastEventsEchoWritergeneratesecho-broadcast-events.d.ts, a module augmentation for Laravel Echo — see Echo Module Augmentation.
Anatomy of a Generated Event File
Given this event:
class OrderShipped implements ShouldBroadcast
{
public function __construct(
public int $orderId,
public string $trackingNumber,
public string $carrier,
public ?array $metadata = null,
) {}
public function broadcastOn(): Channel
{
return new PrivateChannel("orders.{$this->orderId}");
}
}The package generates app/events/OrderShipped.ts:
/** @see App\Events\OrderShipped */
export interface OrderShipped {
orderId: number;
trackingNumber: `${string}-${string}-${string}`;
carrier: string;
metadata?: Record<string, unknown>;
}- The interface name is always the event's short PHP class name.
- A
@seeJSDoc comment links back to the fully-qualified PHP class. - A public property is required when it is constructor-promoted or declared with a default — every property on
OrderShippedis promoted, so all four are required before#[TsCasts]is applied. - A class-body property with a declared type but no default —
public string $label;, assigned inside the constructor — is optional (label?: string), sincejson_encode()omits a typed property that was never assigned. Reflection can't see a constructor assignment, so a property your constructor always sets still renders with the?; give it a declaration default, or promote it, to get a required key. - A nullable property is typed
| null; nullability alone never makes a key optional. - Here,
trackingNumber's template-literal type,metadata'sRecord<string, unknown>type, and the?onmetadataall come from a#[TsCasts]override on the class — see#[TsCasts]below. Without it, both properties would be their raw inferred types (stringandunknown[] | null), andmetadatawould be required.
Property Resolution: broadcastWith() vs. Public Properties
By default, every public property becomes an interface field, in declaration order — constructor-promoted parameters and class-body declarations alike. A @var docblock wins over the native type, so /** @var list<string> */ public array $tags is typed string[] rather than unknown[]. Every trait-declared property is skipped, whatever the trait — reflection reports them as the event's own, so nothing distinguishes them, and a #[TsExtends] trait's fields already arrive through the extends clause.
Define broadcastWith() to send (and type) a different shape — commonly to exclude private/internal fields:
class TeamMessageSent implements ShouldBroadcast
{
public function __construct(
public int $teamId,
public string $content,
private string $senderToken,
) {}
/**
* @return array{teamId: int, content: string}
*/
public function broadcastWith(): array
{
return [
'teamId' => $this->teamId,
'content' => $this->content,
];
}
public function broadcastOn(): Channel
{
return new Channel("teams.{$this->teamId}");
}
}/** @see Workbench\App\Events\TeamMessageSent */
export interface TeamMessageSent {
teamId: number;
content: string;
}senderToken never appears in the generated interface. The analyzer reads broadcastWith()'s body, resolving each $this->… reference against the event's own declared properties, so the @return array{teamId: int, content: string} docblock above is documentation rather than a requirement — the same interface comes out without it.
When broadcastWith() exists it is the only source of the payload; the public properties are not consulted at all. A key it renames, computes, or drops is reflected exactly, so ['team' => $this->teamId, 'kind' => 'message', 'count' => count($this->items)] becomes { team: number; kind: string; count: number } with no teamId in sight.
Model & Enum-Aware Properties
Properties typed as an Eloquent model or a PHP enum resolve to the same types used elsewhere in the package, with imports added automatically:
class MultiModelEvent implements ShouldBroadcast
{
public function __construct(
public readonly Post $post,
public readonly User $user,
) {}
public function broadcastOn(): Channel
{
return new Channel("multi.{$this->post->id}");
}
}import type { Post, User } from "../models";
/** @see Workbench\App\Events\MultiModelEvent */
export interface MultiModelEvent {
post: Partial<Post>;
user: Partial<User>;
}- An Eloquent model property resolves to
Partial<Model>(partial, since a broadcast payload may not include every column) with an automatic import from the generated models output. - A PHP enum property resolves to the enum's
{Name}Typealias (its raw backing-value type) with an automatic import from the generated enums output. An enum renamed with#[TsEnum]keeps that rename here, so the alias always names a type the enum output actually declares:
class EnumBroadcastEvent implements ShouldBroadcast
{
public function __construct(
public readonly Status $status,
public readonly Color $color,
) {}
public function broadcastOn(): Channel
{
return new Channel('enum-events');
}
}import type { ColorType, StatusType } from "../enums";
/** @see Workbench\App\Events\EnumBroadcastEvent */
export interface EnumBroadcastEvent {
status: StatusType;
color: ColorType;
}TIP
When two properties (or two events combined into the index — see import-conflict aliasing) would import a same-named model or enum from different namespaces, each is automatically aliased with a namespace-derived prefix (e.g. AppUser / CrmUser) so both imports coexist without a collision.
Custom Echo Event Names with broadcastAs()
By default, the Echo event name is Laravel's own .Fully.Qualified.ClassName convention (leading dot, backslashes replaced with dots). Override it with broadcastAs():
class ServerCreated implements ShouldBroadcast
{
public function __construct(
public int $serverId,
public string $serverName,
) {}
public function broadcastAs(): string
{
return 'server.created';
}
public function broadcastOn(): Channel
{
return new Channel('servers');
}
}/** @see Workbench\App\Events\ServerCreated */
export interface ServerCreated extends BroadcastableEvent {
serverId: number;
serverName: string;
}The literal string returned by broadcastAs() ('server.created') becomes this event's key everywhere it's referenced — the BroadcastEvent union member, the BroadcastEvents const value, and the Echo augmentation key. Without broadcastAs(), it would instead be '.Workbench.App.Events.ServerCreated'.
broadcastAs() has to return one whole string literal for that to happen. A name built at runtime — return 'order.'.$this->kind; — has no single value to publish, so the event falls back to the .Fully.Qualified.ClassName convention. The alternative is shipping the literal prefix 'order.' as a key Echo will never receive, which is worse than a key you can predict.
(The extends BroadcastableEvent here comes from a per-class #[TsExtends] attribute — see Extending Interfaces below.)
#[TsCasts] — Overriding Property Types
Override an inferred type, or add a virtual property, the same way as models and form requests:
#[TsCasts([
'trackingNumber' => '`${string}-${string}-${string}`',
'metadata' => ['type' => 'Record<string, unknown>', 'optional' => true],
])]
class OrderShipped implements ShouldBroadcast
{
public function __construct(
public int $orderId,
public string $trackingNumber,
public string $carrier,
public ?array $metadata = null,
) {}
// ...
}This is what produces trackingNumber's template-literal type and metadata's Record<string, unknown> type in the Anatomy example above. As with models and resources, each entry can be a plain type string, or an array with type, optional, and/or import keys for a custom type that needs its own import statement.
Extending Interfaces: Global Config vs. #[TsExtends]
Every generated event interface can extends one or more shared interfaces, using either mechanism (both apply together when present):
Global config — applies to every generated event, via ts_extends.broadcast_events in config/ts-publish.php:
// config/ts-publish.php
'ts_extends' => [
'broadcast_events' => [
['extends' => 'HasTimestamps', 'import' => '@/types/common'],
],
],import type { HasTimestamps } from "@/types/common";
/** @see Workbench\App\Events\UserNotification */
export interface UserNotification extends HasTimestamps {
userId: number;
title: string;
message: string;
}#[TsExtends] attribute — applies to one specific event class:
#[TsExtends('BroadcastableEvent', '@/types/broadcast')]
class ServerCreated implements ShouldBroadcast
{
// ...
}See Extending Interfaces for the full attribute and config syntax.
The Combined Index File (broadcast-events.ts)
After every event file is generated, they're combined into a single index:
import type { EnumBroadcastEvent } from "./app/events/EnumBroadcastEvent";
import type { MultiModelEvent } from "./app/events/MultiModelEvent";
import type { OrderShipped } from "./app/events/OrderShipped";
import type { ServerCreated } from "./app/events/ServerCreated";
import type { TeamMessageSent } from "./app/events/TeamMessageSent";
import type { UserSynced as CrmUserSynced } from "./crm/events/UserSynced";
import type { UserSynced as AppUserSynced } from "./app/events/UserSynced";
export type BroadcastEvent =
| ".Workbench.App.Events.EnumBroadcastEvent"
| ".Workbench.App.Events.MultiModelEvent"
| ".Workbench.App.Events.OrderShipped"
| "server.created"
| ".Workbench.App.Events.TeamMessageSent"
| ".Workbench.Crm.Events.UserSynced"
| ".Workbench.App.Events.UserSynced";
export const BroadcastEvents = Object.freeze({
EnumBroadcastEvent: ".Workbench.App.Events.EnumBroadcastEvent",
MultiModelEvent: ".Workbench.App.Events.MultiModelEvent",
OrderShipped: ".Workbench.App.Events.OrderShipped",
ServerCreated: "server.created",
TeamMessageSent: ".Workbench.App.Events.TeamMessageSent",
CrmUserSynced: ".Workbench.Crm.Events.UserSynced",
AppUserSynced: ".Workbench.App.Events.UserSynced",
} as const);
export type {
EnumBroadcastEvent,
MultiModelEvent,
OrderShipped,
ServerCreated,
TeamMessageSent,
CrmUserSynced,
AppUserSynced,
};BroadcastEventis a union of every event's Echo name (itsbroadcastAs()string, or the default dot-FQCN).BroadcastEventsis a flat, frozen const mapping each event's short class name to its Echo name — flat, unlike Wayfinder's deeply-nested namespace tree, since events are addressed by "what event is this?" rather than by where they live in the codebase.- Every event's interface is also re-exported from the index, so you can import either from the index or directly from the per-event file.
- Import-conflict aliasing: when two different event classes share the same short name (like
App\Events\UserSyncedandCrm\Events\UserSyncedabove), both the import and the const key are aliased with a namespace-derived prefix (AppUserSynced/CrmUserSynced) so both coexist without a collision — the same conflict-resolution strategy used for model/enum property imports within a single event file.
An empty event set (no ShouldBroadcast classes found) produces export {}; instead.
Echo Module Augmentation
When broadcast_events.echo_augmentation.enabled is true (the default), the package also writes echo-broadcast-events.d.ts:
import type { EnumBroadcastEvent } from "./app/events/EnumBroadcastEvent";
import type { OrderShipped } from "./app/events/OrderShipped";
import type { ServerCreated } from "./app/events/ServerCreated";
declare module "@laravel/echo" {
interface Events {
".Workbench.App.Events.EnumBroadcastEvent": EnumBroadcastEvent;
".Workbench.App.Events.OrderShipped": OrderShipped;
"server.created": ServerCreated;
}
}This augments Laravel Echo's own Events interface, so Echo.private(channel).listen(eventName, ...) and useEcho() (from @laravel/echo-vue / @laravel/echo-react) infer the correct payload type from the event name string, with no manual type annotation needed.
The declare module target is resolved with this priority:
broadcast_events.echo_augmentation.echo_packageconfig value, if set.- Auto-detected from your
package.jsondependencies —@laravel/echo-vue, then@laravel/echo-react, then@laravel/echo-svelte. - Falls back to
@laravel/echo(the base package every Echo setup depends on).
The same import-conflict aliasing described above applies here too, so identically-named events from different namespaces resolve correctly.
Filtering & Excluding
Broadcast events support the same discovery controls as enums, models, and form requests:
// config/ts-publish.php
'broadcast_events' => [
'included' => [], // only these event classes (empty = all)
'excluded' => [], // exclude these event classes
'additional_directories' => [], // extra directories beyond app/Events
],#[TsExclude] also works at the class level, since BroadcastEventsCollector extends the shared CoreCollector:
use AbeTwoThree\LaravelTsPublish\Attributes\TsExclude;
#[TsExclude]
class InternalDebugEvent implements ShouldBroadcast
{
// Entirely excluded from collection and publishing.
}See Excluding Content for the full attribute reference.
Configuration Reference
The full list of broadcast_events.* config keys — including the Echo augmentation sub-options and pipeline class overrides for advanced customization — lives in the Configuration Reference.