How to use the TypeScript Partial<T> utility type
You're writing a function to update a user's profile, but you only want to change their email address. TypeScript forces you to pass the entire User object with all required fields, even though you're only updating one property. This is where Partial<T> comes in. It's a utility type that makes every property in a type optional, letting you work with subsets of objects without defining multiple interfaces for every scenario.
Understanding TypeScript's Partial
The Partial<T> utility type makes all properties of a type optional. Here's how it works in practice:
interface User {
id: number;
name: string;
email: string;
}
const updateUser = (userId: number, updates: Partial<User>) => {
// Only update the properties you provide
if (updates.name) {
console.log(`Updating name to: ${updates.name}`);
}
if (updates.email) {
console.log(`Updating email to: ${updates.email}`);
}
};
// This works - you only provide what you're changing
updateUser(1, { email: 'newemail@example.com' });
In this example, updateUser accepts any subset of User properties. You can pass just an email, just a name, or both. The utility types like Partial<T> eliminate the need to define separate interfaces for every possible combination of optional fields.
Partial vs Required vs Readonly: When to Use Each
TypeScript provides three property modifier utilities that work in different ways. Here's how they compare:
| Utility Type | What It Does | Common Use Case |
|---|---|---|
Partial<T> | Makes all properties optional | PATCH API endpoints, partial form updates |
Required<T> | Makes all properties required (opposite of Partial) | Validating complete data before database insert |
Readonly<T> | Makes all properties read-only | Configuration objects, immutable state |
interface ApiResponse {
data?: string;
error?: string;
status?: number;
}
// Required ensures all fields are present
type CompleteResponse = Required<ApiResponse>;
// { data: string; error: string; status: number; }
// Readonly prevents modifications
type ImmutableResponse = Readonly<ApiResponse>;
// Can't reassign properties after creation
// Partial allows any combination
type PartialResponse = Partial<ApiResponse>;
// Any field can be undefined
Use Partial<T> when you need flexibility with which properties to include. Use Required<T> when you need to guarantee all properties exist. Use Readonly<T> when you want to prevent mutations.
Creating Flexible Object Types with Partial
You can create a partial type from any existing interface or type:
interface PatchRequest {
id: number;
title: string;
content: string;
publishedAt: Date;
authorId: number;
}
type ArticleUpdate = Partial<PatchRequest>;
// Valid - update just the title
const titleUpdate: ArticleUpdate = {
title: 'New Title',
};
// Valid - update multiple fields
const contentUpdate: ArticleUpdate = {
title: 'Updated Title',
content: 'New content here',
publishedAt: new Date(),
};
Update operations in APIs need this when argument validation without repetition matters. Instead of defining separate types for create vs update operations, you use Partial<T> to indicate which fields can be updated.
Working with API PATCH Requests
One of the most common real-world uses for Partial<T> is handling HTTP PATCH requests where clients send only the fields they want to update:
interface Product {
id: string;
name: string;
price: number;
stock: number;
description: string;
imageUrl: string;
}
async function patchProduct(
productId: string,
updates: Partial<Product>
): Promise<Product> {
// Validate that at least one field is being updated
if (Object.keys(updates).length === 0) {
throw new Error('No fields to update');
}
// Only send the fields that were provided
const response = await fetch(`/api/products/${productId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates),
});
return response.json();
}
// Update just the price
await patchProduct('prod_123', { price: 29.99 });
// Update price and stock together
await patchProduct('prod_123', { price: 29.99, stock: 100 });
REST APIs work exactly like this. The client doesn't need to send the entire object, just the fields that changed.
Building Progressive Forms with Partial
React and other frontend frameworks often need to track partial form state as users fill out multi-step wizards or auto-saving forms:
interface SignupForm {
username: string;
email: string;
password: string;
confirmPassword: string;
phoneNumber: string;
agreedToTerms: boolean;
}
function useFormState() {
// Start with an empty object - nothing is required yet
const [formData, setFormData] = useState<Partial<SignupForm>>({});
const updateField = <K extends keyof SignupForm>(
field: K,
value: SignupForm[K]
) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
// Only enable submit when required fields are present
const canSubmit = Boolean(
formData.email &&
formData.password &&
formData.agreedToTerms
);
return { formData, updateField, canSubmit };
}
With Partial<SignupForm>, you can store whatever fields the user has filled in so far without TypeScript complaining about missing required properties. When working with Convex's server module, you get flexible client-side interfaces that match your backend data structures.
Applying Partial to Existing Interfaces
Use Partial<T> to make all properties of an interface optional:
interface DatabaseUser {
id: number;
email: string;
profile: {
firstName: string;
lastName: string;
avatarUrl: string;
};
settings: {
theme: 'light' | 'dark';
notifications: boolean;
};
}
type UserUpdate = Partial<DatabaseUser>;
const updateUserProfile: UserUpdate = {
profile: {
firstName: 'John',
lastName: 'Doe',
avatarUrl: 'https://example.com/avatar.jpg',
},
};
Here's something important: Partial<T> only makes the top-level properties optional. The profile object becomes optional, but if you include it, all its internal properties (firstName, lastName, avatarUrl) are still required. We'll cover how to handle deeply nested objects later.
Combining Partial with Other Types
You can combine Partial<T> with other utility types to create precise type definitions:
// Make only specific fields optional
type PartialEmailAndName = Partial<Pick<User, 'email' | 'name'>>;
// { email?: string; name?: string; }
// Make everything optional except 'id'
type UpdateUser = Partial<Omit<User, 'id'>> & Pick<User, 'id'>;
// { id: number; name?: string; email?: string; }
You get precision when dealing with complex data structures. The keyof operator pairs well with Partial to create dynamic mapped types. When building backends with Convex, combining Pick<T, K> and Omit<T, K> with Partial creates strong data validation patterns, as shown in the types cookbook.
You can also layer multiple transformations:
interface Article {
id: string;
title: string;
content: string;
authorId: string;
publishedAt: Date | null;
tags: string[];
}
// Create a type for draft articles: id is required, everything else optional
type DraftArticle = Partial<Omit<Article, 'id'>> & Pick<Article, 'id'>;
const draft: DraftArticle = {
id: 'draft_123',
title: 'Work in Progress',
// Other fields can be undefined
};
Handling Deeply Nested Objects
Partial<T> operates one level deep. For nested objects, you need additional handling:
interface AppConfig {
database: {
host: string;
port: number;
credentials: {
username: string;
password: string;
};
};
cache: {
ttl: number;
maxSize: number;
};
}
type PartialConfig = Partial<AppConfig>;
// Results in: {
// database?: { host: string; port: number; credentials: { ... } };
// cache?: { ttl: number; maxSize: number; };
// }
Notice that database is optional, but if you provide it, TypeScript still requires host, port, and the complete credentials object. To make all nested properties optional, create a recursive DeepPartial<T> type:
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
type DeepPartialConfig = DeepPartial<AppConfig>;
// Results in: {
// database?: {
// host?: string;
// port?: number;
// credentials?: { username?: string; password?: string; }
// };
// cache?: { ttl?: number; maxSize?: number; };
// }
// Now you can provide any subset at any level
const devConfig: DeepPartialConfig = {
database: {
host: 'localhost',
// port and credentials can be undefined
},
};
When using generics<T> with Convex's code spelunking techniques, these recursive types maintain type safety throughout your application.
Testing with DeepPartial
DeepPartial<T> shines when writing unit tests where you need to mock complex objects but only care about specific fields:
interface UserWithRelations {
id: string;
profile: {
name: string;
email: string;
avatar: {
url: string;
width: number;
height: number;
};
};
settings: {
notifications: {
email: boolean;
push: boolean;
sms: boolean;
};
privacy: {
profileVisible: boolean;
searchable: boolean;
};
};
}
// In your test file
function createMockUser(overrides?: DeepPartial<UserWithRelations>): UserWithRelations {
return {
id: 'test_123',
profile: {
name: 'Test User',
email: 'test@example.com',
avatar: {
url: '/default-avatar.png',
width: 100,
height: 100,
},
...overrides?.profile,
},
settings: {
notifications: {
email: true,
push: false,
sms: false,
...overrides?.settings?.notifications,
},
privacy: {
profileVisible: true,
searchable: true,
...overrides?.settings?.privacy,
},
},
};
}
// In tests, only specify what matters for that test
const testUser = createMockUser({
profile: { email: 'specific@test.com' },
settings: { notifications: { email: false } },
});
You can create realistic test fixtures without boilerplate. Define defaults once and override only what each test cares about.
Using Partial for Optional Properties
Partial<T> works alongside existing optional properties to create highly flexible interfaces:
interface User {
name: string;
email: string;
age?: number; // Already optional
}
type OptionalUser = Partial<User>;
const user: OptionalUser = {
name: 'John Doe',
email: 'john@example.com',
// age was already optional, still optional
};
With OptionalUser, all properties become optional, even those already marked with ?. You'll use this when building form validation systems or working with optional parameters in functions. When implementing client-side interfaces for Convex's TypeScript best practices, you create flexible type definitions that match your backend data structures.
When NOT to Use Partial
Don't reach for Partial<T> to silence TypeScript errors. Here are common mistakes and better alternatives:
Mistake 1: Using Partial to avoid proper validation
// ❌ Bad - Partial hides the problem
function createUser(data: Partial<User>): User {
// What if data.email is undefined? Runtime error waiting to happen
return data as User;
}
// ✅ Better - Be explicit about what's required
interface CreateUserInput {
email: string; // Required
name?: string; // Actually optional
age?: number;
}
function createUser(data: CreateUserInput): User {
return {
id: generateId(),
email: data.email, // TypeScript ensures this exists
name: data.name ?? 'Anonymous',
age: data.age,
};
}
Mistake 2: Using Partial when you need runtime validation
// ❌ Bad - Partial doesn't protect you at runtime
async function updateProfile(updates: Partial<Profile>) {
// TypeScript says updates.email might be undefined, but what if
// someone sends { email: null } or { email: '' } from the API?
await db.update(updates);
}
// ✅ Better - Validate at runtime too
import { z } from 'zod';
const ProfileUpdateSchema = z.object({
email: z.string().email().optional(),
name: z.string().min(1).optional(),
}).refine(data => Object.keys(data).length > 0, {
message: 'At least one field must be provided',
});
async function updateProfile(updates: z.infer<typeof ProfileUpdateSchema>) {
const validated = ProfileUpdateSchema.parse(updates);
await db.update(validated);
}
Mistake 3: Making everything optional when only some fields should be
// ❌ Bad - Too permissive
type UserUpdate = Partial<User>;
// ✅ Better - Be specific about what can change
type UserUpdate = Pick<User, 'id'> & Partial<Omit<User, 'id' | 'createdAt'>>;
// id is required, createdAt can never be updated, everything else is optional
TypeScript's type system only protects you at compile time. When working with complex filters in Convex, remember that Partial<T> doesn't validate data coming from external sources. You still need runtime checks.
Working with Dynamic Structures
For dynamic object structures where keys aren't known ahead of time, Partial<T> has limited use:
interface DynamicConfig {
[key: string]: any;
}
type PartialDynamicConfig = Partial<DynamicConfig>;
const config: PartialDynamicConfig = {
theme: 'dark',
language: 'fr',
layout: 'compact',
};
This example seems redundant since DynamicConfig already accepts any properties. But Partial<T> becomes valuable when combined with other utilities like keyof to create mapped types. You'll need this when working with flexible object structures that still need to maintain type safety.
Final Thoughts on TypeScript's Partial
Partial<T> turns all properties optional. That's it. But that one thing solves a ton of problems: API PATCH requests, progressive forms, test mocks, and update functions all become cleaner. You stop defining five different interfaces for five different scenarios.
Know when to use it and when to be specific. If you genuinely need flexibility with which properties to include, use Partial<T>. But if you know exactly which fields should be optional, spell that out. Don't make everything optional by default and hope for the best.
Sources: