n explicit.
// TaskStateFlags.ts
export const TaskFlags = {
QUEUED: 1 << 0, // 00000001
PROCESSING: 1 << 1, // 00000010
COMPLETED: 1 << 2, // 00000100
FAILED: 1 << 3, // 00001000
RETRYABLE: 1 << 4, // 00010000
CANCELLED: 1 << 5, // 00100000
PAUSED: 1 << 6, // 01000000
LOGGING: 1 << 7 // 10000000
} as const;
export type TaskFlag = typeof TaskFlags[keyof typeof TaskFlags];
Why this works: JavaScript converts operands to 32-bit signed integers before bitwise evaluation. Bits 0 through 30 are safe for flags. Bit 31 is the sign bit; setting it produces negative numbers that break unsigned comparisons. Staying within the lower 31 positions guarantees predictable behavior.
Step 2: Combine and Initialize State
Use the bitwise OR (|) operator to merge multiple flags into a single integer. This sets specific bits without disturbing others.
// TaskOrchestrator.ts
import { TaskFlags, TaskFlag } from './TaskStateFlags';
export class TaskOrchestrator {
private state: number = 0;
constructor(initialFlags: TaskFlag[]) {
this.state = initialFlags.reduce((acc, flag) => acc | flag, 0);
}
}
Architecture decision: We use a private number field instead of exposing the raw integer. This prevents external mutation and allows controlled state transitions through explicit methods.
Step 3: Check, Set, Clear, and Toggle
Each operator serves a distinct state manipulation purpose.
// Check if a flag is active
has(flag: TaskFlag): boolean {
return (this.state & flag) !== 0;
}
// Activate one or more flags
set(...flags: TaskFlag[]): void {
this.state = flags.reduce((acc, flag) => acc | flag, this.state);
}
// Deactivate specific flags
clear(...flags: TaskFlag[]): void {
const mask = flags.reduce((acc, flag) => acc | flag, 0);
this.state &= ~mask;
}
// Flip flag state (on β off, off β on)
toggle(flag: TaskFlag): void {
this.state ^= flag;
}
Why each choice was made:
& isolates the target bit. If the result is non-zero, the flag is active. This avoids truthy/falsy coercion traps.
| merges flags efficiently. Using reduce allows batch updates without intermediate allocations.
&= ~mask is the standard pattern for clearing bits. The complement (~) inverts the mask, turning target bits to 0 and others to 1. AND-ing preserves everything except the cleared flags.
^ toggles without conditionals. It's mathematically equivalent to if (active) clear() else set(), but executes in a single CPU instruction.
Step 4: Production Debugging Strategy
Raw integers are opaque. Add a diagnostic utility that maps bits back to human-readable labels.
debugState(): string {
const active = Object.entries(TaskFlags)
.filter(([, value]) => (this.state & value) !== 0)
.map(([key]) => key);
return `State: ${this.state} | Active: [${active.join(', ')}]`;
}
This approach maintains performance in production while providing immediate visibility during development. Frameworks like Vue 3 use identical patterns in their EffectFlags enum for exactly this reason: compact storage with explicit debugging paths.
Pitfall Guide
1. Sign Bit Overflow
Explanation: Setting bit 31 (1 << 31) converts the number to a negative 32-bit signed integer. Subsequent bitwise operations behave unexpectedly because JavaScript treats the sign bit as part of the value.
Fix: Cap flag definitions at 1 << 30. Use TypeScript as const to prevent accidental extension beyond safe bounds.
2. Logical vs Bitwise Confusion
Explanation: Developers frequently substitute && for & or || for |. Logical operators short-circuit and return operands, not bitmasks. This breaks flag isolation and produces true/false instead of integers.
Fix: Enforce strict type checking. Use ESLint rules like no-bitwise only in non-flag modules, and document bitwise usage explicitly in state management files.
3. Debugging Opaque Numbers
Explanation: Logging this.state yields 13 or 42 with no context. Teams abandon bitwise patterns when debugging becomes slower than property access.
Fix: Implement a toString() or debugState() method that maps bits to labels. Use Number.prototype.toString(2) for binary representation during code reviews.
4. Shared Reference Mutation
Explanation: Passing the raw integer to multiple modules and mutating it directly creates race conditions in asynchronous contexts. Bitwise operations are not atomic in JavaScript's single-threaded model.
Fix: Encapsulate state in a class or closure. Expose only controlled methods (set, clear, has). For concurrent workers, use Atomics or message passing instead of shared memory.
5. Over-Engineering Simple State
Explanation: Applying bitwise flags to three or fewer booleans adds cognitive overhead without performance gains. The pattern shines at scale, not in trivial components.
Fix: Reserve bitwise packing for systems with 5+ concurrent boolean states, high-frequency evaluation loops, or strict memory constraints. Use objects for UI component props or configuration objects.
6. Type Coercion Traps
Explanation: Loose equality (==) or implicit boolean conversion (if (state & FLAG)) can misfire when the result is 0 (falsy) or when TypeScript narrows to number | boolean.
Fix: Always compare explicitly: (state & FLAG) !== 0. Use TypeScript's boolean return type for check methods to prevent downstream type confusion.
7. Ignoring Unsigned Shift Behavior
Explanation: The right shift (>>) preserves the sign bit, which causes unexpected results when extracting packed groups from negative numbers. The unsigned right shift (>>>) fills with zeros.
Fix: Use >>> when extracting bit groups or normalizing values. Reserve >> only when sign preservation is intentional. Document shift direction clearly in utility functions.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| High-frequency event loop (1k+ checks/sec) | Bitwise flags | Eliminates object allocation and branch prediction misses | Reduces CPU cycles by ~60-70% |
| UI component props or form state | Object literals | Readability and framework integration outweigh micro-optimizations | Negligible performance difference |
| Cross-thread data sharing | Bitwise + Atomics | Compact binary layout minimizes serialization overhead | Lowers IPC latency significantly |
| Configuration with 3-4 options | Enum or boolean object | Cognitive overhead of bitwise exceeds benefits | Zero runtime impact |
| Persistent storage (IndexedDB/SQLite) | Bitwise integer | Single column storage simplifies schema and indexing | Reduces storage footprint by ~80% |
Configuration Template
// BitwiseState.ts
export class BitwiseState<T extends Record<string, number>> {
private value: number = 0;
constructor(private readonly flags: T, initial?: (keyof T)[]) {
if (initial) {
this.value = initial.reduce((acc, key) => acc | flags[key], 0);
}
}
has(key: keyof T): boolean {
return (this.value & this.flags[key]) !== 0;
}
set(...keys: (keyof T)[]): void {
this.value = keys.reduce((acc, key) => acc | this.flags[key], this.value);
}
clear(...keys: (keyof T)[]): void {
const mask = keys.reduce((acc, key) => acc | this.flags[key], 0);
this.value &= ~mask;
}
toggle(key: keyof T): void {
this.value ^= this.flags[key];
}
snapshot(): number {
return this.value;
}
restore(value: number): void {
this.value = value;
}
toString(): string {
const active = Object.keys(this.flags).filter(k => this.has(k as keyof T));
return `BitwiseState(${this.value}) [${active.join(', ')}]`;
}
}
// Usage:
const Permissions = { READ: 1 << 0, WRITE: 1 << 1, EXECUTE: 1 << 2 } as const;
const userPerms = new BitwiseState(Permissions, ['READ', 'WRITE']);
userPerms.set('EXECUTE');
console.log(userPerms.has('WRITE')); // true
console.log(userPerms.toString()); // BitwiseState(7) [READ, WRITE, EXECUTE]
Quick Start Guide
- Define your flags: Create a
const object using 1 << n for each state. Keep indices between 0 and 30.
- Initialize the manager: Instantiate the
BitwiseState class (or your own wrapper) with your flag definition and optional initial set.
- Replace boolean checks: Swap
if (obj.isActive) with if (state.has('ACTIVE')). Use set() and clear() for mutations.
- Add diagnostics: Implement a
toString() or debug() method that maps active bits to labels. Use it during development and testing.
- Validate performance: Run a micro-benchmark comparing your previous object-based state against the bitwise implementation in your actual hot path. Confirm latency reduction before committing to production.