When to Use TypeScript Optional Features
You're processing an API response and TypeScript throws an error because response.data.settings might be undefined. You know some users won't have settings configured yet, but you still need to handle the response safely. Should you mark it as optional with ?, or use a union type with undefined? And what's the difference, anyway?
TypeScript's optional features solve these problems, but they're not all the same. The ? operator works differently depending on where you use it, and understanding these differences will save you from runtime and type errors. This guide covers the patterns you'll use when building real applications.
Optional Properties and Parameters
Optional Properties in Interfaces
Mark properties as optional with a question mark (?) when they're not required in every object implementing the interface. This is perfect for configuration objects, API responses, or any data where some fields might be missing.
interface ApiConfig {
baseUrl: string;
timeout: number;
apiKey?: string; // Not all environments need authentication
retryAttempts?: number; // Can use default elsewhere
headers?: Record<string, string>;
}
const devConfig: ApiConfig = {
baseUrl: "http://localhost:3000",
timeout: 5000
}; // Valid
const prodConfig: ApiConfig = {
baseUrl: "https://api.production.com",
timeout: 10000,
apiKey: process.env.API_KEY,
retryAttempts: 3
}; // Also valid
When working with optional properties, TypeScript utility types like Partial<T> can make all properties in a type optional at once, which is useful for update operations.
Optional Parameters in Functions
Use the question mark (?) to make function parameters optional. The caller can omit these parameters entirely without causing a type error.
function fetchUserData(userId: string, includeMetadata?: boolean) {
const url = `/api/users/${userId}`;
if (includeMetadata) {
return fetch(`${url}?metadata=true`);
}
return fetch(url);
}
fetchUserData("user_123"); // Works fine
fetchUserData("user_456", true); // Also works
For more complex optional parameter handling, you can combine this with default parameters:
function createConnection(host: string, port: number = 3000) {
return `${host}:${port}`;
}
// Default value kicks in when parameter is omitted
createConnection("localhost"); // "localhost:3000"
When working with Convex, proper parameter validation is crucial. The argument validation techniques in Convex help ensure your optional parameters are properly typed and validated.
Handling Optional Chaining
Optional chaining (?.) lets you safely access nested properties that might be null or undefined. Instead of throwing "Cannot read property 'x' of undefined," it returns undefined and moves on.
interface ApiResponse {
data?: {
user?: {
profile?: {
email: string;
};
};
};
}
const response: ApiResponse = { data: { user: {} } };
// Without optional chaining - crashes if any part is undefined
// const email = response.data.user.profile.email; // Error!
// With optional chaining - safely returns undefined
const email = response.data?.user?.profile?.email; // undefined
You can also use optional chaining with function calls and array elements:
// Safely call methods that might not exist
const onSuccess = apiConfig?.callbacks?.onSuccess?.();
// Safely access array elements
const firstResult = queryResults?.data?.[0];
For a deep dive into all the ways you can use optional chaining, check out our complete guide on TypeScript optional chaining.
When building with Convex, optional chaining combines well with TypeScript's type system to safely access data from queries that might return undefined or null values.
Optional (?) vs Union with Undefined (| undefined)
Here's a distinction that trips up many developers: optional properties and union types with undefined aren't the same thing. They look similar but behave differently.
interface ConfigWithOptional {
apiKey?: string; // Property can be omitted entirely
}
interface ConfigWithUnion {
apiKey: string | undefined; // Property is required, but can be undefined
}
// Optional: omitting the property is fine
const config1: ConfigWithOptional = {}; // ✓ Valid
// Union: you MUST provide the property, even if it's undefined
const config2: ConfigWithUnion = {}; // ✗ Error: Property 'apiKey' is missing
const config3: ConfigWithUnion = { apiKey: undefined }; // ✓ Valid
This matters more than you might think. Optional properties let you omit the key entirely, while union types require you to explicitly pass the property (even if its value is undefined).
When to Use Each Pattern
Use optional properties (?) when:
- The property might not exist at all in the object
- You're working with partial updates where some fields can be skipped
- You want to allow callers to omit the parameter completely
Use union with undefined (| undefined) when:
- The property must always be present in the object
- You need to distinguish between "not provided" and "explicitly set to undefined"
- You're modeling nullable database fields that might contain NULL
// Good use of optional: partial updates
interface UserUpdate {
name?: string;
email?: string;
age?: number;
}
function updateUser(id: string, updates: UserUpdate) {
// Only update fields that were provided
}
updateUser("123", { name: "Alice" }); // Updates only name
// Good use of union: nullable values from database
interface DatabaseUser {
id: string;
name: string;
bio: string | null; // Bio can be null in database
deletedAt: Date | null; // Explicitly nullable timestamp
}
When working with TypeScript union types, the | undefined pattern is common for values that might not exist.
For Convex applications, understanding how to properly handle optional types is essential when validating arguments in your API functions.
Implementing Optional Fields in Classes
TypeScript classes can have optional fields just like interfaces. Use the ? after the property name to indicate it might not be set.
class HttpClient {
private baseUrl: string;
private timeout: number;
private apiKey?: string; // Optional - not all APIs need auth
private retryCount?: number;
constructor(baseUrl: string, timeout: number = 5000) {
this.baseUrl = baseUrl;
this.timeout = timeout;
}
setAuthentication(apiKey: string) {
this.apiKey = apiKey;
}
async fetch<T>(endpoint: string): Promise<T> {
const headers: Record<string, string> = {};
// Only add auth header if apiKey was configured
if (this.apiKey) {
headers['Authorization'] = `Bearer ${this.apiKey}`;
}
const response = await fetch(`${this.baseUrl}${endpoint}`, {
headers,
timeout: this.timeout
});
return response.json();
}
}
const publicClient = new HttpClient("https://api.example.com");
const authenticatedClient = new HttpClient("https://api.secure.com");
authenticatedClient.setAuthentication("secret_key");
When building type-safe applications with Convex, you might want to review TypeScript best practices for working with optional fields in your data models.
Managing Optional Arguments in Constructors
Constructors can accept optional arguments just like regular functions. Combine optional parameters with default values or nullish coalescing for clean initialization logic.
class DatabaseConnection {
private host: string;
private port: number;
private database: string;
private poolSize: number;
private ssl: boolean;
constructor(
host: string,
database: string,
port?: number,
options?: { poolSize?: number; ssl?: boolean }
) {
this.host = host;
this.database = database;
this.port = port ?? 5432; // Default PostgreSQL port
this.poolSize = options?.poolSize ?? 10;
this.ssl = options?.ssl ?? false;
}
connect() {
console.log(`Connecting to ${this.host}:${this.port}/${this.database}`);
console.log(`Pool size: ${this.poolSize}, SSL: ${this.ssl}`);
}
}
// All of these work
const local = new DatabaseConnection("localhost", "myapp");
const staging = new DatabaseConnection("staging.db.example.com", "myapp", 5433);
const prod = new DatabaseConnection("prod.db.example.com", "myapp", 5432, {
poolSize: 20,
ssl: true
});
The TypeScript double question mark operator (nullish coalescing) works well with optional parameters to provide default values only when the parameter is null or undefined.
For Convex applications, understanding these patterns helps when defining schema validators with the VNull class for handling optional fields in your database schema.
Applying Optional Modifiers in Type Aliases
Type aliases support optional properties just like interfaces. This is useful when modeling complex data structures where not all fields are guaranteed to exist.
type ApiResponse<T> = {
data?: T;
error?: {
code: string;
message: string;
details?: Record<string, unknown>;
};
metadata?: {
requestId: string;
timestamp: number;
};
};
// Either data or error will be present, but not both
const successResponse: ApiResponse<{ userId: string }> = {
data: { userId: "user_123" },
metadata: { requestId: "req_456", timestamp: Date.now() }
};
const errorResponse: ApiResponse<never> = {
error: {
code: "AUTH_FAILED",
message: "Invalid credentials"
}
};
You can also combine optional types with TypeScript utility types like Partial<T>, Pick<T, K>, and Omit<T, K> to create more complex type transformations:
type User = {
id: string;
name: string;
email: string;
phone: string;
role: "admin" | "user";
};
// Make a type where all fields except id are optional
type UserUpdate = Pick<User, 'id'> & Partial<Omit<User, 'id'>>;
const update: UserUpdate = {
id: "user_123", // Required
email: "newemail@example.com" // Only updating email
};
For Convex developers, understanding these patterns is essential when working with the TypeScript schema and creating type-safe database operations.
Understanding the exactOptionalPropertyTypes Flag
By default, TypeScript treats optional properties (?) and properties typed as | undefined almost interchangeably. You can explicitly set an optional property to undefined, and TypeScript won't complain. The exactOptionalPropertyTypes compiler flag changes this behavior to be more strict.
interface Settings {
theme?: "light" | "dark";
notifications?: boolean;
}
// Without exactOptionalPropertyTypes (default behavior)
const settings1: Settings = {}; // ✓ Valid
const settings2: Settings = { theme: undefined }; // ✓ Also valid
// With exactOptionalPropertyTypes enabled in tsconfig.json
const settings3: Settings = {}; // ✓ Still valid
const settings4: Settings = { theme: undefined }; // ✗ Error!
// Type 'undefined' is not assignable to type '"light" | "dark"'
This flag helps you distinguish between "property not provided" and "property explicitly set to undefined." It's particularly useful when you're merging objects or working with partial updates.
When to Enable This Flag
Enable exactOptionalPropertyTypes if you want to:
- Enforce clearer intent in your code (missing vs explicitly undefined)
- Catch bugs where you accidentally pass
undefinedinstead of omitting the property - Work with APIs or libraries that distinguish between missing and undefined values
Keep it disabled if:
- You're working with a large existing codebase (it can cause breaking changes)
- You regularly use
Partial<T>for updates and want to allow explicitundefinedvalues - Your team's coding style doesn't distinguish between omitted and undefined properties
To enable it, add it to your tsconfig.json:
{
"compilerOptions": {
"exactOptionalPropertyTypes": true
}
}
If you need to explicitly allow undefined as a value with this flag enabled, add it to the type union:
interface Settings {
theme?: "light" | "dark" | undefined; // Now undefined is explicitly allowed
}
const settings: Settings = { theme: undefined }; // ✓ Valid
Common Mistakes to Avoid
Let's look at the pitfalls that catch developers off guard when working with optional features.
Confusing Optional Chaining Syntax for Arrays
Optional chaining with array elements requires ?.[index], not ?[index]. Missing the dot causes a syntax error.
const users = [{ name: "Alice" }, { name: "Bob" }];
// Wrong - syntax error
// const firstName = users?[0].name;
// Correct
const firstName = users?.[0]?.name;
Forgetting That ?. Only Checks for Null and Undefined
Optional chaining (?.) only stops for null and undefined. It doesn't check for other falsy values like 0, "", or false. If you need to check for any falsy value, use the && operator instead.
const count = 0;
// Optional chaining doesn't stop for 0
const result1 = count?.toString(); // "0" - it works!
// Logical AND stops for 0 (falsy value)
const result2 = count && count.toString(); // 0 - doesn't call toString()
Mixing Up Optional Parameters and Default Parameters
Optional parameters can be omitted and will be undefined. Default parameters have a fallback value when omitted. Don't confuse the two.
// Optional parameter - could be undefined
function log(message: string, level?: string) {
console.log(level); // Could be undefined
}
// Default parameter - guaranteed to have a value
function logWithDefault(message: string, level: string = "info") {
console.log(level); // Always a string, defaults to "info"
}
log("test"); // level is undefined
logWithDefault("test"); // level is "info"
Not Checking Optional Values Before Using Them
Just because a property is optional doesn't mean you can use it without checking. TypeScript will warn you if you try to use an optional value directly.
interface ApiConfig {
timeout?: number;
}
function makeRequest(config: ApiConfig) {
// Wrong - timeout might be undefined
// const delay = config.timeout * 2; // Error!
// Correct - check first or provide fallback
const delay = (config.timeout ?? 5000) * 2;
}
Accidentally Breaking Optional Chains
When using optional chaining, you need ?. at every level that could be undefined. Missing even one link breaks the safety.
const response = {
data: {
user: undefined
}
};
// Wrong - crashes if user is undefined
// const email = response.data?.user.profile.email; // Error!
// Correct - chain at every uncertain level
const email = response.data?.user?.profile?.email; // undefined
Final Thoughts on TypeScript Optional Features
You'll use TypeScript's optional features constantly when building real applications. Here's what matters most:
- Use
?for truly optional data where the property might not exist at all - Use
| undefinedor| nullwhen the property must be present but can have a null/undefined value - Enable
exactOptionalPropertyTypesif you want stricter checking around missing vs undefined properties - Chain optional accesses (
?.) at every level that could be undefined to avoid runtime crashes - Combine
?.with??to provide sensible fallback values
The key difference to remember: optional (?) means "can be omitted," while union with undefined (| undefined) means "must be provided, but can be undefined." Get this distinction right, and you'll avoid a whole class of bugs.
When building with Convex, these patterns combine well with the type-safe approach to database operations, helping you build reliable applications with confidence.