5.8 Optional Properties and Their Interaction with strictNullChecks
Now, let’s get into the weeds on optional properties and strictNullChecks. This is where TypeScript stops being polite and starts getting real. You’ve probably defined an interface where not every property is required. You slap a ? on it and call it a day. But when you flip on strictNullChecks—which you absolutely should, it’s the single most important compiler flag—that innocent little question mark starts to mean a whole lot more.
An optional property doesn’t just mean “this might be missing.” Under strictNullChecks, it means “this property can be undefined.” This is a crucial distinction. It’s the difference between a truly absent property and one that exists but is explicitly set to undefined. TypeScript’s type system models the former as, well, an absence, and the latter as the value undefined.
The Two Flavors of “Not There”
Consider this common pattern for configuration objects:
interface AppConfig {
apiUrl: string;
retryCount?: number;
timeoutMs: number;
}
// This is valid. `retryCount` is absent.
const config1: AppConfig = {
apiUrl: "https://api.example.com",
timeoutMs: 30_000
};
// This is ALSO valid. `retryCount` is present but explicitly undefined.
// This is the surprise for many developers.
const config2: AppConfig = {
apiUrl: "https://api.example.com",
retryCount: undefined,
timeoutMs: 30_000
};
Both config1 and config2 are perfectly acceptable to the type checker. The optional property retryCount can be either missing or undefined. This is why if you try to use it, you have to check for both possibilities.
function attemptRequest(config: AppConfig) {
// Error: Object is possibly 'undefined'. Did you forget the tears?
console.log(`Retrying ${config.retryCount * 2} times`);
// Correct. You must confront the nullish reality.
if (config.retryCount != null) {
console.log(`Retrying ${config.retryCount * 2} times`); // Safe
}
}
When You Absolutely, Positively Need a Value
Sometimes, an optional property is a lie. You get an object from an API or a function, and the property will be there at runtime, but the type system doesn’t know it. This is where we reach for non-null assertion operator (!). Use it like a sharp knife: carefully, sparingly, and only when you’re sure you won’t cut yourself.
interface UserResponse {
id: string;
name?: string; // The API docs say this is optional...
}
function processUser(user: UserResponse) {
// ...but our business logic knows that for active users, it's always there.
// We've checked the runtime data and this is safe. The type system is wrong.
const displayName = user.name!; // Tell the compiler to shut up about it.
console.log(displayName.toUpperCase());
}
If you’re wrong about this, you’ll get a runtime undefined error and it will be entirely your fault. The compiler trusted you. Don’t make it regret that.
The Pit of Despair: Partial and Spreading
Here’s a classic “I’m in hell” moment. You use Partial<T> to create an object piecemeal, then assign it to the full type. This feels like it should work, but under strictNullChecks, it’s a type error waiting to happen.
interface DatabaseConnection {
host: string;
port: number;
username: string;
}
function createConnection(configOverrides: Partial<DatabaseConnection>): DatabaseConnection {
const defaultConfig: DatabaseConnection = {
host: "localhost",
port: 5432,
username: "admin"
};
// TypeScript will scream at you here.
// The merged object is still Partial because configOverrides might set things to undefined!
return { ...defaultConfig, ...configOverrides };
}
The correct, more verbose way is to provide a complete object yourself.
function createConnectionSafe(configOverrides: Partial<DatabaseConnection>): DatabaseConnection {
// Create the full object first, then override.
const connection: DatabaseConnection = {
host: configOverrides.host ?? "localhost",
port: configOverrides.port ?? 5432,
username: configOverrides.username ?? "admin"
};
return connection;
}
The designers gave us a wonderfully precise tool in strictNullChecks, but it forces you to be explicit about your assumptions. That optional property ? is a contract that says, “I might be nothing, and you will deal with that possibility.” It’s annoying until the moment it saves you from a midnight debugging session chasing a Cannot read property 'x' of undefined error. And that moment makes it all worth it.