Skip to content

Named values

Every string-literal union that code branches on has a const object of the same name. Code compares against the object, as in head === Arrowhead.triangle, never against a raw string. ADR 0006 (docs/decisions/0006-named-values-in-comparisons.md) records the rule, and the draw/no-literal-comparison lint rule enforces it. Data is still written with plain strings, as in { kind: "arrow", ... }.

Object Values Type it names
Arrowhead none, arrow, triangle, dot, bar Arrowhead
DrawableKind rectangle, ellipse, diamond, line, freehand, text, arrow Drawable["kind"]
HandleKind vertex, terminal, bend Handle["kind"]
FillKind none, hachure, crossHatch ("cross-hatch"), solid Fill["kind"]
StrokeStyle solid, dashed, dotted StrokeStyle
TextSizing auto, fixedWidth ("fixed-width") TextSizing

A hyphenated value gets a camelCase key: FillKind.crossHatch is "cross-hatch".

Keeping objects and types in step

For a standalone union, the object is the source and the type is derived from it, as in type Arrowhead = (typeof Arrowhead)[keyof typeof Arrowhead].

For a discriminant inside a bigger union, such as Drawable["kind"], the union stays the source. The object is annotated with that type, as in export const DrawableKind: NamedValues<DrawableKind> = { ... }. NamedValues<U> maps each member of U to a camelCase key whose value is that member, so the compiler rejects an object that misses a member, adds one, or misspells one.

type NamedValues<U extends string> = { readonly [K in U as Camel<K>]: K };

Other packages and apps use NamedValues for their own unions. @draw/sketch uses it for PathCommandKind and DrawOpKind.