Typescript

stream-chat-react-native and stream-chat-js are written in TypeScript and provide full type support.

The SDK supports custom fields on messages, channels, users, and more. The TypeScript implementation provides static type safety for both built-in and custom data.

Best Practices

  • Declare custom data types in a single .d.ts file to avoid conflicts.
  • Extend Default*Data types to stay compatible with future SDK changes.
  • Avoid using index signatures unless you truly need untyped custom fields.
  • Keep custom types narrow and explicit to improve autocomplete and safety.
  • Re-run type checks after SDK upgrades to catch breaking changes early.

Module augmentation and interface merging

Use module augmentation to define custom data shapes.

See the full list of customizable interfaces here, which are merged with your definitions.

The interfaces concerning the entities which can be extended will always be named in the format of Custom<uppercase-entity-name>Data.

Define these in a module declaration file (.d.ts), for example:

import { DefaultChannelData } from "stream-chat-react-native";

declare module "stream-chat" {
  interface CustomChannelData extends DefaultChannelData {
    "custom-channel-property": string;
  }

  interface CustomAttachmentData {
    id: string;
    size: number;
  }

  interface CustomCommandData {
    poke: unknown;
  }
}

Note that commands (CustomCommandType) are a special case which transforms from string union type (commandType: 'custom-command' | 'other-custom-command') to an interface from which only the keys would be used, the value type is not important and unknown would suffice.

The declaration adds custom-channel-property to Channel and keeps default properties. It also adds id and size to Attachment (without extending the default SDK attachment type). Command types add a new poke command.

You can place the .d.ts file anywhere; the TypeScript compiler will consume it.

Note: declaration merging does not work as an extension chain; it overrides unless you explicitly extend.

Default SDK properties

The React Native SDK defines additional properties on top of stream-chat and exposes them in default types you can reuse.

You may find a full list of these defaults here, as well as the module declaration used in the SDK itself. If interfaces are defined within your own application, they will override the interfaces in the SDK rather than extend them (and this is done by design).

The default SDK types are always going to be named in the format of Default<uppercase-entity-name>Data.

When defining custom data, you can use these defaults or omit them. In the example above, CustomChannelData respects SDK defaults while CustomAttachmentData does not.

If you do not wish to extend any of the default types but still require strong typing based on the SDK's types, you may do the following:

import {
  DefaultAttachmentData,
  DefaultChannelData,
  DefaultCommandData,
  DefaultEventData,
  DefaultMemberData,
  DefaultMessageData,
  DefaultPollData,
  DefaultPollOptionData,
  DefaultReactionData,
  DefaultThreadData,
  DefaultUserData,
} from "stream-chat-react-native";

declare module "stream-chat" {
  /* eslint-disable @typescript-eslint/no-empty-object-type */

  interface CustomAttachmentData extends DefaultAttachmentData {}

  interface CustomChannelData extends DefaultChannelData {}

  interface CustomCommandData extends DefaultCommandData {}

  interface CustomEventData extends DefaultEventData {}

  interface CustomMemberData extends DefaultMemberData {}

  interface CustomUserData extends DefaultUserData {}

  interface CustomMessageData extends DefaultMessageData {}

  interface CustomPollOptionData extends DefaultPollOptionData {}

  interface CustomPollData extends DefaultPollData {}

  interface CustomReactionData extends DefaultReactionData {}

  interface CustomThreadData extends DefaultThreadData {}

  /* eslint-enable @typescript-eslint/no-empty-object-type */
}

Even if many defaults are currently empty, extending them now protects you from future changes.

In that regard, it is important to add all of them here unless you have a very good reason not to.

Here is a more realistic example:

import {
  DefaultAttachmentData,
  DefaultChannelData,
} from "stream-chat-react-native";

declare module "stream-chat" {
  interface CustomChannelData extends DefaultChannelData {
    color: string;
    topic: "gardening" | "cats" | "f1";
  }

  interface CustomUserData {
    nickname: string;
  }

  interface CustomMessageData {
    isSecret: boolean;
  }

  interface CustomAttachmentData extends DefaultAttachmentData {
    lat?: string;
    lon?: string;
  }

  interface CustomReactionData {
    // Turn off type checks for custom properties
    [key: string]: unknown;
  }
}

In this case, your custom data will be properly typed:

let channel: Channel | undefined;
console.log(channel?.data?.topic);

let user: User | undefined;
console.log(user?.nickname);

let message: StreamMessage | undefined;
console.log(message?.isSecret);

let attachment: Attachment | undefined;
console.log(attachment?.lat);
console.log(attachment?.lon);

let reaction: Reaction | undefined;
// CustomReactionData allows any key on Reaction
console.log(reaction?.foo);

Any types you don't define fall back to the stream-chat defaults.

Disabling type checks

If you do not want type checks for custom properties on a specific entity, you can define the custom interface like this:

interface CustomMessageData {
  // Turn off type checks for custom properties
  [key: string]: unknown;
}

This allows any custom keys.