@verisure-italy/funnel-types
v1.7.3
Published
Types for Funnel service
Readme
@verisure-italy/funnel-types
Technical README describing the type system and Zod schemas used to model Funnel, Funnel Path and Survey domain objects.
Purpose
This package centralizes strongly-typed, runtime-validated schemas for the Funnel service. It leverages Zod to provide:
- Runtime validation of incoming / persisted payloads
- Static TypeScript inference for compile‑time safety
- Discriminated unions for polymorphic structures (questions, routing conditions)
- Consistent error message keys for i18n / UI mapping
Consumers import only what they need; all symbols are re‑exported from src/index.ts for convenience.
Installation
In a workspace setup this is referenced via workspace:*. External usage:
pnpm add @verisure-italy/funnel-typesBuild Overview
The package is built with tsup to produce both ESM and CJS bundles (dist/index.js / dist/index.cjs) plus type declarations. Tree‑shaking is supported because Zod schemas are plain JS objects.
Scripts:
pnpm build– build once with d.tspnpm dev– watch mode
Core Enumerations & Basic Types
export const funnelTypes = ['home', 'business'] as const
export type FunnelType = 'home' | 'business'
export const questionTypes = ['choices', 'zip_code', 'phone', 'phone_number', 'text'] as const
export type QuestionType = 'choices' | 'zip_code' | 'phone' | 'phone_number' | 'text'
export const routingConditions = [
'exact', 'contains', 'starts_with', 'ends_with', 'out_of_area', 'match_all'
] as const
export type RoutingCondition =
| 'exact' | 'contains' | 'starts_with' | 'ends_with'
| 'out_of_area' | 'match_all'All enumerations have companion Zod schemas (funnelTypeSchema, questionTypeSchema, routingConditionSchema) that give both validation and TypeScript inference.
Modeling Strategy
The package prefers explicit discriminated unions using a type discriminator key to:
- Enforce different required fields per variant
- Narrow TS types automatically after refinement
- Avoid optional fields that are only relevant for subsets of variants
Routing Conditions
funnelRoutingSchema models conditional transitions between questions:
condition: z.discriminatedUnion('type', [
// value must be a non-empty string for these types
z.object({
type: z.enum(['exact', 'contains', 'starts_with', 'ends_with']),
value: z.string().min(1, 'value.required'),
}),
// value explicitly null when matching all
z.object({ type: z.literal('match_all'), value: z.null() }),
// boolean flag (default true) for out_of_area condition
z.object({ type: z.literal('out_of_area'), value: z.boolean().default(true) }),
])Semantics:
exact|contains|starts_with|ends_with:valueis a required non-empty string used for pattern matching.match_all:valueisnull, meaning unconditional match.out_of_area:valueis a boolean (defaults totrue) indicating routing based on geolocation failure / area exclusion.
Question Variants
funnelQuestionSchema discriminates on type and constrains options accordingly:
choicesoptions.responses: Array of{ label, value }with at least one entry and enforced uniquevalueset.
phone|phone_numberoptions.onlyMobile: Boolean flag (e.g., restrict to mobile numbers).
zip_code|textoptionsoptional and currently empty (placeholder for future extensions).
This mirrors a previous Yup schema but yields native TS inference without manual type duplication.
Timestamp Extension
Schemas representing persisted entities (funnelSchema, funnelPathSchema, surveySchema) extend timestampFieldsSchema from @verisure-italy/shared-types, appending createdAt, updatedAt, etc. This keeps temporal metadata consistent across services.
Exported Schemas & Types (Summary)
| Symbol | Kind | Purpose |
|--------|------|---------|
| funnelContextSchema | Zod Object | Minimal context definition (code/title) |
| funnelFlowQuestionSchema | Zod Object | Links context + question code in a flow |
| funnelFlowSchema | Zod Object | Ordered list of questions + default flag |
| funnelRoutingTargetSchema | Zod Object | A routing target (flow/question codes) |
| funnelRoutingSchema | Zod Object | Conditional transition definition |
| funnelQuestionSchema | Zod Discriminated Union | Polymorphic question model |
| funnelSchema | Zod Object | Entire funnel aggregate |
| funnelPathSchema | Zod Object | Path-specific configuration |
| surveyMetaSchema | Zod Object | Collected metadata (client, UA, etc.) |
| surveySchema | Zod Object | Submitted survey instance |
Each schema has a matching type (e.g., FunnelRouting) via z.infer<typeof schema>.
Usage Examples
Validating Input
import { funnelQuestionSchema } from '@verisure-italy/funnel-types'
const payload = {
code: 'entry-product',
script: 'choose_product',
description: null,
progress: 10,
type: 'choices',
options: {
responses: [
{ label: 'Alarm', value: 'alarm' },
{ label: 'Camera', value: 'camera' },
],
},
}
const parsed = funnelQuestionSchema.parse(payload) // Throws on validation error
// parsed inferred as FunnelQuestion (narrowed to choices variant)Narrowing by Discriminator
function handleQuestion(q: FunnelQuestion) {
switch (q.type) {
case 'choices':
// q.options.responses available and typed
break
case 'phone':
case 'phone_number':
// q.options.onlyMobile boolean
break
case 'zip_code':
case 'text':
// q.options possibly undefined
break
}
}Safe Parsing
import { funnelRoutingSchema } from '@verisure-italy/funnel-types'
const result = funnelRoutingSchema.safeParse(maybePayload)
if (!result.success) {
// result.error.format() can feed UI error mapping
}Error Message Strategy
Validation constraints include custom message keys (e.g., code.required, slug.invalid_format) rather than raw human text. This supports localization and consistent front-end mapping. Consumers should maintain a dictionary mapping these keys to user-facing strings.
Adding a New Question Type
- Add new literal to
questionTypesarray intypes.ts. - Extend discriminated union in
funnel-models.ts:- Define a new
const newQuestionSchema = baseQuestionSchema.extend({ type: z.literal('your_new_type'), options: ... }). - Append schema to the
funnelQuestionSchemadiscriminatedUnion array.
- Define a new
- Adjust any services relying on exhaustive
switch(q.type)statements. - Increment package version following semver (new type usually minor).
Adding a New Routing Condition
- Add literal to
routingConditionsintypes.ts. - Add corresponding object schema in the discriminated union in
funnelRoutingSchemawith propervaluevalidation. - Update control flow or matching logic in downstream services.
Extensibility Guidelines
- Prefer extending via
.extend()on a base schema to keep shared fields centralized. - Use
z.discriminatedUnionwhen future variants are anticipated; it scales better than unions requiring manual refinement checks. - Keep default values in schemas (e.g.,
default(false)) rather than injecting outside; ensures consistent shape after.parse().
Performance Considerations
Zod validation is synchronous and relatively fast for these small structures. For high-frequency validation (e.g., bulk survey imports) consider:
- Using
.safeParse()to avoid exception overhead. - Caching compiled schemas (already inherently cached by module system).
Interoperability
Because schemas are runtime objects, they can be used to:
- Generate OpenAPI (with additional tooling / transforms).
- Serialize validation metadata for front-end form builders.
- Provide consistent type guards across microservices.
Type Narrowing & Exhaustiveness
When switching on discriminators, enable TypeScript's --strict mode so non-exhaustive branches produce warnings. This protects against missing logic when adding new variants.
Import Paths
import {
funnelSchema,
funnelQuestionSchema,
funnelRoutingSchema,
Funnel,
FunnelQuestion,
FunnelRouting,
} from '@verisure-italy/funnel-types'All exports are flattened. Avoid deep imports (e.g., @verisure-italy/funnel-types/dist/...) to retain compatibility with future internal restructuring.
Testing Recommendations
Use unit tests to assert both success and failure cases:
expect(() => funnelQuestionSchema.parse(invalid)).toThrow()
expect(funnelQuestionSchema.parse(valid).type).toBe('choices')Focus on edge cases: empty responses, duplicate value, progress out of range, invalid routing condition/value combinations.
Versioning & Change Management
- Adding new optional fields: MINOR version bump
- Adding new required fields or changing validation: MAJOR version bump
- Documentation updates / internal refactors: PATCH
FAQ
Q: Why Zod over Yup?
A: Native TS inference, discriminated union support, simpler extension, no separate type duplication.
Q: Can I serialize schemas?
A: Zod does not serialize schemas directly; for doc generation integrate with zod-to-openapi or custom mappers.
Q: How do I handle unknown extra properties?
A: Use .strict() on object schemas if you want to forbid unknown keys (current schemas are permissive). Adjust as needed.
Roadmap Ideas
- Introduce
numberordatequestion types - Add internationalization metadata (labels per locale) to questions
- Provide OpenAPI generation helper
- Add helper functions for routing evaluation
License
Internal proprietary – published to npm with restricted usage under Verisure Italy organizational policies.
For questions or changes, open a PR referencing this README section you modify to keep documentation in sync.
