Skip to main content
userTourKit
@tour-kit/surveys

Types

TypeScript type definitions exported by @tour-kit/surveys — survey config, question config, scoring results, context, events, queue, and position types

domidex01Published

All types are exported from the package root. Import them as type-only:

import type {
  SurveyConfig,
  QuestionConfig,
  NPSResult,
  SurveysProviderProps,
} from '@tour-kit/surveys';

Survey types

SurveyConfig

Main configuration object for a survey.

interface SurveyConfig {
  id: string;
  type: SurveyType;
  displayMode: DisplayMode;
  priority?: SurveyPriority;
  title?: string;
  description?: string | ReactNode;
  questions: QuestionConfig[];
  frequency?: FrequencyRule;
  schedule?: Schedule;          // from @tour-kit/scheduling (optional peer)
  audience?: AudienceCondition[];
  globalCooldownDays?: number;
  samplingRate?: number;        // 0–1
  maxSnoozeCount?: number;
  snoozeDelayDays?: number;
  maxPerSession?: number;
  modalOptions?: ModalOptions;
  slideoutOptions?: SlideoutOptions;
  bannerOptions?: BannerOptions;
  popoverOptions?: PopoverOptions;
  metadata?: Record<string, unknown>;
  onShow?: () => void;
  onDismiss?: (reason: DismissalReason) => void;
  onComplete?: (responses: Map<string, AnswerValue>) => void;
  onAnswer?: (questionId: string, value: AnswerValue) => void;
}

SurveyState

Runtime state for a single survey, managed internally by SurveysProvider.

interface SurveyState {
  id: string;
  isActive: boolean;
  isVisible: boolean;
  isDismissed: boolean;
  isSnoozed: boolean;
  isCompleted: boolean;
  viewCount: number;
  lastViewedAt: Date | null;
  dismissedAt: Date | null;
  dismissalReason: DismissalReason | null;
  completedAt: Date | null;
  snoozeCount: number;
  snoozeUntil: Date | null;
  currentStep: number;
  responses: Map<string, AnswerValue>;
  /** Transient per-question validation errors — never persisted. */
  validationErrors: Map<string, string>;
}

SurveyType

type SurveyType = 'nps' | 'csat' | 'ces' | 'custom';

DisplayMode

type DisplayMode = 'popover' | 'modal' | 'slideout' | 'banner' | 'inline';

SurveyPriority

type SurveyPriority = 'critical' | 'high' | 'normal' | 'low';

FrequencyRule

type FrequencyRule =
  | 'once'
  | 'session'
  | 'always'
  | { type: 'times'; count: number }
  | { type: 'interval'; days: number };

DismissalReason

type DismissalReason =
  | 'close_button'
  | 'overlay_click'
  | 'escape_key'
  | 'snooze'
  | 'completed'
  | 'programmatic';

SurveyStorageAdapter

interface SurveyStorageAdapter {
  getItem(key: string): string | null | Promise<string | null>;
  setItem(key: string, value: string): void | Promise<void>;
  removeItem(key: string): void | Promise<void>;
}

Question types

QuestionConfig

interface QuestionConfig {
  id: string;
  type: QuestionType;
  text: string;
  description?: string;
  required?: boolean;
  placeholder?: string;
  ratingScale?: RatingScale;       // only for 'rating'
  options?: SelectOption[];         // only for 'single-select' | 'multi-select'
  maxLength?: number;               // only for 'text' | 'textarea'
  skipLogic?: SkipLogic[];
  validation?: (value: AnswerValue) => string | null;
}

QuestionType

type QuestionType =
  | 'rating'
  | 'text'
  | 'textarea'
  | 'single-select'
  | 'multi-select'
  | 'boolean';

AnswerValue

type AnswerValue = string | number | boolean | string[];

RatingScale

interface RatingScale {
  min: number;
  max: number;
  step?: number;
  labels?: {
    min?: string;
    max?: string;
  };
  style?: 'numeric' | 'stars' | 'emoji';
}

step controls the increment between rating options (default 1). The scale renders every value from min to max inclusive, stepping by step — so { min: 0, max: 10, step: 5 } renders exactly three options: 0, 5, 10.

<QuestionRating
  id="effort"
  label="How much effort did that take?"
  ratingScale={{ min: 0, max: 10, step: 5 }} // → 0, 5, 10
/>

SelectOption

interface SelectOption {
  value: string;
  label: string;
  disabled?: boolean;
}

SkipLogic

interface SkipLogic {
  questionId: string;
  condition: (answer: AnswerValue) => boolean;
  skipTo: string;
}

Validation

QuestionConfig.validation runs at the advance boundary (when you call nextQuestion), not on every keystroke. Return a string to block advancing and surface it as a field error; return null or undefined to allow the survey to move on. nextQuestion returns that same string (or null on success), and the error is also readable via getValidationError(surveyId, questionId) on the context — or validationError(questionId) from useSurvey. Errors are transient UI state and are never persisted, so a reload starts clean.

The value passed to validation is the answer recorded so far, which is undefined until the question is answered — that's the case to guard for a required field. Handle a falsy value rather than assuming a populated answer.

const question: QuestionConfig = {
  id: 'email',
  type: 'text',
  text: 'Work email',
  required: true,
  validation: (value) =>
    typeof value === 'string' && value.includes('@') ? null : 'Enter a valid email',
};

// In a question UI built on the headless hook:
const { nextQuestion, validationError } = useSurvey('signup');
const error = validationError('email'); // string while invalid, undefined once it passes

<input
  aria-invalid={error ? true : undefined}
  aria-describedby={error ? 'email-error' : undefined}
  onChange={(e) => answer('email', e.target.value)}
/>;
{error && <p id="email-error" role="alert">{error}</p>}
<button onClick={() => nextQuestion()}>Next</button>; // stays put until `value` validates

Scoring types

NPSResult

interface NPSResult {
  score: number;         // −100 to 100
  promoters: number;
  passives: number;
  detractors: number;
  promoterPct: number;
  passivePct: number;
  detractorPct: number;
  total: number;
  responses: number[];
}

CSATResult

interface CSATResult {
  score: number;         // 0–100 (percentage)
  positive: number;
  negative: number;
  total: number;
  threshold: number;
  responses: number[];
}

CESResult

interface CESResult {
  score: number;         // average response value
  easy: number;          // responses ≥ 5
  difficult: number;     // responses ≤ 3
  neutral: number;       // responses = 4
  total: number;
  responses: number[];
}

Context types

SurveysContextValue

interface SurveysContextValue {
  surveys: Map<string, SurveyState>;
  activeSurvey: string | null;
  queue: string[];
  register: (config: SurveyConfig) => void;
  unregister: (id: string) => void;
  show: (id: string) => void;
  hide: (id: string) => void;
  dismiss: (id: string, reason?: DismissalReason) => void;
  snooze: (id: string) => void;
  answer: (surveyId: string, questionId: string, value: AnswerValue) => void;
  /** Returns the validation error string when the current question blocks advance, else `null`. */
  nextQuestion: (surveyId: string) => string | null;
  prevQuestion: (surveyId: string) => void;
  complete: (surveyId: string) => void;
  reset: (id: string) => void;
  resetAll: () => void;
  getState: (id: string) => SurveyState | undefined;
  getValidationError: (surveyId: string, questionId: string) => string | undefined;
  getConfig: (id: string) => SurveyConfig | undefined;
  canShow: (id: string) => boolean;
}

SurveysProviderProps

interface SurveysProviderProps {
  children: ReactNode;
  surveys?: SurveyConfig[];
  queueConfig?: Partial<SurveyQueueConfig>;
  storage?: Storage | null;
  storageKey?: string;
  userContext?: Record<string, unknown>;
  globalCooldownDays?: number;
  samplingRate?: number;
  maxPerSession?: number;
  onSurveyShow?: (id: string) => void;
  onSurveyDismiss?: (id: string, reason: DismissalReason) => void;
  onSurveyComplete?: (id: string, responses: Map<string, AnswerValue>) => void;
  onSurveyAnswer?: (surveyId: string, questionId: string, value: AnswerValue) => void;
  onSurveySnooze?: (id: string) => void;
  onQuestionAnswered?: (surveyId: string, questionId: string, value: AnswerValue) => void;
  onScoreCalculated?: (
    surveyId: string,
    scoreType: 'nps' | 'csat' | 'ces',
    result: NPSResult | CSATResult | CESResult
  ) => void;
}

Events types

SurveyEventType

type SurveyEventType =
  | 'survey_registered'
  | 'survey_shown'
  | 'survey_dismissed'
  | 'survey_snoozed'
  | 'survey_completed'
  | 'survey_question_answered'
  | 'survey_score_calculated';

BaseSurveyEvent

interface BaseSurveyEvent {
  type: SurveyEventType;
  surveyId: string;
  surveyType: SurveyType;
  displayMode: DisplayMode;
  timestamp: number;
  metadata?: Record<string, unknown>;
}

SurveyEvent (discriminated union)

type SurveyEvent =
  | SurveyShownEvent
  | SurveyDismissedEvent
  | SurveySnoozedEvent
  | SurveyCompletedEvent
  | SurveyQuestionAnsweredEvent
  | SurveyScoreCalculatedEvent;

Individual event shapes:

interface SurveyShownEvent extends BaseSurveyEvent {
  type: 'survey_shown';
  viewCount: number;
  fromQueue: boolean;
}

interface SurveyDismissedEvent extends BaseSurveyEvent {
  type: 'survey_dismissed';
  reason: DismissalReason;
  viewDuration: number;
}

interface SurveySnoozedEvent extends BaseSurveyEvent {
  type: 'survey_snoozed';
  snoozeCount: number;
  snoozeUntil: Date;
}

interface SurveyCompletedEvent extends BaseSurveyEvent {
  type: 'survey_completed';
  viewDuration: number;
  responses: Map<string, AnswerValue>;
  completionRate: number;   // 0–100
}

interface SurveyQuestionAnsweredEvent extends BaseSurveyEvent {
  type: 'survey_question_answered';
  questionId: string;
  questionType: QuestionType;
  stepIndex: number;
}

interface SurveyScoreCalculatedEvent extends BaseSurveyEvent {
  type: 'survey_score_calculated';
  scoreType: 'nps' | 'csat' | 'ces';
  score: number;
}

Queue types

SurveyQueueConfig

interface SurveyQueueConfig {
  maxConcurrent: number;                          // default: 1
  priorityOrder: PriorityOrder;                   // default: 'priority'
  stackBehavior: StackBehavior;                   // default: 'queue'
  delayBetween: number;                           // ms, default: 500
  priorityWeights: Record<SurveyPriority, number>; // critical: 1000, high: 100, normal: 10, low: 1
  autoShow: boolean;                              // default: true
}

PriorityOrder

type PriorityOrder = 'priority' | 'fifo' | 'lifo';

StackBehavior

type StackBehavior = 'queue' | 'replace' | 'stack';

SurveyQueueItem

interface SurveyQueueItem {
  id: string;
  priority: SurveyPriority;
  addedAt: number;
  weight: number;
  sequence: number;
}

Variant option types

ModalOptions

interface ModalOptions {
  size?: 'sm' | 'md' | 'lg';
  closeOnOverlayClick?: boolean;
  closeOnEscape?: boolean;
  showCloseButton?: boolean;
}

SlideoutOptions

interface SlideoutOptions {
  position?: SlideoutPosition;
  size?: 'sm' | 'md' | 'lg';
  closeOnOverlayClick?: boolean;
  closeOnEscape?: boolean;
  showCloseButton?: boolean;
}

BannerOptions

interface BannerOptions {
  position?: BannerPosition;
  sticky?: boolean;
  dismissable?: boolean;
  intent?: 'info' | 'feedback';
}

PopoverOptions

interface PopoverOptions {
  position?: PopoverPosition;
  offset?: number;
  showCloseButton?: boolean;
}

Position types

SlideoutPosition

type SlideoutPosition = 'left' | 'right';

BannerPosition

type BannerPosition = 'top' | 'bottom';

PopoverPosition

type PopoverPosition = 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';

Audience types

AudienceCondition

interface AudienceCondition {
  type: 'user_property' | 'segment' | 'feature_flag' | 'custom';
  key: string;
  operator:
    | 'equals'
    | 'not_equals'
    | 'contains'
    | 'not_contains'
    | 'in'
    | 'not_in'
    | 'exists'
    | 'not_exists';
  value?: unknown;
}

Component Props

The headless API exposes per-question render-prop callbacks (Question*RenderProps); the styled components below export their own Question*Props interfaces. Use the *Props types when wrapping or theming the styled components, and the *RenderProps types when building custom UI on top of the headless primitives.

QuestionRatingProps

interface QuestionRatingProps {
  id: string
  min?: number   // default 0
  max?: number   // default 10
  style?: 'numeric' | 'stars' | 'emoji'
  size?: 'sm' | 'md' | 'lg'
  value?: number | null
  onChange?: (value: number) => void
  label: string
  lowLabel?: string
  highLabel?: string
  isRequired?: boolean
  emojiMap?: Record<number, string>
  className?: string
}

QuestionTextProps

interface QuestionTextProps {
  id: string
  mode?: 'text' | 'textarea'
  value?: string
  onChange?: (value: string) => void
  placeholder?: string
  maxLength?: number
  showCharacterCount?: boolean
  rows?: number
  isRequired?: boolean
  label: string
  isAutoFocus?: boolean
  size?: 'sm' | 'md' | 'lg'
  className?: string
}

QuestionSelectProps

interface QuestionSelectProps {
  id: string
  mode?: 'single' | 'multi'
  options: SelectOption[]
  value?: string | string[]
  onChange?: (value: string | string[]) => void
  label: string
  isRequired?: boolean
  size?: 'sm' | 'md' | 'lg'
  className?: string
}

QuestionBooleanProps

interface QuestionBooleanProps {
  id: string
  value?: boolean | null
  onChange?: (value: boolean) => void
  label: string
  yesLabel?: string
  noLabel?: string
  isRequired?: boolean
  size?: 'sm' | 'md' | 'lg'
  className?: string
}

SurveyProgressProps

interface SurveyProgressProps {
  current: number
  total: number
  mode?: 'text' | 'bar' | 'both'
  labelTemplate?: string
  size?: 'sm' | 'md' | 'lg'
  className?: string
}

Free & open source

Ship onboarding, not config.

npm i @tour-kit/core is MIT and free. The Pro packages work unlicensed too — a one-time $99 license removes the production watermark when you ship.

MIT-licensed — no signup, no credit card. Pay once, only when you ship.