npm install @playlive/twitch-shared
Zero-dependency TypeScript interfaces shared across the @playlive/twitch-*
family — @playlive/twitch-charity,
@playlive/twitch-helix,
@playlive/twitch-extension,
@playlive/twitch-eventsub — and any downstream consumer.
The package ships two frozen constants (PACKAGE_NAME, KNOWN_URLS) and
nothing else at runtime — every other export is an interface that disappears
at build time. It exists so that services and clients can agree on Twitch wire
shapes and on pluggable client/database contracts without depending on each
other, which keeps build graphs acyclic.
bun add @playlive/twitch-shared
No peer dependencies and no runtime dependencies.
If you only ever write import type { … }, the package erases completely and
can live in devDependencies instead:
bun add -D @playlive/twitch-shared
Importing the PACKAGE_NAME / KNOWN_URLS values makes it a real runtime
dependency — use the plain bun add form in that case. The package is
sideEffects: false, so a type-only import tree-shakes to zero bytes either
way.
import type { TwitchChannel } from "@playlive/twitch-shared";
function describeChannel(channel: TwitchChannel): string {
return `${channel.broadcaster_name} — ${channel.title} (${channel.game_name})`;
}
Field names are the upstream Helix JSON verbatim (snake_case) so a raw response body can be cast without renaming:
const body = (await res.json()) as { data: TwitchChannel[] };
const channel = body.data[0];
| Subpath | Description |
|---|---|
@playlive/twitch-shared |
Single barrel — every type + constant. |
There's deliberately only one entrypoint: the package is so small that subpath splitting would just add noise to consumers.
Full generated API documentation: https://packages.playlive.experience.stjude.org/p/@playlive/twitch-shared/docs/
| Export | Kind | Notes |
|---|---|---|
TwitchChannel |
interface | Helix GET /helix/channels?broadcaster_id=… shape. |
TwitchStream |
interface | Helix GET /helix/streams?user_id=… shape. |
TwitchUser |
interface | Helix GET /helix/users?login=… minimal shape (id / login / display_name). |
RawTwitchAppToken |
interface | Client-credentials token response (refresh_token optional). |
RawTwitchUserToken |
interface | Authorisation-code token response (refresh_token + scope[] required). |
RawTwitchCharity |
interface | Helix GET /helix/charity/campaigns shape (snake_case, minor-unit amounts). |
ITwitchAPIClient |
interface | Minimum Helix-client surface (chatbot-friendly, 3 methods). |
ITwitchChatbotDatabase |
interface | Minimum chatbot-database surface (2 methods). |
PACKAGE_NAME |
const | "@playlive/twitch-shared". Runtime value. |
KNOWN_URLS |
const | Frozen empty array — no network traffic from this package. |
Method signatures for the two pluggable interfaces:
interface ITwitchAPIClient {
getChannelInfo(twitchUserID: string): Promise<TwitchChannel | null>;
getTwitchUserData(args: {
token?: string;
twitchUserID?: string;
twitchUsername?: string;
}): Promise<TwitchUser | null>;
sendChatMessage(
message: string,
broadcasterUserID: string,
prefix?: string,
): Promise<unknown>;
}
interface ITwitchChatbotDatabase {
saveBotOAuth(token: RawTwitchAppToken): Promise<unknown>;
setStreamStatus(twitchUsername: string, online: boolean): Promise<unknown>;
}
Type shapes track the public Twitch Helix API reference —
https://dev.twitch.tv/docs/api/reference/ — which is the authority for every
snake_case field name here. Helix is unversioned (additive changes only), so
these interfaces describe the current Helix responses for:
| Interface | Twitch endpoint |
|---|---|
TwitchChannel |
GET /helix/channels — https://dev.twitch.tv/docs/api/reference/#get-channel-information |
TwitchStream |
GET /helix/streams — https://dev.twitch.tv/docs/api/reference/#get-streams |
TwitchUser |
GET /helix/users — https://dev.twitch.tv/docs/api/reference/#get-users |
RawTwitchCharity |
GET /helix/charity/campaigns — https://dev.twitch.tv/docs/api/reference/#get-charity-campaign |
RawTwitchAppToken / RawTwitchUserToken mirror the OAuth token responses
documented at
https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/ (client
credentials and authorisation code grants respectively).
Interfaces are intentionally minimal subsets: only the fields Play Live consumes are declared, so an upstream addition never breaks a build.
This package performs no fetch or WebSocket traffic, so the
KNOWN_URLS export is a frozen empty array:
import { KNOWN_URLS } from "@playlive/twitch-shared";
console.log(KNOWN_URLS); // []
console.log(Object.isFrozen(KNOWN_URLS)); // true
Nothing from this package needs to appear on a Twitch Extension's manifest URL allowlist.
ITwitchAPIClient over native fetchITwitchAPIClient exists so downstream code (chatbots, simulators, tests) can
depend on a behaviour instead of on
@playlive/twitch-helix directly, which keeps build graphs
acyclic. Any object with these three methods is a valid client:
import type {
ITwitchAPIClient,
RawTwitchAppToken,
TwitchChannel,
TwitchUser,
} from "@playlive/twitch-shared";
const HELIX = "https://api.twitch.tv/helix";
export function createHelixShim(
token: RawTwitchAppToken,
clientId: string,
): ITwitchAPIClient {
const headers = {
Authorization: `Bearer ${token.access_token}`,
"Client-Id": clientId,
};
return {
async getChannelInfo(twitchUserID: string): Promise<TwitchChannel | null> {
const res = await fetch(
`${HELIX}/channels?broadcaster_id=${encodeURIComponent(twitchUserID)}`,
{ headers },
);
if (!res.ok) return null;
const body = (await res.json()) as { data: TwitchChannel[] };
return body.data[0] ?? null;
},
async getTwitchUserData({ twitchUserID, twitchUsername }): Promise<TwitchUser | null> {
const qs = twitchUserID
? `id=${encodeURIComponent(twitchUserID)}`
: `login=${encodeURIComponent(twitchUsername ?? "")}`;
const res = await fetch(`${HELIX}/users?${qs}`, { headers });
if (!res.ok) return null;
const body = (await res.json()) as { data: TwitchUser[] };
return body.data[0] ?? null;
},
async sendChatMessage(message, broadcasterUserID, prefix): Promise<unknown> {
const res = await fetch(`${HELIX}/chat/messages`, {
method: "POST",
headers: { ...headers, "content-type": "application/json" },
body: JSON.stringify({
broadcaster_id: broadcasterUserID,
sender_id: broadcasterUserID,
message: prefix ? `${prefix} ${message}` : message,
}),
});
return res.json();
},
};
}
Consuming code never names a concrete client:
async function announceChannel(api: ITwitchAPIClient, broadcasterId: string) {
const channel = await api.getChannelInfo(broadcasterId);
if (!channel) return;
await api.sendChatMessage(`Now playing ${channel.game_name}!`, broadcasterId, "[bot]");
}
RawTwitchCharity mirrors the Helix JSON, so amounts arrive as integer minor
units plus a decimal_places divisor — never as a float:
import type { RawTwitchCharity } from "@playlive/twitch-shared";
const campaign: RawTwitchCharity = {
broadcaster_id: "1",
broadcaster_login: "playliver",
broadcaster_name: "Playliver",
charity_description: "Finding cures. Saving children.",
charity_logo: "https://cdn.twitch/st-jude.png",
charity_name: "St. Jude",
charity_website: "https://stjude.org",
current_amount: { currency: "USD", decimal_places: 2, value: 284_750 },
target_amount: { currency: "USD", decimal_places: 2, value: 500_000 },
id: "tw-c-1",
};
const raised = campaign.current_amount.value / 10 ** campaign.current_amount.decimal_places;
console.log(raised); // 2847.5
To translate that into the canonical Tiltify shapes the overlays render, use
the converters in @playlive/twitch-charity.
MIT © St. Jude Children's Research Hospital