Handling optional and union types is a fundamental skill in strongly typed languages like TypeScript, and it also plays a crucial role in designing APIs and data models that are both flexible and safe. Over the years, I’ve seen how properly managing these types can prevent a lot of runtime errors and improve code readability and maintainability. But it’s not just about knowing the syntax; it’s about understanding when and why to use them, the trade-offs involved, and how to avoid common pitfalls.
Optional types typically represent values that may or may not be present. In TypeScript, you often see this as a property marked with a question mark (?) or a union with undefined. For example:
interface User {
id: number;
name: string;
email?: string; // optional property
}
Here, email might be undefined or missing entirely. This is common in APIs where some fields are not always returned or in forms where certain inputs are optional.
null. Mixing these up causes bugs.Union types allow a variable to hold one of several types. This is powerful for handling cases where a value could be multiple things, like a string or a number, or different object shapes. For example:
type ID = string | number;
function printId(id: ID) {
if (typeof id === 'string') {
console.log('ID as string:', id.toUpperCase());
} else {
console.log('ID as number:', id.toFixed(2));
}
}
Here, id can be either a string or a number, and the function narrows the type at runtime using typeof.
Optional types are essentially a shorthand for a union with undefined. For example, email?: string is equivalent to email: string | undefined. However, the choice depends on the context:
| Aspect | Optional Types | Union Types |
|---|---|---|
| Use Case | Properties that might be missing or omitted | Values that can be one of several types |
| Syntax | Property marked with ? |
Type declared as TypeA | TypeB |
| Type Checking | Compiler checks for presence or absence | Requires explicit type narrowing |
| Common Pitfall | Assuming property always exists | Accessing properties without type guards |
In a project I worked on recently, we had an API response where some fields were optional depending on user permissions. We used optional types to model this:
interface ApiResponse {
userId: number;
profilePictureUrl?: string; // only present if user has uploaded a picture
lastLogin?: string; // might be missing for new users
}
By marking these fields optional, the frontend code was forced to check for their presence before rendering, which avoided a lot of null reference errors.
On the other hand, for a payment processing module, we had a union type to represent different payment methods:
type PaymentMethod =
| { type: 'credit_card'; cardNumber: string; expiry: string }
| { type: 'paypal'; email: string }
| { type: 'bank_transfer'; iban: string };
function processPayment(method: PaymentMethod) {
switch (method.type) {
case 'credit_card':
// handle credit card payment
break;
case 'paypal':
// handle PayPal payment
break;
case 'bank_transfer':
// handle bank transfer
break;
}
}
This discriminated union pattern is excellent for ensuring that all cases are handled explicitly, which improves maintainability and reduces bugs.
?.) or explicit checks.typeof, instanceof, or discriminant properties to narrow down the type before accessing specific fields.type) to union variants to simplify type narrowing.From a runtime perspective, optional and union types don’t add overhead because they are erased during compilation in TypeScript. However, the way you handle them in code can affect performance:
In practice, these performance concerns are rarely a bottleneck, but it’s good to be mindful of them when designing large-scale systems.
Optional and union types can also impact security, especially when dealing with external input like API requests or user data:
as to force a type without checks can open security holes if the data shape is not guaranteed.Sometimes developers are tempted to use any or unknown types to bypass strict typing when dealing with optional or multiple types. Here’s a quick comparison:
| Type | Pros | Cons | When to Use |
|---|---|---|---|
| Optional / Union Types | Strong type safety, clear intent, compiler checks | Requires explicit checks, can be verbose | When you know the possible types or optionality |
| any | Very flexible, no compiler errors | No type safety, prone to runtime errors | Legacy code, quick prototyping (avoid in production) |
| unknown | Safe alternative to any, forces type checks | Requires explicit narrowing, more boilerplate | When input types are truly unknown and must be validated |
In production code, I strongly prefer optional and union types over any. unknown can be useful when dealing with untyped external data, but it forces you to do proper validation, which is a good thing.
Handling optional and union types well is about balancing flexibility with safety. Optional types let you represent missing data cleanly, while union types allow for multiple possible shapes or types. Both require careful checks and thoughtful design to avoid common mistakes like unchecked access or overly broad types. Using discriminated unions and proper type narrowing improves maintainability and reduces bugs. And always remember that these types are tools to help you write clearer, safer code—not just syntactic sugar.