ValueOf Utility Type
`keyof` gives you the union of property names; `ValueOf<T>` gives you the union of property values. Combined with `as const` it derives a string-literal union from a single source-of-truth object, so the runtime constant and the static type can never drift. This snippet covers the one-line definition, an enum-replacement pattern, and a discriminator helper that drives type-safe `switch` blocks.
1,174 views
25
type ValueOf<T> = T[keyof T];
const Severity = {
'low': 1,
'medium': 2,
'high': 3,
} as const;
type SeverityValue = ValueOf<typeof Severity>;
function shouldPage(level: SeverityValue): boolean {
return level >= 2;
}
console.log(shouldPage(Severity.low));
console.log(shouldPage(Severity.high));T[keyof T] indexes a type by all of its keys at once, which TypeScript expands into the union of all property value types. With as const, the literal types stay narrow (1 | 2 | 3 instead of number), so SeverityValue becomes a precise sentinel-set the compiler can exhaustively check. The pattern fits anywhere you would have reached for a numeric or string enum but want a plain object that survives JSON.stringify and tree-shakes cleanly. The runtime behavior is just an object lookup, no extra emit cost.
type ValueOf<T> = T[keyof T];
const Status = {
'queued': 'queued',
'running': 'running',
'done': 'done',
'failed': 'failed',
} as const;
type StatusKey = ValueOf<typeof Status>;
function nextStatus(s: StatusKey): StatusKey {
if (s === 'queued') return 'running';
if (s === 'running') return 'done';
return s;
}
console.log(nextStatus('queued'));
console.log(nextStatus('done'));When the keys and values of the constant object match (a string-keyed dictionary that maps to its own key), ValueOf produces a string-literal union that reads like an enum but stays a plain object at runtime. This is the recommended TypeScript replacement for string enums (enum Status { Queued = 'queued' }) because it avoids the extra runtime object that enum emits and works in 'erasableSyntaxOnly' mode. Pair this with a state-machine function and the compiler will refuse any unknown status string in callers.
type ValueOf<T> = T[keyof T];
const Action = {
'add': 'ADD',
'remove': 'REMOVE',
'reset': 'RESET',
} as const;
type ActionType = ValueOf<typeof Action>;
function reduce(action: ActionType, count: number): number {
switch (action) {
case Action.add:
return count + 1;
case Action.remove:
return Math.max(0, count - 1);
case Action.reset:
return 0;
}
}
console.log(reduce(Action.add, 5));
console.log(reduce(Action.reset, 5));Driving a switch block from Action.<key> constants instead of bare strings makes refactors safe: rename the constant once and every reference updates with a normal find-replace. Combined with ActionType, the compiler reports a missing branch the moment you add a fourth action, so the reducer cannot silently fall through. This is the kernel of every type-safe Redux/Zustand-style store and most state machines you will write in TS. For exhaustiveness on the switch, follow up with the assertNever helper from the ts-exhaustive-check snippet.
