When to use a Question Mark in TypeScript
You're calling response.user.profile.email in your app, and suddenly you're staring at "Cannot read property 'email' of undefined." The API returned a user without a profile, and now your app's broken. You need a way to handle these uncertain values without littering your code with if statements.
That's where TypeScript's question mark operators come in. Whether you're marking properties as optional, safely accessing nested data, or providing fallback values, the ? symbol gives you multiple tools to handle uncertainty. This guide covers the practical patterns you'll use every day.
1. The Ternary Operator (? :)
The ternary operator lets you write conditional expressions in one line. It takes three parts: a condition, a value if true, and a value if false.
const status = response.status === 200 ? 'success' : 'error';
This is cleaner than an if-else block for simple conditions. You'll see it frequently in JSX return statements, variable assignments, and function returns:
function getApiUrl(environment: string): string {
// Choose API endpoint based on environment
return environment === 'production'
? 'https://api.production.com'
: 'https://api.staging.com';
}
const userRole = user.isAdmin ? 'admin' : 'viewer';
const discount = cartTotal > 100 ? 0.15 : 0.10; // 15% off for orders over $100
The ternary operator shines for straightforward true/false decisions. When you need more than two outcomes or complex logic, stick with traditional if-else statements for readability.
2. Optional Properties in Interfaces
Use the ? operator to mark properties as optional in interfaces and types:
interface ApiConfig {
endpoint: string;
timeout: number;
apiKey?: string; // Optional - not all environments need auth
retryAttempts?: number; // Optional - defaults can be set elsewhere
}
This lets you create objects with or without certain properties. It's particularly useful when working with configuration objects, API responses, or any data that might have missing fields. Optional properties are central to TypeScript's flexibility, as explained in our guide to TypeScript optional properties.
When combined with TypeScript utility types like Partial<T>, you can create types where all properties become optional.
3. Optional Function Parameters
Make function parameters optional by adding ? in the parameter list:
function formatCurrency(amount: number, currency?: string): string {
// Default to USD if no currency provided
const symbol = currency === 'EUR' ? '€' : '$';
return `${symbol}${amount.toFixed(2)}`;
}
// Works with one or two arguments
formatCurrency(42.5); // "$42.50"
formatCurrency(42.5, 'EUR'); // "€42.50"
Optional parameters must come after required ones. You can't do this:
// Error: Required parameter cannot follow optional parameter
function calculate(width?: number, length: number): number {
return width * length;
}
This pattern provides flexibility in function signatures while maintaining type safety. When building backend functions with Convex, you can apply similar patterns. For instance, in Convex's API generation system, optional parameters help create flexible query functions.
4. Optional Chaining (?.)
The optional chaining operator ?. safely accesses nested properties without throwing errors:
// API might return user without profile data
const email = apiResponse?.user?.profile?.email;
If any part of the chain is null or undefined, the entire expression returns undefined instead of crashing. This TypeScript optional chaining feature eliminates verbose null checks:
// Without optional chaining - verbose and repetitive
let street;
if (user && user.address && user.address.street) {
street = user.address.street;
}
// With optional chaining - clean and safe
const street = user?.address?.street;
Optional chaining works with method calls too:
// Only calls getUserName() if user exists and has the method
const name = user?.getUserName?.();
// Safely access array elements
const firstItem = data?.items?.[0];
When working with potentially undefined values, it's crucial to understand how TypeScript handles null values and undefined values. These concepts are fundamental when building robust applications.
5. Optional Chaining vs && Operator
Optional chaining (?.) and the logical AND operator (&&) might look similar, but they behave differently:
const userCount = 0;
// Logical AND returns 0 (falsy value stops the chain)
const result1 = userCount && calculateStats(userCount); // 0
// Optional chaining only checks for null/undefined
const result2 = userCount?.toString(); // "0"
The && operator treats all falsy values as chain stoppers: 0, "", false, NaN, null, and undefined. Optional chaining only stops for null and undefined.
Use optional chaining when you specifically want to handle missing values. Use && when you need to check for any falsy condition:
// Good use of optional chaining - checking if property exists
const email = user?.contact?.email;
// Good use of && - checking if there's any data before processing
const stats = data.length && calculateStats(data);
6. Combining Optional Chaining with Nullish Coalescing
You can combine optional chaining with the nullish coalescing operator (??) to provide fallback values:
const displayName = user?.profile?.displayName ?? 'Anonymous';
This returns the display name if it exists, or 'Anonymous' if any part of the chain is null or undefined. This pattern creates resilient code that handles missing data gracefully:
function getApiTimeout(config?: ApiConfig): number {
// Use configured timeout, or default to 5000ms
return config?.timeout ?? 5000;
}
// Works for both optional data and deeply nested properties
const theme = settings?.ui?.theme ?? 'light';
const maxResults = queryParams?.pagination?.limit ?? 20;
Unlike the || operator, ?? only falls back for null and undefined - not for other falsy values like 0 or "":
const port = config.port ?? 3000; // Uses port 0 if explicitly set
const port = config.port || 3000; // Falls back to 3000 even if port is 0
When building with Convex, you'll often need to handle optional data from queries. The question mark operator works seamlessly with TypeScript interfaces and TypeScript types, making it invaluable for database interactions.
7. Nullable Types
To define a type that can be null, use a union type:
type NullableString = string | null;
type NullableUser = User | null;
let username: NullableString = "john_doe";
username = null; // Valid
This pattern explicitly declares that a value can be either the specified type or null. It's different from optional properties - nullable types can be explicitly set to null, while optional properties can be omitted entirely:
interface UserProfile {
bio: string | null; // Must be provided, but can be null
nickname?: string; // Can be omitted entirely
}
// Valid
const profile1: UserProfile = { bio: null };
const profile2: UserProfile = { bio: "Developer" };
// Invalid - bio is required (even if null)
// const profile3: UserProfile = { nickname: "dev" };
Understanding nullable types is fundamental when working with TypeScript union types. When building database-driven applications with Convex, you'll frequently encounter scenarios where data might be missing or optional. The TypeScript null type helps model these real-world scenarios accurately.
8. Common Pitfalls with Optional Chaining
Can't Assign to Optional Chains
You can't use optional chaining on the left side of an assignment:
// Error: Invalid left-hand side in assignment
user?.profile?.email = 'new@example.com';
// Check existence first, then assign
if (user?.profile) {
user.profile.email = 'new@example.com';
}
Missing ?. in the Middle of a Chain
Optional chaining only protects the immediate left value, not subsequent properties:
// Still crashes if contact is undefined
const street = user?.contact.address.street;
// Chain at every uncertain level
const street = user?.contact?.address?.street;
Array Indexing Requires ?.[]
When accessing array elements, you need ?. before the bracket notation if the array itself might be undefined:
// Crashes if items is undefined
const firstProduct = data?.items[0];
// Safely access array and element
const firstProduct = data?.items?.[0];
Comparison Operators with undefined
Be careful with comparison operators when using optional chaining:
// Error: Object is possibly 'undefined'
if (results?.length > 0) {
processResults(results);
}
// Check for undefined first
if (results && results.length > 0) {
processResults(results);
}
// Or use nullish coalescing with a default
if ((results?.length ?? 0) > 0) {
processResults(results);
}
Final Thoughts on the TypeScript Question Mark
The question mark in TypeScript gives you multiple tools for handling uncertainty in your code. The ternary operator (? :) streamlines simple conditionals. Optional properties and parameters (?) provide flexibility in interfaces and functions. Optional chaining (?.) safely navigates nested objects. Combined with nullish coalescing (??), these operators help you write code that gracefully handles missing data.
Master these patterns, and you'll write TypeScript that's more resilient and easier to maintain. The key is knowing which operator fits your situation - checking for any falsy value, handling just null/undefined, or providing fallback values.