Dictionary extensions API
Venue-specific additions as one declaration: the extension schema, the runtime merge that applies it, and the literal-typed map helpers that keep Tags.SymbolName hovering as 1007. Generated from @boarteam/fix 0.5.2 as installed; the doc text is the library’s own. Guide: Extending a dictionary.
defineExtensionfunctionsince 0.2.0
function defineExtension<const E extends DictionaryExtension>(ext: E & GuardWidening<E>): ESource: packages/fix/src/dictionary/extension.ts:212
Pin an extension declaration's literal types once — a zero-cost identity whose TS 5 const type parameter keeps every tag: 1007 / msgType: 'UP1' literal without as const, so the SAME value can drive extendDictionary (runtime) and tagsOf/msgTypesOf (types).
const ctrader = defineExtension({ id: 'ctrader', fields: { SymbolName: { tag: 1007, type: 'String' }, SymbolDigits: { tag: 1008, type: 'int' }, }, messages: { SecurityList: { groups: { NoRelatedSym: { append: ['SymbolName', 'SymbolDigits'] } } }, },});| Parameter | Type | Description |
|---|---|---|
| ext | E & GuardWidening<E> | The extension declaration, written inline so its literals stay narrow. |
Returns E — The same object, its literal types pinned.
extendDictionaryfunctionsince 0.2.0
function extendDictionary(base: DictionaryJSON, ...extensions: DictionaryExtension[]): ExtendResultSource: packages/fix/src/dictionary/extendDictionary.ts:74
Apply venue extension declarations to a dictionary, producing a NEW DictionaryJSON the engine consumes unchanged. Never throws on data problems — every collision, unresolvable reference, malformed entry, or unsafe placement is a returned FixIssue (extend/* codes), and the offending operation is skipped or reverted rather than applied unsafely.
Semantics:
- Pure: inputs are never mutated; the result is deterministic.
- Composable: extensions apply left to right; later extensions see earlier ones.
- Idempotent: re-applying an extension changes nothing (duplicate members are skipped with a warning; identical field redefinitions are silent).
- Delimiter-safe: placements are append/after-anchor only, and every operation is checked against the corruption hazards the grammar alone cannot rule out — a group entry delimiter shifting (
extend/group-delimiter-shift, reverted), a placed member colliding with a group scope that could be open on the wire around the insertion point in either direction (extend/ambiguous-boundary, skipped; optional members between are treated as potentially absent), a new group with no resolvable entry delimiter (extend/unresolvable-group-delimiter, skipped), and a component reference cycle (extend/component-cycle, reverted).
Run validateDictionary on the result as the final gate, exactly as for any other dictionary.
const { dictionary, issues } = extendDictionary(fix44, ctrader);if (issues.some((i) => i.severity === 'error')) { // an operation was skipped or reverted — fix the extension declaration}const engine = createFixEngine(dictionary);| Parameter | Type | Description |
|---|---|---|
| base | DictionaryJSON | The dictionary to extend; never mutated. |
| extensions | DictionaryExtension[] | Extension declarations, applied left to right. |
Returns ExtendResult — The merged dictionary plus extend/* diagnostics.
ExtendResultinterfacesince 0.2.0
interface ExtendResultSource: packages/fix/src/dictionary/extendDictionary.ts:23
The result of extendDictionary: the merged dictionary plus diagnostics.
dictionary: DictionaryJSONA fresh
DictionaryJSON(deep clone — the base and the extensions are never mutated), consumed unchanged byloadDictionary/createFixEngine. When the base passesvalidateDictionaryandissuescarries no error-severity entries, the result passesvalidateDictionarytoo.issues: FixIssue[]extend/*diagnostics, returned as data — never thrown. Severity encodes the outcome:error= the operation was skipped or reverted,warning= applied (or, forextend/duplicate-member, already satisfied) but review,info= advisory.
DictionaryExtensioninterfacesince 0.2.0
interface DictionaryExtensionSource: packages/fix/src/dictionary/extension.ts:125
One venue extension declaration — the single source of truth for both layers.
readonly id?: stringProvenance label, e.g.
'ctrader'; echoed into issue paths and the merged JSON.readonly fields?: Readonly<Record<string, ExtensionFieldDef>>New fields, keyed by NAME.
readonly enums?: Readonly<Record<string, readonly ExtensionEnumValue[]>>Enum values appended to EXISTING fields, keyed by field NAME.
readonly components?: Readonly<Record<string, ComponentExtension>>New components and additions to existing ones, keyed by component name.
readonly messages?: Readonly<Record<string, MessageExtension | NewMessageSpec>>New messages and patches to existing ones, keyed by message name.
ExtensionFieldDefinterfacesince 0.2.0
interface ExtensionFieldDefSource: packages/fix/src/dictionary/extension.ts:38
A new field. Keyed by its NAME in DictionaryExtension.fields; the tag lives here. isGroupCounter is never authored — it is inferred from type deriving from NumInGroup, mirroring the invariant documented on the dictionary contract.
readonly tag: numberThe field's numeric tag, e.g.
1007.readonly type: stringMust name an existing datatype in the target dictionary (
'String','int','Price', …).readonly enumValues?: readonly ExtensionEnumValue[]Allowed values, when the new field is enumerated.
readonly lengthField?: numberFor a
data-based field: the tag of the companionLengthfield preceding it.readonly description?: stringHuman description, carried into the merged
FieldDef.
ExtensionEnumValueinterfacesince 0.2.0
interface ExtensionEnumValueSource: packages/fix/src/dictionary/extension.ts:24
One allowed value to add to a field's enum. description defaults to name.
readonly value: stringThe on-the-wire value, verbatim and opaque (
"1","004","AB").readonly name: stringA code-identifier-safe name, e.g.
"Buy". The identity on conflicts.readonly description?: stringHuman description; defaults to
namewhen omitted.
MemberSpectypesince 0.2.0
type MemberSpec = string | { readonly field: string; readonly reqd?: Reqd;} | { readonly component: string; readonly reqd?: Reqd;} | { /** A NEW nested repeating group; `group` names its counter field. */ readonly group: string; readonly reqd?: Reqd; readonly members: readonly MemberSpec[];}Source: packages/fix/src/dictionary/extension.ts:56
A member to place into a body. The string shorthand is an optional field by name (reqd: 'N'). Names resolve against the extension's own DictionaryExtension.fields first, then the base dictionary's fields/components.
GroupExtensioninterfacesince 0.2.0
interface GroupExtensionSource: packages/fix/src/dictionary/extension.ts:68
Members to add to one repeating group's entry body.
readonly append: readonly MemberSpec[]The members to add to the entry body, in order.
readonly after?: stringAnchor member (field/component/group-counter name) to insert immediately AFTER; omit to append at the end of the entry body (which is also the encode position).
before/startare deliberately unsupported: a group's first body field is its entry delimiter, and inserting ahead of it would change entry detection.
MessageExtensioninterfacesince 0.2.0
interface MessageExtensionSource: packages/fix/src/dictionary/extension.ts:81
A patch to an existing message (addressed by name via the record key).
readonly msgType?: neverNever set on a patch — its presence is what marks an entry as a
NewMessageSpec.readonly append?: readonly MemberSpec[]Appended at the end of the body, kept BEFORE the trailing Standard Message Trailer.
readonly after?: stringAnchor for
append, as inGroupExtension.after.readonly groups?: Readonly<Record<string, GroupExtension>>Additions to EXISTING repeating groups, keyed by counter-field-name path — dotted for nesting (
'NoRelatedSym.NoUnderlyings'). A group that lives behind a shared component is auto-resolved only when it is reachable via exactly one single-reference component chain; otherwise the placement is refused with aextend/target-not-foundhint naming the component to target explicitly.
NewMessageSpecinterfacesince 0.2.0
interface NewMessageSpecSource: packages/fix/src/dictionary/extension.ts:99
A brand-new message (the presence of msgType is what marks an entry as new).
readonly msgType: stringThe
MsgType(tag 35) value, case-sensitive ('U1','UP1', …).readonly category?: MessageCategorySession (
admin) vs application (app) message. Defaults to'app'.readonly members: readonly MemberSpec[]Body only — the base dictionary's Standard Message Header/Trailer component refs are injected automatically (reported via
extend/header-trailer-injected).
ComponentExtensiontypesince 0.2.0
type ComponentExtension = { readonly members: readonly MemberSpec[];} | { readonly append?: readonly MemberSpec[]; readonly after?: string; readonly groups?: Readonly<Record<string, GroupExtension>>;}Source: packages/fix/src/dictionary/extension.ts:116
A component entry: { members } defines a NEW component; the other form extends an existing one (additions become visible in every referencing scope — deliberate, and reported via extend/component-fanout).
tagsOffunctionsince 0.2.0
function tagsOf<const E extends DictionaryExtension>(ext: E): TagsOf<E>Source: packages/fix/src/dictionary/extension.ts:226
Derive the literal name → tag map from an extension declaration, typed as TagsOf. Pure and total; collisions are the runtime merge's concern.
export const Tags = extendTags(Fix44Tags, tagsOf(ctrader)); // Tags.SymbolName: 1007| Parameter | Type | Description |
|---|---|---|
| ext | E | The extension declaration to derive from. |
Returns TagsOf<E> — The literal name → tag map of the extension’s fields.
msgTypesOffunctionsince 0.2.0
function msgTypesOf<const E extends DictionaryExtension>(ext: E): MsgTypesOf<E>Source: packages/fix/src/dictionary/extension.ts:240
Derive the literal name → msgType map of an extension's NEW messages, typed as MsgTypesOf. Message patches (entries without msgType) are skipped.
| Parameter | Type | Description |
|---|---|---|
| ext | E | The extension declaration to derive from. |
Returns MsgTypesOf<E> — The literal name → msgType map of the extension’s NEW messages.
TagsOftypesince 0.2.0
type TagsOf<E extends DictionaryExtension> = Readonly<{ [K in keyof FieldsOf<E> & string]: FieldsOf<E>[K] extends { tag: infer T extends number; } ? T : never;}>Source: packages/fix/src/dictionary/extension.ts:148
The literal name → tag map type derived from an extension's fields record — TagsOf<typeof ctrader> is { readonly SymbolName: 1007; readonly SymbolDigits: 1008 }. Feed the value-level counterpart tagsOf to extendTags.
MsgTypesOftypesince 0.2.0
type MsgTypesOf<E extends DictionaryExtension> = Readonly<{ [K in keyof MsgsOf<E> & string as MsgsOf<E>[K] extends { msgType: string; } ? K : never]: MsgsOf<E>[K] extends { msgType: infer M extends string; } ? M : never;}>Source: packages/fix/src/dictionary/extension.ts:158
The literal name → msgType map type derived from an extension's NEW messages (entries carrying msgType); message patches are excluded.
extendTagsfunctionsince 0.2.0
function extendTags<B extends Record<string, number>, const T extends Record<string, number>>(base: B, ext: T): ExtendTags<B, T>Source: packages/fix/src/dictionary/extendTags.ts:74
Merge venue-specific tag entries over a base Tags map, keeping every literal type. The const type parameter (TS ≥ 5.0) pins call-site literals without as const; on a name collision the extension wins (object-spread semantics), and the ExtendTags result type mirrors that exactly.
Pure: returns a new object; never mutates base or ext.
const Tags = extendTags(Fix44Tags, { SymbolName: 1007, SymbolDigits: 1008 });Tags.SymbolName; // typed 1007type TagName = keyof typeof Tags; // includes 'SymbolName'Note: literals survive only for call-site object literals (or as const values) — an extension pre-declared as Record<string, number> is already widened and yields number-typed entries.
| Parameter | Type | Description |
|---|---|---|
| base | B | The shipped Tags map to extend. |
| ext | T | Venue entries; on a name collision the extension wins. |
Returns ExtendTags<B, T> — The merged map, literal types preserved.
extendMsgTypesfunctionsince 0.2.0
function extendMsgTypes<B extends Record<string, string>, const T extends Record<string, string>>(base: B, ext: T): ExtendMsgTypes<B, T>Source: packages/fix/src/dictionary/extendTags.ts:114
Merge venue-specific message-type entries over a base MsgType map — the string-valued mirror of extendTags.
const MsgType = extendMsgTypes(Fix44MsgType, { CTraderPing: 'UP1' });MsgType.CTraderPing; // typed 'UP1'| Parameter | Type | Description |
|---|---|---|
| base | B | The shipped MsgType map to extend. |
| ext | T | Venue entries; on a name collision the extension wins. |
Returns ExtendMsgTypes<B, T> — The merged map, literal types preserved.
invertTagsfunctionsince 0.2.0
function invertTags<const T extends Record<string, number>>(tags: T): InvertTags<T>Source: packages/fix/src/dictionary/extendTags.ts:94
Build the tag → name reverse of a Tags map, typed as InvertTags: literal lookups for known tags plus the shipped packages' widened name | undefined number index for arbitrary input.
Total and silent by contract: two names sharing one tag resolve last-write-wins (mirroring what a spread-built forward map does); extendDictionary is where such collisions are reported. For a standalone bijection guarantee, assert Object.keys(invertTags(m)).length === Object.keys(m).length in a test, as the shipped packages' names.test.ts does.
| Parameter | Type | Description |
|---|---|---|
| tags | T | The name → tag map to invert. |
Returns InvertTags<T> — The tag → name reverse, typed as InvertTags.
invertMsgTypesfunctionsince 0.2.0
function invertMsgTypes<const T extends Record<string, string>>(msgTypes: T): InvertMsgTypes<T>Source: packages/fix/src/dictionary/extendTags.ts:127
Build the msgType → name reverse of a MsgType map — the string-valued mirror of invertTags.
| Parameter | Type | Description |
|---|---|---|
| msgTypes | T | The name → msgType map to invert. |
Returns InvertMsgTypes<T> — The msgType → name reverse, typed as InvertMsgTypes.
ExtendTagstypesince 0.2.0
type ExtendTags<B extends Record<string, number>, T extends Record<string, number>> = Readonly<Omit<B, keyof T> & T>Source: packages/fix/src/dictionary/extendTags.ts:27
The result of extendTags: a key-remapped merge where extension keys cleanly REPLACE base keys. Deliberately not a bare intersection — with B & T, a name present in both with different tags would collapse to 55 & 9955 = never; here it types as the extension's literal, matching the runtime spread.
ExtendMsgTypestypesince 0.2.0
type ExtendMsgTypes<B extends Record<string, string>, T extends Record<string, string>> = Readonly<Omit<B, keyof T> & T>Source: packages/fix/src/dictionary/extendTags.ts:43
ExtendTags for MsgType maps (name → msgType strings).
InvertTagstypesince 0.2.0
type InvertTags<T extends Record<string, number>> = { readonly [K in keyof T as T[K] & number]: K & string;} & { readonly [tag: number]: (keyof T & string) | undefined;}Source: packages/fix/src/dictionary/extendTags.ts:38
The result of invertTags: the precise tag → name mapped inversion (so TagNames[1007] hovers as its literal name) intersected with the widened index signature the shipped packages use (so TagNames[someNumber] is name | undefined, never a type error).
InvertMsgTypestypesince 0.2.0
type InvertMsgTypes<T extends Record<string, string>> = { readonly [K in keyof T as T[K] & string]: K & string;} & { readonly [msgType: string]: (keyof T & string) | undefined;}Source: packages/fix/src/dictionary/extendTags.ts:49
InvertTags for MsgType maps: msgType → name with a string index.
GuardWideningtypesince 0.2.0
type GuardWidening<E extends DictionaryExtension> = (TagsOf<E>[keyof TagsOf<E>] extends never ? unknown : number extends TagsOf<E>[keyof TagsOf<E>] ? WidenedExtensionError : unknown) & (MsgTypesOf<E>[keyof MsgTypesOf<E>] extends never ? unknown : string extends MsgTypesOf<E>[keyof MsgTypesOf<E>] ? WidenedExtensionError : unknown)Source: packages/fix/src/dictionary/extension.ts:180
Compile-time guard: resolves to unknown (no-op) when the extension's tag and msgType literals survived inference, and to WidenedExtensionError when they widened to number/string — which happens when the declaration was pre-typed as DictionaryExtension before the call. Without this, the flagship literal hovers would silently degrade; with it, the degradation is a readable type error.
WidenedExtensionErrorinterfacesince 0.2.0
interface WidenedExtensionErrorSource: packages/fix/src/dictionary/extension.ts:168
The compile error surfaced by GuardWidening when an extension's literals were widened before reaching defineExtension. Not meant to be used directly.
readonly 'ERROR: extension literals were widened': 'declare the extension inline at the defineExtension call site (or as const) so tags stay literal types like 1007'The remedy, carried in the property NAME so it reads in full inside editor hovers.