Handling Unexpected Union Types in TypeScript: Best Practices for Edge Cases
Learn how to effectively manage unexpected union types in TypeScript with simple techniques that improve your error handling and code safety.
TypeScript's union types allow variables to hold multiple types, which is very powerful. However, when dealing with unexpected or edge case values inside these unions, your code can encounter runtime errors if these cases aren't properly handled. This article focuses on best practices for managing unexpected union types in TypeScript to keep your code safe and predictable.
Let's say you have a union type for a function argument that can accept either a string or a number. Usually, you handle both cases explicitly, but what if another type sneaks in during development, causing errors? We will discuss how to safely handle these scenarios.
type Input = string | number;
function processInput(input: Input) {
if (typeof input === 'string') {
console.log('String input:', input.toUpperCase());
} else if (typeof input === 'number') {
console.log('Number input:', input.toFixed(2));
} else {
// Unexpected case - this should never happen
console.error('Unexpected input type:', input);
}
}
processInput('hello');
processInput(42);
// @ts-expect-error simulating unexpected input
processInput(true);To catch such unexpected cases at compile time, TypeScript's type narrowing and exhaustive checking patterns come in handy. One common pattern is to use the never type with a helper function to ensure all cases in a union type are handled properly.
function assertNever(x: never): never {
throw new Error("Unexpected value: " + x);
}
function processInputExhaustive(input: Input) {
if (typeof input === 'string') {
console.log('String input:', input.toUpperCase());
} else if (typeof input === 'number') {
console.log('Number input:', input.toFixed(2));
} else {
assertNever(input); // This line helps ensure that all union types are handled
}
}
Using this approach, if you add a new type to the `Input` union in the future (for example, `boolean`), TypeScript will enforce that you update the function to handle the new case, helping prevent runtime errors from unhandled types.
Also, when you expect to receive union types that might be broader or external (like user input or API responses), it's good practice to validate and sanitize data before processing it. Type guards, custom type predicates, or libraries like `zod` can make this easier.
function isString(input: unknown): input is string {
return typeof input === 'string';
}
function safeProcess(input: unknown) {
if (isString(input)) {
console.log('Safe string input:', input.toUpperCase());
} else {
console.error('Invalid input type!');
}
}In summary, handling unexpected union types involves explicit type checking, exhaustive handling using the `never` type, and validating inputs. These practices enhance your TypeScript code’s robustness, making your applications less prone to bugs caused by unforeseen edge cases.