# 🛠️ Helpers

> TypeScript utilities, React components, and hooks for building robust Telegraph components.

![Telegraph by Knock](https://github.com/knocklabs/telegraph/assets/29106675/9b5022e3-b02c-4582-ba57-3d6171e45e44)

[![npm version](https://img.shields.io/npm/v/@telegraph/helpers.svg)](https://www.npmjs.com/package/@telegraph/helpers)
[![minzipped size](https://img.shields.io/bundlephobia/minzip/@telegraph/helpers)](https://bundlephobia.com/result?p=@telegraph/helpers)
[![license](https://img.shields.io/npm/l/@telegraph/helpers)](https://github.com/knocklabs/telegraph/blob/main/LICENSE)

## Installation

```bash
npm install @telegraph/helpers
```

> **Note**: This package contains TypeScript utilities and React helpers. No stylesheets required.

## Quick Start

```tsx
import { Button } from "@telegraph/button";
import {
  PolymorphicProps,
  RefToTgphRef,
  TgphElement,
  TgphSlot,
  VisuallyHidden,
  callLegacyDismissHandlers,
  createTgphBaseUIRender,
  getBaseUIMotionOffset,
  useControllableState,
  useDeterminateState,
} from "@telegraph/helpers";

// Type-safe polymorphic component
type ButtonProps<T extends TgphElement> = PolymorphicProps<T> & {
  variant?: "solid" | "outline";
};

// Hook for loading states with minimum duration
const state = useDeterminateState({
  value: isLoading ? "loading" : "idle",
  determinateValue: "loading",
  minDurationMs: 1000,
});

// Base UI render bridge for Telegraph components
const renderTrigger = createTgphBaseUIRender(<Button>Open</Button>);

// Controlled/uncontrolled state bridge for component primitives
const [open, setOpen] = useControllableState({
  prop: openProp,
  defaultProp: false,
  onChange: onOpenChange,
});

// Legacy Radix-style dismissal and animation compatibility for Base UI wrappers
const shouldCancelDismiss = callLegacyDismissHandlers(eventDetails, {
  onEscapeKeyDown,
});
const initial = { opacity: 0, ...getBaseUIMotionOffset(side) };
```

## API Reference

### Type Utilities

#### `Required<T, K>`

Make specific properties of a type required.

```tsx
import { Required } from "@telegraph/helpers";

type User = {
  name?: string;
  email?: string;
  age?: number;
};

// Make name and email required
type UserWithRequiredFields = Required<User, "name" | "email">;
// Result: { name: string; email: string; age?: number; }
```

#### `Optional<T, K>`

Make specific properties of a type optional.

```tsx
import { Optional } from "@telegraph/helpers";

type User = {
  name: string;
  email: string;
  age: number;
};

// Make age optional
type UserWithOptionalAge = Optional<User, "age">;
// Result: { name: string; email: string; age?: number; }
```

#### `RemappedOmit<T, K>`

Enhanced version of TypeScript's `Omit` that ensures complete field removal.

```tsx
import { RemappedOmit } from "@telegraph/helpers";

type User = {
  id: string;
  name: string;
  password: string;
};

// Remove sensitive fields
type PublicUser = RemappedOmit<User, "password">;
// Result: { id: string; name: string; }
```

### Polymorphic Component Types

#### `TgphElement`

Type alias for `React.ElementType` used throughout Telegraph.

```tsx
import { TgphElement } from "@telegraph/helpers";

type ComponentProps<T extends TgphElement> = {
  as?: T;
  children: React.ReactNode;
};
```

#### `TgphComponentProps<T>`

Type alias for `React.ComponentProps<T>`.

```tsx
import { TgphComponentProps } from "@telegraph/helpers";

type ButtonProps = TgphComponentProps<"button"> & {
  variant?: string;
};
```

#### `AsProp<C>`

Type for the `as` prop used in polymorphic components.

```tsx
import { AsProp } from "@telegraph/helpers";

type BaseProps = AsProp<React.ElementType> & {
  children: React.ReactNode;
};
```

#### `PolymorphicProps<E>`

Props type for polymorphic components. It declares `as`, `children`,
`className` and `style`, and adds the props of the element `E` renders.

The element props are dropped when `E` is unresolved. An unresolved `E` is the
whole `React.ElementType` union, and `Omit<ComponentProps<ElementType>, "as">`
is `{ [x: string]: any }`. That index signature turns off prop checking on
every component that inherits it. See the "Type checking" section below.

```tsx
import { PolymorphicProps, TgphElement } from "@telegraph/helpers";

type BoxProps<T extends TgphElement> = PolymorphicProps<T> & {
  padding?: string;
  margin?: string;
};

const Box = <T extends TgphElement = "div">({
  as,
  padding,
  margin,
  ...props
}: BoxProps<T>) => {
  const Component = as || "div";
  return <Component style={{ padding, margin }} {...props} />;
};

// Usage examples:
<Box>Default div</Box>
<Box as="section">Semantic section</Box>
<Box as="button" onClick={() => {}}>Button element</Box>
```

#### `PolymorphicPropsWithTgphRef<E, R>`

Polymorphic props with Telegraph-specific ref handling.

```tsx
import { PolymorphicPropsWithTgphRef, TgphElement } from "@telegraph/helpers";

type InputProps<T extends TgphElement> = PolymorphicPropsWithTgphRef<
  T,
  HTMLInputElement
> & {
  placeholder?: string;
};

const Input = <T extends TgphElement = "input">({
  as,
  tgphRef,
  ...props
}: InputProps<T>) => {
  const Component = as || "input";
  return <Component ref={tgphRef} {...props} />;
};
```

#### `PropsWithAs<C, P>`

Utility for combining element props with custom props.

```tsx
import { PropsWithAs } from "@telegraph/helpers";

type CustomButtonProps = PropsWithAs<
  "button",
  {
    variant: "primary" | "secondary";
    loading?: boolean;
  }
>;

const CustomButton = ({
  as: Component = "button",
  variant,
  loading,
  ...props
}: CustomButtonProps) => {
  return <Component disabled={loading} data-variant={variant} {...props} />;
};
```

## React Components

### `RefToTgphRef`

Component for handling ref forwarding between external libraries and Telegraph
components.

```tsx
import { Button } from "@telegraph/button";
import { RefToTgphRef } from "@telegraph/helpers";

// Use when an external primitive passes a React `ref` to a single child,
// but the Telegraph child expects `tgphRef`.
<RefToTgphRef>
  <Button>Open</Button>
</RefToTgphRef>;
```

#### Use Cases

**With Base UI render props:**

```tsx
import { Popover } from "@base-ui/react/popover";
import { Button } from "@telegraph/button";
import { createTgphBaseUIRender } from "@telegraph/helpers";

const PopoverExample = () => (
  <Popover.Root>
    <Popover.Trigger render={createTgphBaseUIRender(<Button>Open</Button>)} />
    <Popover.Portal>
      <Popover.Positioner>
        <Popover.Popup>{/* Popover content */}</Popover.Popup>
      </Popover.Positioner>
    </Popover.Portal>
  </Popover.Root>
);
```

**With Form Libraries:**

```tsx
import { RefToTgphRef } from "@telegraph/helpers";
import { Input } from "@telegraph/input";
import { Controller, useForm } from "react-hook-form";

const FormExample = () => {
  const { control } = useForm();

  return (
    <Controller
      name="email"
      control={control}
      render={({ field }) => (
        <RefToTgphRef>
          <Input {...field} placeholder="Email" />
        </RefToTgphRef>
      )}
    />
  );
};
```

### `TgphSlot`

Component for merging wrapper props into a single child element while preserving
native refs, custom `forwardRef` children, and Telegraph `tgphRef` children.

```tsx
import { TgphSlot } from "@telegraph/helpers";

<TgphSlot data-state="open" className="trigger">
  <button type="button">Open</button>
</TgphSlot>;
```

### `VisuallyHidden`

Component for rendering accessible text that should remain available to assistive
technology without being visible in the UI.

```tsx
import { VisuallyHidden } from "@telegraph/helpers";
import { Text } from "@telegraph/typography";

<VisuallyHidden>
  <Text as="label" htmlFor="search">
    Search
  </Text>
</VisuallyHidden>;
```

Use `asChild` when the hidden styles need to be applied directly to a specific
element.

```tsx
<VisuallyHidden asChild>
  <label htmlFor="email">Email</label>
</VisuallyHidden>
```

### `createTgphBaseUIRender`

Helper for adapting Base UI `render` callbacks to Telegraph components that
use `tgphRef`.

```tsx
import { Popover } from "@base-ui/react/popover";
import { Button } from "@telegraph/button";
import { createTgphBaseUIRender } from "@telegraph/helpers";

const PopoverExample = () => (
  <Popover.Root>
    <Popover.Trigger
      render={createTgphBaseUIRender(<Button>Open Popover</Button>)}
    />
    <Popover.Portal>
      <Popover.Positioner>
        <Popover.Popup>{/* Popover content */}</Popover.Popup>
      </Popover.Positioner>
    </Popover.Portal>
  </Popover.Root>
);
```

The helper accepts a React element or a state callback. It preserves Base UI
props, event handlers, class names, styles, native refs, custom `forwardRef`
children, and Telegraph `tgphRef` children. When the child already has its own
`tgphRef`, the helper composes it with the Base UI ref so both refs receive the
same rendered element.

```tsx
import { Popover } from "@base-ui/react/popover";
import { Button } from "@telegraph/button";
import { createTgphBaseUIRender } from "@telegraph/helpers";
import type { ComponentProps } from "react";

<Popover.Trigger
  render={createTgphBaseUIRender<ComponentProps<"button">, { open: boolean }>(
    (state) => (
      <Button>{state.open ? "Close" : "Open"}</Button>
    ),
  )}
/>;
```

### `inferTriggerNativeButton`

Resolves the `nativeButton` value for a Base UI trigger. Explicit values
normally win; a recognized polymorphic component can override one when its
props force a known render target, such as a disabled Telegraph Button coercing
an anchor to a native button. Without an explicit value, the helper infers from
intrinsic and recognized polymorphic children and preserves Base UI's default
for unknown components.

```tsx
import { inferTriggerNativeButton } from "@telegraph/helpers";

const nativeButton = inferTriggerNativeButton({
  asChild,
  children,
  nativeButton: nativeButtonProp,
});
```

Polymorphic components can define a resolver with
`defineNativeButtonResolver`. The resolver uses a global symbol, so it
continues to work when consumers and wrappers load different package instances.

### Base UI Compatibility Utilities

Small helpers for preserving legacy Radix-style Telegraph behavior while
wrapping Base UI primitives.

```tsx
import {
  callLegacyDismissHandlers,
  getBaseUIMotionOffset,
  getBaseUIPositionerVisibilityStyle,
} from "@telegraph/helpers";

const shouldCancelDismiss = callLegacyDismissHandlers(eventDetails, {
  onEscapeKeyDown,
  onPointerDownOutside,
});

const initial = {
  opacity: 0,
  ...getBaseUIMotionOffset(side),
};

const positionerStyle = getBaseUIPositionerVisibilityStyle({
  anchorHidden,
  hideWhenDetached,
  zIndex: "var(--tgph-zIndex-tooltip)",
});
```

## React Hooks

### `useControllableState`

Hook for component primitives that support both controlled and uncontrolled
usage.

```tsx
import { useControllableState } from "@telegraph/helpers";

const [open, setOpen] = useControllableState({
  prop: openProp,
  defaultProp: false,
  onChange: onOpenChange,
});
```

The hook mirrors Telegraph's controlled prop conventions:

- `prop` controls the state when it is not `undefined`.
- `defaultProp` seeds uncontrolled state.
- `onChange` is called only when the next value differs from the current value.
- Updater functions are supported, matching React's `setState` ergonomics.

### `useDeterminateState`

Hook for managing state transitions with minimum duration guarantees.

```tsx
import { useDeterminateState } from "@telegraph/helpers";

const useDeterminateState = <T>({
  value: T;                    // Current value
  determinateValue: T;         // Value that should persist for minimum duration
  minDurationMs?: number;      // Minimum duration (default: 1000ms)
}): T
```

#### Basic Usage

```tsx
import { useDeterminateState } from "@telegraph/helpers";
import { useState } from "react";

const LoadingButton = () => {
  const [isLoading, setIsLoading] = useState(false);

  // Ensure loading state persists for at least 1 second
  const buttonState = useDeterminateState({
    value: isLoading ? "loading" : "idle",
    determinateValue: "loading",
    minDurationMs: 1000,
  });

  const handleClick = async () => {
    setIsLoading(true);
    try {
      await someAsyncOperation();
    } finally {
      setIsLoading(false); // Will transition back after minimum duration
    }
  };

  return (
    <button onClick={handleClick} disabled={buttonState === "loading"}>
      {buttonState === "loading" ? "Loading..." : "Click me"}
    </button>
  );
};
```

#### Advanced Usage

```tsx
import { useDeterminateState } from "@telegraph/helpers";

type FormState = "idle" | "submitting" | "success" | "error";

const FormWithFeedback = () => {
  const [formState, setFormState] = useState<FormState>("idle");

  // Ensure success/error states are visible for at least 2 seconds
  const displayState = useDeterminateState({
    value: formState,
    determinateValue:
      formState === "success" || formState === "error" ? formState : "idle",
    minDurationMs: 2000,
  });

  const handleSubmit = async () => {
    setFormState("submitting");
    try {
      await submitForm();
      setFormState("success");
      // Auto-reset after success is shown
      setTimeout(() => setFormState("idle"), 2500);
    } catch (error) {
      setFormState("error");
      setTimeout(() => setFormState("idle"), 2500);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <button disabled={displayState === "submitting"}>
        {displayState === "submitting" && "Submitting..."}
        {displayState === "success" && "✓ Saved!"}
        {displayState === "error" && "✗ Error"}
        {displayState === "idle" && "Save"}
      </button>
    </form>
  );
};
```

## Advanced Usage

### Creating Polymorphic Components

```tsx
import { forwardRef } from "react";
import { PolymorphicPropsWithTgphRef, TgphElement } from "@telegraph/helpers";

type TextProps<T extends TgphElement> = PolymorphicPropsWithTgphRef<T, HTMLElement> & {
  size?: "small" | "medium" | "large";
  weight?: "normal" | "bold";
  color?: string;
};

const Text = forwardRef<HTMLElement, TextProps<TgphElement>>(
  <T extends TgphElement = "span">({
    as,
    size = "medium",
    weight = "normal",
    color,
    tgphRef,
    style,
    ...props
  }: TextProps<T>, ref) => {
    const Component = as || "span";

    return (
      <Component
        ref={tgphRef || ref}
        style={{
          fontSize: size === "small" ? "12px" : size === "large" ? "18px" : "14px",
          fontWeight: weight === "bold" ? "bold" : "normal",
          color,
          ...style,
        }}
        {...props}
      />
    );
  }
);

// Usage examples:
<Text>Default span text</Text>
<Text as="p" size="large" weight="bold">Paragraph text</Text>
<Text as="h1" color="blue">Heading text</Text>
<Text as={Link} href="/about">Link text</Text>
```

### Type-Safe Component APIs

```tsx
import { Required, Optional, TgphComponentProps } from "@telegraph/helpers";

// Base props with all optional styling
type BaseCardProps = {
  padding?: string;
  shadow?: boolean;
  rounded?: boolean;
  background?: string;
};

// Card variant that requires certain props
type PrimaryCardProps = Required<BaseCardProps, "background"> & {
  variant: "primary";
};

// Card variant with some optional props
type SecondaryCardProps = Optional<BaseCardProps, "padding"> & {
  variant: "secondary";
};

type CardProps = (PrimaryCardProps | SecondaryCardProps) & {
  children: React.ReactNode;
};

const Card = ({ variant, children, ...props }: CardProps) => {
  if (variant === "primary") {
    // TypeScript knows `background` is required here
    return <div style={{ background: props.background }} />;
  }

  // Handle secondary variant
  return <div>{children}</div>;
};

// Usage:
<Card variant="primary" background="white">Content</Card> // ✓ Valid
<Card variant="primary">Content</Card> // ✗ TypeScript error - missing background
```

### Integration with External Libraries

```tsx
import { Popover } from "@base-ui/react/popover";
import { Button } from "@telegraph/button";
import { PolymorphicProps, createTgphBaseUIRender } from "@telegraph/helpers";
import type { ReactNode } from "react";

type BasePopoverProps = PolymorphicProps<"div"> & {
  content: ReactNode;
};

const BasePopover = ({ children, content, ...props }: BasePopoverProps) => (
  <Popover.Root>
    <Popover.Trigger
      render={createTgphBaseUIRender(<Button>{children}</Button>)}
    />
    <Popover.Portal>
      <Popover.Positioner>
        <Popover.Popup {...props}>{content}</Popover.Popup>
      </Popover.Positioner>
    </Popover.Portal>
  </Popover.Root>
);
```

## TypeScript

### Utility Type Examples

```tsx
import { Optional, RemappedOmit, Required } from "@telegraph/helpers";

// API response type
type User = {
  id: string;
  name: string;
  email: string;
  avatar?: string;
  createdAt: Date;
  updatedAt: Date;
};

// Create user payload (remove server-generated fields)
type CreateUserPayload = RemappedOmit<User, "id" | "createdAt" | "updatedAt">;

// Update user payload (make all fields optional except id)
type UpdateUserPayload = Required<
  Optional<User, "name" | "email" | "avatar">,
  "id"
>;

// Public user (safe for client-side)
type PublicUser = RemappedOmit<User, "email">;
```

### Component Prop Patterns

```tsx
import {
  PolymorphicProps,
  PolymorphicPropsWithTgphRef,
  TgphComponentProps,
  TgphElement,
} from "@telegraph/helpers";

// Pattern 1: Simple polymorphic component
type SimpleProps<T extends TgphElement> = PolymorphicProps<T> & {
  variant?: string;
};

// Pattern 2: Component with ref forwarding
type WithRefProps<T extends TgphElement> = PolymorphicPropsWithTgphRef<
  T,
  HTMLElement
> & {
  variant?: string;
};

// Pattern 3: Extending native element props
type NativeProps = TgphComponentProps<"button"> & {
  loading?: boolean;
};

// Pattern 4: Conditional props based on variant
type ConditionalProps<T extends TgphElement> = PolymorphicProps<T> &
  (
    | { variant: "icon"; icon: React.ComponentType; label?: never }
    | { variant: "text"; label: string; icon?: never }
  );
```

## Best Practices

### Type Utility Guidelines

1. **Use `RemappedOmit` over `Omit`**: Ensures complete field removal
2. **Prefer `Required`/`Optional` for partial updates**: More explicit than `Partial`
3. **Use `PolymorphicProps` for flexible components**: Enables `as` prop pattern
4. **Always provide default `TgphElement`**: Improves TypeScript inference

### Component Development

1. **Use `createTgphBaseUIRender` with Base UI `render` props**: Preserves Base UI props while forwarding refs to Telegraph `tgphRef`
2. **Use `TgphSlot` for single-child prop composition**: Preserves native refs and `tgphRef`
3. **Use `RefToTgphRef` with external libraries**: Ensures ref compatibility
4. **Implement `useDeterminateState` for loading states**: Improves UX with minimum durations
5. **Type polymorphic components properly**: Use appropriate helper types
6. **Test type constraints**: Verify TypeScript catches errors correctly

## Type checking

Telegraph components reject props they do not declare. Two cases surprise
people, so they are written out here.

### A `data-*` key alone in a nested prop bag

JSX exempts hyphenated **attributes** from excess-property checks, so setting
one on a component always works. An object literal gets no such exemption, and
several Telegraph components take nested prop bags such as `textProps`.

Two different checks apply. A fresh object literal gets excess-property
checking. A variable does not, but it hits weak-type detection when it shares
no property with an all-optional target.

```tsx
const dataAttrs = { "data-testid": "x" };
const bag = { size: "2" as const, "data-testid": "x" };

<Input data-testid="x" />                                // fine, an attribute
<Input textProps={{ size: "2", "data-testid": "x" }} />  // fails, fresh literal
<Input textProps={{ size: "2", ...dataAttrs }} />        // fine, spreads are exempt
<Input textProps={bag} />                                // fine, not fresh
<Input textProps={{ ...dataAttrs }} />                   // fails, nothing in common
```

Any bag that carries one real prop passes either check.

### `tgphRef` must match the element

`tgphRef` is no longer typed as `any`. A ref whose element type does not match
what the component renders is an error. `Combobox.Trigger` renders a `button`,
so a `RefObject<HTMLDivElement>` on it now fails.

## References

- [TypeScript Handbook - Utility Types](https://www.typescriptlang.org/docs/handbook/utility-types.html)
- [React TypeScript Cheatsheet](https://react-typescript-cheatsheet.netlify.app/)

## Contributing

See our [Contributing Guide](../../CONTRIBUTING.md) for more details.

## License

MIT License - see [LICENSE](../../LICENSE) for details.
