Skip to main content

TypeScript's Pick<T, K> Utility Type

You're building an API that returns user data, and you realize you're accidentally exposing password hashes to the frontend. Or maybe you're passing an entire database model to a component that only needs two properties. These situations happen all the time, and they're exactly where TypeScript's Pick<T, K> utility type saves you. Instead of manually redefining types or hoping you remember which fields to exclude, Pick<T, K> lets you explicitly select the properties you need. This guide will show you practical patterns for using Pick<T, K> to create focused, type-safe subsets of your interfaces.

Introduction to Pick<T, K>

The Pick<T, K> utility type selects specific properties from an existing type. The syntax is Pick<Type, Keys>, where Type is your source type and Keys are the properties you want to include:

interface User {
id: number;
name: string;
email: string;
passwordHash: string;
createdAt: Date;
}

type PublicUser = Pick<User, 'id' | 'name'>;

// PublicUser now only has:
// { id: number; name: string; }

This creates a new type that contains only the selected properties. It's particularly useful when you need to expose data without sensitive information, or when different parts of your application need different views of the same data model.

The TypeScript utility types library includes Pick<T, K> as one of its built-in type transformations, giving you a clean way to create derived types without duplicating property definitions.

When working with Convex, the argument validation without repetition pattern can be enhanced with Pick<T, K> to create more focused argument validators from existing schema types.

Pick vs Omit: When to Use Each

Here's a question you'll face constantly: should you use Pick to select what you want, or Omit to exclude what you don't want? The answer depends on how many properties you're working with.

Use Pick<T, K> when:

  • You only need a few specific properties from a large type
  • You're creating focused types for specific contexts (like API responses or UI components)
  • You want to be explicit about exactly what data is available

Use Omit<T, K> when:

  • You need most properties but want to exclude just a few
  • You're removing sensitive fields (passwords, tokens) from otherwise complete types
  • The type might grow over time and you want new fields to automatically appear

Here's a practical comparison:

interface DatabaseUser {
id: number;
name: string;
email: string;
passwordHash: string;
sessionToken: string;
createdAt: Date;
updatedAt: Date;
lastLoginAt: Date;
}

// Pick: Selecting only what the profile component needs
type ProfileData = Pick<DatabaseUser, 'id' | 'name' | 'email'>;

// Omit: Excluding only sensitive fields for the frontend
type FrontendUser = Omit<DatabaseUser, 'passwordHash' | 'sessionToken'>;
ScenarioUse PickUse Omit
Selecting 2-3 properties from 10+
Excluding 1-2 sensitive fields
Creating minimal API responses
Removing auth tokens from types
Type will gain new fields over time

For more details on Omit, check out the TypeScript Omit documentation.

Creating Focused Types for Real-World Scenarios

Let's look at practical scenarios where Pick<T, K> solves real problems.

API Response Sanitization

When your backend returns full database models but your frontend only needs specific fields:

interface Order {
id: string;
userId: string;
items: Array<{ productId: string; quantity: number; }>;
total: number;
paymentMethod: string;
billingAddress: string;
internalNotes: string; // For warehouse staff only
fulfillmentStatus: string;
createdAt: Date;
}

// What the customer-facing API should return
type CustomerOrderView = Pick<Order, 'id' | 'items' | 'total' | 'fulfillmentStatus' | 'createdAt'>;

function getCustomerOrders(userId: string): CustomerOrderView[] {
// TypeScript ensures you can't accidentally leak internalNotes
// or other sensitive fields to the customer
}

This pattern is valuable when building APIs or data transfer objects where you want to expose only relevant data. The resulting type maintains the exact same property types as the original, ensuring type safety throughout your application.

When working with Convex databases, this pattern pairs well with the best practices for data retrieval, where efficient querying often requires selecting specific fields rather than fetching entire documents.

Role-Based Data Access

Different user roles need different data. Pick<T, K> makes this explicit:

interface Employee {
id: number;
name: string;
email: string;
department: string;
salary: number;
performanceReviews: string[];
emergencyContact: string;
}

// What team members can see about each other
type TeamMemberView = Pick<Employee, 'id' | 'name' | 'email' | 'department'>;

// What HR can access
type HRView = Pick<Employee, 'id' | 'name' | 'salary' | 'performanceReviews' | 'emergencyContact'>;

// Type-safe functions for each role
function getTeamDirectory(): TeamMemberView[] { /* ... */ }
function getHRRecords(): HRView[] { /* ... */ }

This approach aligns with Convex's design philosophy of useState-less state management, where you define exact data structures for your application's needs rather than managing complex state objects with unnecessary properties.

Form Handling and Validation

When you have a complete data model but your form only collects a subset:

interface Product {
id: string;
name: string;
description: string;
price: number;
stockQuantity: number;
createdAt: Date;
updatedAt: Date;
createdBy: string;
}

// The product creation form only needs these fields
type ProductFormData = Pick<Product, 'name' | 'description' | 'price' | 'stockQuantity'>;

function handleProductSubmit(formData: ProductFormData) {
// TypeScript knows formData doesn't have id, createdAt, etc.
// The system will generate those values
}

When working with Convex, you can use Pick<T, K> with complex filters to create more focused data retrieval patterns, selecting only the fields you need and reducing unnecessary data transfer.

Working with Nested Properties

Here's where Pick<T, K> has a limitation: it only works with top-level properties. You can't do Pick<Product, 'details.manufacturer'> directly. But there are practical workarounds.

The Problem with Nested Structures

Let's say you have deeply nested data:

interface Product {
id: string;
details: {
manufacturer: string;
warranty: string;
specifications: {
weight: number;
dimensions: string;
};
};
pricing: {
currency: string;
amount: number;
taxRate: number;
};
}

// This won't work:
// type ProductSummary = Pick<Product, 'id' | 'details.manufacturer'>;

Solution 1: Pick Top-Level, Then Select Properties

The most straightforward approach is to pick the entire nested object, then access specific properties using bracket notation:

// Pick the nested objects you need
type ProductWithDetails = Pick<Product, 'id' | 'details' | 'pricing'>;

// Create a new type with the specific nested properties
type ProductSummary = {
id: Product['id'];
manufacturer: Product['details']['manufacturer'];
price: Product['pricing']['amount'];
};

This pattern combines Pick<T, K> with TypeScript keyof operator principles to access nested structures while maintaining type safety.

Solution 2: Restructure for Shallow Objects

If you control the data model, consider flattening structures where it makes sense:

interface FlatProduct {
id: string;
manufacturer: string;
warranty: string;
priceCurrency: string;
priceAmount: number;
taxRate: number;
}

// Now Pick works naturally
type ProductSummary = Pick<FlatProduct, 'id' | 'manufacturer' | 'priceAmount'>;

When working with Convex applications, this aligns with argument validation without repetition patterns, where you can reuse field validators across different parts of your application.

Composing Pick with Other Utility Types

Pick<T, K> becomes even more powerful when combined with other TypeScript utilities.

Pick + Partial for Optional Updates

Create types for partial updates where you select specific fields and make them optional:

interface UserProfile {
id: number;
name: string;
email: string;
bio: string;
avatarUrl: string;
location: string;
}

// Only these fields can be updated, and all are optional
type ProfileUpdate = Partial<Pick<UserProfile, 'name' | 'bio' | 'avatarUrl' | 'location'>>;

function updateProfile(userId: number, updates: ProfileUpdate) {
// TypeScript ensures:
// 1. Only allowed fields can be updated (not id or email)
// 2. All fields are optional (partial updates are fine)
}

Learn more about making properties optional with TypeScript Partial.

Pick with Generics for Reusable Patterns

Create generic functions that work with picked properties:

function serializeForApi<T, K extends keyof T>(
data: T,
fields: K[]
): Pick<T, K> {
const result = {} as Pick<T, K>;

for (const field of fields) {
result[field] = data[field];
}

return result;
}

// Usage with type inference
const publicUser = serializeForApi(fullUserData, ['id', 'name']);
// Type: Pick<User, 'id' | 'name'>

Pick with Record for Dynamic Keys

Combine Pick<T, K> with TypeScript Record to create mappings of selected properties:

interface ApiEndpoint {
path: string;
method: string;
auth: boolean;
rateLimit: number;
description: string;
}

// Create a record of endpoint configs with only the runtime-needed fields
type EndpointConfigs = Record<
string,
Pick<ApiEndpoint, 'path' | 'method' | 'auth'>
>;

const endpoints: EndpointConfigs = {
getUser: {
path: '/users/:id',
method: 'GET',
auth: true
},
createPost: {
path: '/posts',
method: 'POST',
auth: true
}
};

Common Pitfalls and How to Fix Them

Error: "Type is not assignable to type"

This happens when you try to assign a full type to a picked type:

interface User {
id: number;
name: string;
email: string;
role: string;
}

type BasicUser = Pick<User, 'id' | 'name'>;

const fullUser: User = { id: 1, name: 'Alice', email: 'alice@example.com', role: 'admin' };

// ❌ Error: Type 'User' is not assignable to type 'BasicUser'
const basic: BasicUser = fullUser;

// ✅ Solution: Explicitly select the properties
const basic: BasicUser = {
id: fullUser.id,
name: fullUser.name
};

// ✅ Or use a helper function
function toBasicUser(user: User): BasicUser {
return { id: user.id, name: user.name };
}

Error: "Property does not exist on type"

You'll see this when trying to access properties that weren't picked:

type PublicUser = Pick<User, 'id' | 'name'>;

function displayUser(user: PublicUser) {
console.log(user.name); // ✅ Works
console.log(user.email); // ❌ Error: Property 'email' does not exist
}

// Solution: Either pick the property you need or use a type guard
function displayUserWithEmail(user: User | PublicUser) {
console.log(user.name); // ✅ Works for both

if ('email' in user) {
console.log(user.email); // ✅ Type narrowed to include email
}
}

Picking Non-Existent Properties

TypeScript will catch this at compile time:

interface User {
id: number;
name: string;
}

// ❌ Error: Type '"username"' does not exist in type 'User'
type InvalidPick = Pick<User, 'id' | 'username'>;

// This is actually helpful - it prevents typos and keeps your types in sync

Using Pick with Interfaces vs Types

Pick works with both, but behaves slightly differently:

// ✅ Works perfectly
interface UserInterface {
id: number;
name: string;
}
type PickedFromInterface = Pick<UserInterface, 'id'>;

// ✅ Also works
type UserType = {
id: number;
name: string;
};
type PickedFromType = Pick<UserType, 'id'>;

// ❌ This doesn't work - can't use Pick in interface declaration
// interface NewInterface extends Pick<UserInterface, 'id'> {} // Error

For more on the differences, see TypeScript interface vs TypeScript object type comparisons.

Final Thoughts on Pick<T, K> in TypeScript

TypeScript's Pick<T, K> utility type is your go-to tool for creating focused, purpose-specific types from larger interfaces. By selecting only the properties you need, you write cleaner code, prevent accidental data leaks, and make your intentions explicit. Whether you're sanitizing API responses, implementing role-based access control, or creating focused form types, Pick<T, K> keeps your types precise and your data secure.

Remember: use Pick when you're selecting a small number of properties, and reach for Omit when you're excluding just a few. Combine Pick with Partial, generics, and other utility types to build sophisticated type transformations that make your TypeScript codebase more maintainable.

Pick<T, K> works well alongside other TypeScript utility types like Omit<T, K>, Partial<T>, and Record<K, T> to give you complete control over your type definitions. For teams using Convex, Pick<T, K> pairs perfectly with their TypeScript-first approach to backend development, allowing you to create precisely tailored types for queries, mutations, and UI components.