If you only need compile-time validation, you can simplify significantly with string literal union types:
/** * Message types using string literal unions for compile-time type safety */// Extension to Webview message typesexport type ExtensionToWebviewMessageType = 'focusNoteChanged';// Webview to Extension message types export type WebviewToExtensionMessageType = 'log';// Combined message type unionexport type MessageType = ExtensionToWebviewMessageType | WebviewToExtensionMessageType;
That’s it! This gives you:
✅ Full compile-time type safety - TypeScript will catch typos and invalid values
✅ Autocompletion - Your IDE will suggest valid message types
✅ Zero runtime overhead - These types are erased during compilation
✅ Much simpler code - No objects, no as const, no type guards needed
Usage Example
// TypeScript will enforce these are valid message typesfunction handleMessage(type: MessageType, payload: any) { switch (type) { case 'focusNoteChanged': // ✅ Valid break; case 'log': // ✅ Valid break; case 'invalid': // ❌ TypeScript error! break; }}// Function signatures enforce correct typesfunction sendExtensionMessage(type: ExtensionToWebviewMessageType, data: any) { // Implementation}sendExtensionMessage('focusNoteChanged', {}); // ✅ ValidsendExtensionMessage('log', {}); // ❌ TypeScript error!
This is the most idiomatic TypeScript approach when you only need compile-time validation. The type system handles everything for you without any runtime code.
CON: No Runtime validation
CON: No Runtime Validation
Risk of runtime type mismatches - If message types come from external sources (user input, APIs, parsed JSON, configuration files, etc.), TypeScript can’t validate them at runtime. A malformed message could crash your application or cause unexpected behavior.
// These scenarios will compile fine but fail at runtime:const userInput = getUserInput(); // Returns "invalidMessageType"handleMessage(userInput as MessageType, {}); // 💥 Runtime errorconst apiResponse = JSON.parse(response); // { type: "typo-in-message-type" }handleMessage(apiResponse.type, apiResponse.data); // 💥 Silent failureconst config = loadConfig(); // Contains outdated message typesendMessage(config.messageType, data); // 💥 Unpredictable behavior
When this matters: If your message types cross boundaries (network, file system, user input), you’ll want runtime validation. If they’re purely internal to your TypeScript codebase, compile-time validation is usually sufficient.