Types
TypeScript type definitions exported by @tour-kit/surveys — survey config, question config, scoring results, context, events, queue, and position types
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` validatesScoring 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
}Related
- SurveysProvider — Uses SurveysProviderProps
- useSurvey — Returns SurveyState and SurveyConfig
- Scoring utilities — Produce NPSResult, CSATResult, CESResult
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.