@cio/question-types
Shared, framework-agnostic question type contracts for ClassroomIO exercise products.
This package is the source of truth for:
- Question type keys and DB id mappings
- Type metadata and capability flags
- Stable render contracts and list contracts
- Answer payload and serialization helpers
- Renderer registry helpers (without UI implementation)
- Form state primitives for question flows
It is intentionally pure TypeScript and contains no UI components.
Why this package exists
@cio/question-types centralizes exercise domain contracts so every app/package can use the same behavior:
apps/dashboarduses the same question keys/types as API and DB consumersapps/apican rely on stable question type IDs@cio/uican implement renderers against one shared contract- Future products (for example, forms/assessments) can reuse the same domain model
Package boundary
What belongs here:
- Domain constants and type-level contracts
- Pure mapping/lookup helpers
- Deterministic utilities (same input => same output)
- Serialization rules for answer payloads
What does not belong here:
- Svelte/React/Vue components
- Package-specific UI behavior
- Network calls
- DB query implementations
Architecture
flowchart LR
subgraph Product Apps
DASH[apps/dashboard]
API[apps/api]
FUTURE[future app e.g. forms]
end
subgraph Shared Packages
QT[@cio/question-types\ncontracts + registry + serializer]
UI[@cio/ui/custom/exercise-question\nrendering implementation]
DB[(DB question_types ids)]
end
DASH -->|question models, keys, lookups| QT
FUTURE -->|same contracts| QT
UI -->|imports types + registry helpers| QT
DASH -->|renders via props/contracts| UI
API -->|imports QUESTION_TYPE_IDS| QT
QT -->|stable id <-> key mapping| DB
DASH -->|answersByKey| QT
QT -->|serializeExerciseAnswerMap| API
Source layout
packages/question-types/src/
answer-serializer.ts
exercise-types.ts
form-state.ts
index.ts
question-answer-payload.ts
question-scoring-rules.ts
question-type-capabilities.ts
question-type-keys.ts
question-type-lookup.ts
question-type-registry.ts
question-type-types.ts
render-contract.ts
renderer-registry.ts
Supported question types
Registry source: src/question-type-registry.ts
| key | id | label | autoGradable | supportsPartialCredit | manualGradingRequired |
|---|---|---|---|---|---|
RADIO |
1 | Single answer | yes | no | no |
CHECKBOX |
2 | Multiple answers | yes | yes | no |
TEXTAREA |
3 | Paragraph | no | no | yes |
TRUE_FALSE |
4 | True/False | yes | no | no |
SHORT_ANSWER |
5 | Short answer | no | no | yes |
NUMERIC |
6 | Numeric answer | yes | yes | no |
FILL_BLANK |
7 | Fill in the blank | yes | yes | no |
FILE_UPLOAD |
8 | File upload | no | no | yes |
MATCHING |
9 | Matching | yes | yes | no |
ORDERING |
10 | Ordering | yes | yes | no |
HOTSPOT |
11 | Hotspot | yes | yes | no |
LINK |
12 | Links | no | no | yes |
Public API overview
1) Keys and metadata
QUESTION_TYPE_KEYQuestionTypeKeyQUESTION_TYPE_REGISTRYQUESTION_TYPE_IDSQUESTION_TYPE_BY_KEYQUESTION_TYPE_TYPENAME_TO_KEYQUESTION_TYPE_ID_TO_KEY
Lookup helpers:
getQuestionTypeByKey(key)getQuestionTypeByTypename(typename)getQuestionTypeById(id)getQuestionTypeId(key)
Capability helpers:
isAutoGradableQuestionType(key)supportsPartialCredit(key)requiresManualGrading(key)
2) Exercise rendering contracts
Core types (src/exercise-types.ts):
ExerciseQuestionModelExerciseQuestionOptionExerciseAnswerValueExerciseRendererMode(edit | take | preview | view)ExerciseRendererDefinition<TRenderer>ExerciseQuestionRendererProps
Render contracts (src/render-contract.ts):
ExerciseQuestionRenderContractExerciseQuestionListRenderContractExerciseQuestionImageUploadergetExerciseQuestionContractKey(question, fallbackIndex)
3) Renderer registry helpers (UI-agnostic)
From src/renderer-registry.ts:
ExerciseRendererRegistry<TRenderer>getRendererDefinition(registry, questionType, fallback)getRendererForMode(registry, questionType, mode, fallback)
Important behavior:
mode: 'view'resolves to'take'internally ingetRendererForMode.
4) Answer payload and serialization
Answer payload union (src/question-answer-payload.ts):
QuestionAnswerPayloadand per-type payload interfaces (RadioAnswerPayload,CheckboxAnswerPayload, etc.)
Serializer (src/answer-serializer.ts):
serializeExerciseAnswer(question, answer)serializeExerciseAnswerMap(questions, answersByKey)SerializedExerciseAnswer
5) Form state and scoring constants
Form state (src/form-state.ts):
ExerciseQuestionFormStatecreateExerciseQuestionFormState(initialQuestions)getQuestionStateKey(question, fallbackIndex)
Scoring constants (src/question-scoring-rules.ts):
QUESTION_SCORING_MODEATTEMPT_SCORING_RULESHOW_CORRECT_ANSWERS_POLICY
Deterministic contract guarantees
The following helpers are deterministic and should be treated as stable contracts:
getExerciseQuestionContractKeygetQuestionStateKeygetQuestionTypeById/getQuestionTypeIdgetRendererForModeserializeExerciseAnswer/serializeExerciseAnswerMap
If given the same inputs, they always return the same outputs.
Usage examples
Resolve question type from id
import { getQuestionTypeById } from '@cio/question-types';
const metadata = getQuestionTypeById(10);
// => { key: 'ORDERING', label: 'Ordering', ... }
Build a renderer registry in another package
import {
QUESTION_TYPE_KEY,
getRendererForMode,
type ExerciseRendererRegistry,
type ExerciseRendererDefinition
} from '@cio/question-types';
type Renderer = unknown;
const registry: ExerciseRendererRegistry<Renderer> = {
[QUESTION_TYPE_KEY.RADIO]: {
edit: 'RadioEditRenderer',
take: 'RadioTakeRenderer',
preview: 'RadioPreviewRenderer'
}
};
const fallback: ExerciseRendererDefinition<Renderer> = {
edit: 'FallbackEdit',
take: 'FallbackTake',
preview: 'FallbackPreview'
};
const renderer = getRendererForMode(registry, QUESTION_TYPE_KEY.RADIO, 'view', fallback);
// 'view' resolves to 'take'
Serialize answers for API submission
import { serializeExerciseAnswerMap } from '@cio/question-types';
const payload = serializeExerciseAnswerMap(questions, answersByKey);
// => Array<{ questionId, optionId?, answer?, payload? }>
Create question form state
import { createExerciseQuestionFormState } from '@cio/question-types';
const state = createExerciseQuestionFormState(initialQuestions);
Adding a new question type
See ADDING_A_QUESTION_TYPE.md for a step-by-step guide. At a high level:
Build
pnpm -C packages/question-types build
Notes for consumers
- Prefer importing keys/lookup helpers from this package instead of duplicating constants.
- Use
getExerciseQuestionContractKeyconsistently for stable answer maps. - Keep UI implementation in
@cio/ui; keep domain contracts in@cio/question-types.