Skip to content

noctcore-contracts/wire-message-naming

A message-schema’s type discriminant must be the kebab-case of its const name minus its role suffix. 🔧

Recommended preset: error · Autofix: yes · Suggestions: no · Type information: not needed

When wire messages are modelled as zod objects with a type: z.literal('…') discriminant, the const name and the on-the-wire discriminant are two spellings of the same fact. Deriving one from the other by convention (TaskCompletedEvent'task-completed') removes a whole class of copy-paste drift where the const is renamed but the literal is not.

For every export const whose name ends in a role suffix (default Event / Command / Query) and whose zod object declares a type: z.literal('…') property, the literal must equal kebab(constName minus the role suffix). A mismatch is reported and autofixed to the expected value.

Incorrect · 2 reports
// camelCase discriminant: autofixes to 'task-completed'
export const TaskCompletedEvent = z.object({ type: z.literal('taskCompleted') });
// wrong value: autofixes to 'run-task'
export const RunTaskCommand = z.object({ type: z.literal('run') });
Correct
export const TaskCompletedEvent = z.object({ type: z.literal('task-completed') });
export const RunTaskCommand = z.object({ type: z.literal('run-task') });
  • Consts without a role suffix (TaskSchema), whatever their type literal says.
  • Role-suffixed consts without a type: z.literal(...) property.
  • Consts that are not exported: only export const declarations are checked.
Option Type Default Meaning
roleSuffixes string[] ['Event', 'Command', 'Query'] Const-name suffixes that mark a schema as a wire message.
'noctcore-contracts/wire-message-naming': ['error', { roleSuffixes: ['Message'] }]
Incorrect
// rule options: {"roleSuffixes":["Message"]}
// with roleSuffixes: ['Message'], autofixes to 'task-done'
export const TaskDoneMessage = z.object({ type: z.literal('done') });

If your messages do not carry a role-suffixed const name plus a type: z.literal(...) discriminant, this rule never fires. Pair it with zod-schema-naming (listing the same suffixes there) so every export is covered by exactly one naming contract.