Skip to main content

Patterns: TypeScript

Interface vs type, the I-prefix policy, enum vs string-literal union, React import form, and TS-error suppression. Load via the patterns-components context group.

interface vs type

Use interface for object shapes, type for unions and utility types. The src/interfaces/ directory contains roughly 31 interface declarations and 5 type aliases; the split tracks this rule cleanly.

Use interface when declaring an object shape:

export interface IDefaultProperties {
botDivId: string;
apiUrl: string;
widgetStyleSet: IWidgetStyleSet;
// ...
}

Use type for:

  • String-literal unions (style discriminators): type CSATStyle = "numbers" | "stars" | "emotions" (src/interfaces/csat.ts).
  • Intersection / extension with an external library type: type ExtendedStyleOptions = StyleOptions & { ... }.
  • Utility / mapped types: Without<T, U>, XOR<T, U> (src/interfaces/utils.ts).

Interfaces compose better when extended (extends) and produce clearer error messages on type mismatches; reach for type only when the value being typed is genuinely not an object shape.

The I prefix policy

The codebase uses two different conventions for two different kinds of types, and the rule is positional:

WhereConventionExamples
Shared domain interfaces in src/interfaces/*.tsIFoo (with I prefix)IDefaultProperties, IDirectLineOptions, IWebchatAction, ITranscriptItem, ICustomNotification, IHCNButton, IBubbleWidgetStyleSetOptions
Component prop interfaces co-located with the componentFooProps (no prefix)BackButtonDecoratorProps, CloseIconProps, CustomNotificationProps, RatingFormPopupDecoratorProps, ListeningOverlayProps

Why two rules: src/interfaces/ is the cross-domain registry — interfaces here flow through the codebase and are imported from many call sites. The I prefix disambiguates them from value identifiers that often share the same name (IUserInfo vs a userInfo variable). Component prop interfaces, by contrast, are co-located with the component, used once, and never share a name with a runtime value.

Rule for new code

  • New interface in src/interfaces/? Use the I prefix.
  • New component prop interface in the same file as the component? No prefix; suffix with Props.
  • New utility type or string-literal union in src/interfaces/? Treat as a domain interface — use the I prefix (e.g., IExtendedStyleOptions, ICSATStyle). Pure utility types in src/interfaces/utils.ts (Without, XOR) are exempt because they are not domain types.

Existing outliers

The convention is partially observed in the codebase: 11 of the 25+ exported interfaces in src/interfaces/ already have the I prefix; the remaining ~14 do not. Outliers are tracked for incremental rename in .agent-files/tasks/20260428_hbf-webchat-pattern-migration.md. Examples currently missing the prefix: UserInfo, ClientPostbackMessage, CSATValue, RatingFormProps (in src/interfaces/ratingForm.ts — note this is a shared interface, not a component prop), MarkdownOptions, InitConfigurations, HBFWebchatWindow.

Enum vs String-Literal Union

TypeScript enum for closed sets shared across multiple files. All enums live in src/interfaces/enums.ts (the central registry):

// src/interfaces/enums.ts
export enum DIRECT_LINE { /* 6 action-type strings */ }
export enum WEBCHAT { /* 9 action-type strings */ }
export enum LiveChatEventType { /* 13 event types */ }
export enum SendBoxState { ENABLED, DISABLED }
export enum ChatMessageType { GENERIC, LIVECHAT }
export enum HeaderStyle { CENTER, LEFT }

One exception: NotificationLevels is in src/interfaces/CustomNotificationInterfaces.ts because it is co-located with the ICustomNotification interface that uses it. This is the single tolerated exception; new enums go in enums.ts.

String-literal union for style discriminators that live next to a single domain interface:

// src/interfaces/csat.ts
export type CSATStyle = "numbers" | "stars" | "emotions";

// src/interfaces/ratingForm.ts
export type RatingFormStyle = "numbers" | "stars";

These are open/closed at the type level but visually look like configuration values; type keeps them lightweight without an enum's runtime object. Use this form when:

  • The values are user-visible style choices configurable from botPreferences.
  • The values are referenced only in one or two domain files.
  • You do not need to iterate over them at runtime (no Object.values(MyEnum) pattern).

If any of those is false, use a TS enum in src/interfaces/enums.ts.

React import form

import React from "react";          // ✅ default form (preferred)
import * as React from "react"; // ❌ namespace form (legacy)

The codebase is split exactly 50/50 between the two forms (14 files each). The default form is the codified direction; new files use it. Existing namespace-form files are inventoried in the migration task.

React.Component, React.ReactNode, React.FC, etc. are all accessible under the default import — modern TypeScript exposes the React namespace either way, so React.Component works after import React from "react".

TS error suppression

Prefer @ts-expect-error over @ts-ignore for new code:

// ✅ new code: forces removal when the underlying type issue is fixed
// @ts-expect-error Wrong type from MS WebChat
foo.someBadlyTypedProp = bar;

// ⚠️ existing pattern: silently passes when the issue is fixed (rot risk)
// @ts-ignore Wrong type from MS WebChat
foo.someBadlyTypedProp = bar;

The codebase currently has 12 occurrences of @ts-ignore and zero @ts-expect-error. This is a known anti-pattern — @ts-ignore rots silently when the underlying type gets fixed, while @ts-expect-error errors at compile time the moment the suppression is no longer needed.

Always include a reason after the directive:

// @ts-expect-error <reason>

@ts-ignore is not part of the cleanup migration task (the task scope is limited to interface renames and React imports), but flagged in anti-patterns.md so reviewers can catch new instances.

Interface declaration order in domain files

Within a src/interfaces/*.ts file, conventional order is:

  1. Imports (top-level)
  2. Type aliases (type Foo = ...) for unions used by interfaces below
  3. Sub-interfaces (used by composition)
  4. The main exported interface(s)

Example: src/interfaces/botProperties.ts declares helper types (ExtendedStyleOptions, UserInfo, IBubbleWidgetStyleSetOptions, IEmbedWidgetStyleSetOptions, etc.) before the main IDefaultProperties interface that composes them.