Patterns: TypeScript
Interface vs type, the
I-prefix policy, enum vs string-literal union, React import form, and TS-error suppression. Load via thepatterns-componentscontext 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:
| Where | Convention | Examples |
|---|---|---|
Shared domain interfaces in src/interfaces/*.ts | IFoo (with I prefix) | IDefaultProperties, IDirectLineOptions, IWebchatAction, ITranscriptItem, ICustomNotification, IHCNButton, IBubbleWidgetStyleSetOptions |
| Component prop interfaces co-located with the component | FooProps (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 theIprefix. - 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 theIprefix (e.g.,IExtendedStyleOptions,ICSATStyle). Pure utility types insrc/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:
- Imports (top-level)
- Type aliases (
type Foo = ...) for unions used by interfaces below - Sub-interfaces (used by composition)
- 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.