Stop Using Optional Properties in TypeScript: Here's Why Being Explicit Beats Convenience
In TypeScript, optional properties have their place—especially in APIs or when dealing with user input where some properties may legitimately be absent. But for internal application logic, leaning heavily on optional properties can be more harmful than helpful. It’s tempting to reach for prop?: string
when you think a property might not always be defined, but this habit can lead to sneaky, silent bugs that are hard to debug and track down. Let's dive into why being explicit about undefined values can make your TypeScript code more predictable, readable, and safer.
The Case Against Optional Properties in Internal Logic
When you declare a property as prop?: string
, TypeScript interprets this as prop: string | undefined
. This seems convenient, as it allows you to skip initializing the property and later access it directly without much fuss. But here’s the catch: because TypeScript doesn’t require you to handle the potential undefined
value explicitly, you’re at risk of running into undefined
values in places where you expect a defined one.
This issue is even more noticeable in complex applications. When your app grows, the absence of a property can lead to unexpected bugs that slip through the cracks. Here’s why:
- Implicit Undefineds: When you use
prop?: string
, TypeScript isn’t strict about whetherprop
is actually defined or not. This leniency means you could forget to set the property, and TypeScript won’t throw an error—it just considers itundefined
. - Silent Failures: Without strict handling of potential undefined values, you’re more likely to hit runtime errors. Optional properties are effectively nullable, meaning any code that assumes
prop
is always defined could break in unexpected ways.
Use prop: string | undefined
Instead
Being explicit with prop: string | undefined
forces you to handle undefined
as a possibility every time you access the property. This approach makes your code more robust by helping you address the edge cases directly. Here’s what you gain by declaring properties this way:
- Cleaner Code: You’re forced to acknowledge every
undefined
possibility. This may seem like more work at first, but it prevents assumptions about data that may or may not exist. - Improved Readability: Colleagues and future-you will thank you for this. When someone reads
prop: string | undefined
, they know immediately thatprop
can beundefined
and should be handled as such. It’s explicit and, therefore, more readable. - Better Compiler Support: TypeScript’s type checker becomes much more effective when it knows you’re explicitly accounting for
undefined
. This means better error messages and fewer bugs.
Example Comparison
Here’s a comparison that demonstrates the difference between prop?: string
and prop: string | undefined
:
// ❌ prop?: string - possible runtime error if accessed without checking
function greet(user?: { name?: string }) {
console.log(`Hello, ${user.name.toUpperCase()}!`); // Runtime error if name is undefined
}
// ✅ prop: string | undefined - forces you to handle undefined cases
function greet(user: { name: string | undefined }) {
if (user.name) {
console.log(`Hello, ${user.name.toUpperCase()}!`);
} else {
console.log("Hello, guest!");
}
}
In the second example, TypeScript will alert you if you try to access user.name
without checking if it’s undefined
, which helps prevent runtime errors.
Other Tips for Writing Safe TypeScript Code
Beyond avoiding optional properties in internal logic, here are other best practices for writing safer, more predictable TypeScript code:
- Leverage Strict Null Checks: Make sure
strictNullChecks
is enabled in yourtsconfig.json
. This setting forces you to explicitly handlenull
andundefined
values, ensuring you can’t ignore potential nullish values in your code. - Use
NonNullable
When Possible: If a property or variable shouldn’t be nullable, use TypeScript’sNonNullable
utility type to enforce this. For example:type NonNullableName = NonNullable<string | undefined>;
. This helps TypeScript throw errors if you accidentally introduceundefined
values where they shouldn’t exist. - Initialize Properties in Constructors: Another way to avoid undefined properties is by initializing them in constructors. For instance,
class User { name: string; constructor(name: string) { this.name = name; } }
. This ensuresname
is always defined, and you won’t have to handle undefined cases. - Consider Type Guards: If you find yourself working with values that could be undefined, consider creating type guards to check them. This approach helps to clarify when values are defined, making your code easier to follow.
function isDefined<T>(value: T | undefined): value is T {
return value !== undefined;
}
Now you can use this guard in conditional statements, making your code safer:
const user = { name: undefined };
if (isDefined(user.name)) {
console.log(user.name.toUpperCase());
}
- Use
unknown
for Safer APIs: When handling data from APIs or third-party libraries, usingunknown
instead ofany
can protect you from unsafe assumptions about data types. This forces you to explicitly define types before using data, which pairs well with the practice of avoiding optional properties.
Finally
In short, avoiding optional properties (prop?: string
) in your internal TypeScript code can drastically improve the predictability and robustness of your application. By using prop: string | undefined
, you’re forced to handle the undefined
case explicitly, reducing the likelihood of silent errors. Pair this with other TypeScript best practices—such as enabling strict null checks, initializing properties in constructors, and using type guards—and you’ll find that your code is cleaner, more readable, and far less prone to unexpected behavior.
Embrace being explicit—it’s the small change that can save you from a lot of debugging headaches down the road.