# Primitives API Guide for library authors building UI primitives (like ``, ``). ## Problem Primitives don't know which preset the user chose: ```tsx // Inside @videojs/react - shipped to users export function PlayButton() { const player = usePlayer(); // What type is this? // User might use presets.website or presets.background // We don't know if playbackFeature is included } ``` They need: 1. Loosely typed access to the player 2. A way to check if a feature exists 3. Type narrowing when the feature is present ## Solution: `hasFeature`, `getFeature`, `throwMissingFeature` Three utilities for feature access: | Function | Returns | Use case | | ----------------------- | ------------------------------------ | ---------------------------------- | | `hasFeature(player, f)` | `boolean` (type guard) | Conditional narrowing, `if` blocks | | `getFeature(player, f)` | Typed object, props `T \| undefined` | Direct access, optional chaining | | `throwMissingFeature` | `never` (throws) | Critical features, fail fast | ### `hasFeature` — Type Guard ```tsx import { features, hasFeature, throwMissingFeature, usePlayer } from '@videojs/react'; export function PlayButton() { const player = usePlayer(); // UnknownPlayer - loosely typed if (!hasFeature(player, features.playback)) { throwMissingFeature(features.playback, { displayName: 'PlayButton' }); } // TypeScript narrows: player.paused and player.play() are now typed return ; } ``` ### `getFeature` — Direct Access For optional features, use `getFeature` with safe access: ```tsx const volume = getFeature(player, features.volume); volume.setVolume?.(0.5); // Safe - no crash if undefined ``` ## Types ### `StoreProxy` Contract All proxies implement `StoreProxy`, which holds a reference to the underlying store: ```ts const STORE_SYMBOL: unique symbol; interface StoreProxy { readonly [STORE_SYMBOL]: T; [key: string]: unknown; } ``` The index signature `[key: string]: unknown` allows any property access. After `hasFeature` narrows, explicit properties take precedence. ### Proxy Types ```ts interface UnknownPlayerStore extends Store {} interface UnknownMediaStore extends Store {} interface UnknownPlayer extends StoreProxy {} interface UnknownMedia extends StoreProxy {} ``` The `[]` for features means "no features statically typed" — the store has features at runtime, but TypeScript doesn't know which ones. Use `hasFeature` to narrow. ### Type Summary | Type | Description | | -------------------- | ---------------------------------- | | `StoreProxy` | Base interface for all proxies | | `UnknownPlayer` | Player proxy with unknown features | | `UnknownMedia` | Media proxy with unknown features | | `UnknownPlayerStore` | Player store with unknown features | | `UnknownMediaStore` | Media store with unknown features | ### Creating Proxies Internally, proxies are created from stores via `createProxy()`: ```ts import { createProxy } from '@videojs/store'; const store = createStore({ ... }); const proxy = createProxy(store); // StoreProxy ``` This is used internally by `createPlayer` and controllers. Library authors typically receive proxies via `usePlayer()` or `controller.value`. ### Via Controller (Lit/ReactiveElement) Controllers expose the proxy via `.value`: ```ts const controller = new PlayerController(this); controller.value; // UnknownPlayer (tracked proxy) ``` ## Implementation Both functions access `target[STORE_SYMBOL].features.has(feature.id)` at runtime: - **`hasFeature`** — Type guard that narrows the proxy to include feature's state and requests - **`getFeature`** — Returns same proxy typed to feature, with properties as `T | undefined` ## Cross-Framework Consistency The same API works in React and Lit: | Concept | React | Lit/ReactiveElement | | -------------------- | ------------------------------------ | --------------------------------------- | | Loosely typed player | `usePlayer()` → `UnknownPlayer` | `controller.value` → `UnknownPlayer` | | Type guard | `hasFeature(player, feature)` | `hasFeature(controller.value, feature)` | | Direct access | `getFeature(player, feature)` | `getFeature(controller.value, feature)` | | Throw on missing | `throwMissingFeature(feature, opts)` | `throwMissingFeature(feature, opts)` | ### Package Exports | Package | Exports | | ------------------- | ------------------------------------------------------------------------------------------- | | `@videojs/store` | `createProxy`, `hasFeature`, `getFeature`, `throwMissingFeature`, `subscribe`, `StoreProxy` | | `@videojs/core/dom` | `UnknownPlayer`, `UnknownMedia`, `UnknownPlayerStore`, `UnknownMediaStore` | | `@videojs/react` | Re-exports above + `usePlayer`, `useMedia`, `createPlayer` | | `@videojs/html` | Re-exports above + `PlayerController`, `MediaController`, `createPlayer` | ## Example: Mixing Required and Optional ```tsx export function TimeSlider() { const player = usePlayer(); // Required — throw if missing if (!hasFeature(player, features.time)) { throwMissingFeature(features.time, { displayName: 'TimeSlider' }); } // Optional — graceful degradation const playback = getFeature(player, features.playback); return ; } ```