
As web developers, we constantly work with objects. We receive data, manipulate it, pass it around, and often need to create new objects based on existing ones, perhaps adding, modifying, or, critically, omitting certain properties.
You might get a user object from a database that includes a sensitive password field, but you only want to send the user’s public profile data to the frontend. Or you might have an internal configuration object with fields relevant only to server-side logic, which you want to strip before passing the config to a client-side context.
So, how do you efficiently and cleanly create a new object that contains almost all the properties of an old one, except for a select few?
If you’ve seen or used syntax like this:
const userData = {
id: 123,
username: 'developer',
email: 'dev@example.com',
password: 'hashedpassword123', // Sensitive!
createdAt: '...',
};
// We want a new object without 'password'
const { password: _, ...publicUserData } = userData;
console.log(publicUserData);
// Expected output:
// {
// id: 123,
// username: 'developer',
// email: 'dev@example.com',
// createdAt: '...',
// }
console.log(userData); // Original object is unchanged
// Expected output:
// {
// id: 123,
// username: 'developer',
// email: 'dev@example.com',
// password: 'hashedpassword123', // Still here!
// createdAt: '...',
// }
And wondered exactly what’s happening or if this is a “correct” way to do things — wonder no more! This syntax is not only correct but also a common, clean, and highly idiomatic pattern in modern JavaScript and TypeScript for the precise task of omitting properties.
Let’s break down this elegant little line:
Generated typescript
const { propertyToOmit: aliasOrUnderscore, ...restOfObject } = sourceObject;
Breaking Down the Syntax
This uses a combination of two powerful ES6 features: Object Destructuring and the Rest Property.
- propertyToOmit: aliasOrUnderscore:
- propertyToOmit: This is the name of the property you want to exclude from the new object (password in our example).
- : aliasOrUnderscore: This part assigns the value of propertyToOmit from the sourceObject to a new variable named aliasOrUnderscore in the current scope.
- Using _: By convention, using _ (an underscore) as the alias signifies that we are assigning the value to a variable specifically because the destructuring syntax requires it, but we have no intention of using this variable (_) anywhere else. It’s a signal to yourself and other developers that “I extracted this value, but I’m ignoring it.”
- The Effect: This step extracts the desired property and removes it from the object that the …rest operator will then process.
2. …restOfObject:
- The … here is the Rest Property (when used on the left-hand side of an assignment or in destructuring).
- restOfObject: This is the name of a new object that will be created.
- The Effect: This collects all the remaining properties from the sourceObject that were not individually destructured on the left side (in our case, everything except propertyToOmit). It then gathers them into this restOfObject.
In simple terms: “Take propertyToOmit and throw its value into _ (which I don’t care about), and collect everything else into a new object called restOfObject.”
✅ Why This Approach Is Preferred
- Immutability: The original sourceObject is left completely unchanged. You are creating a brand-new object (restOfObject) with the desired properties. This aligns with principles of functional programming and state management libraries (like Redux) where immutability is key.
- Conciseness: It’s incredibly short and expressive for what it achieves compared to manual copying or deletion.
- Readability: Once you understand the pattern, it clearly communicates intent: “destructure these specific fields (and discard them) and put the rest into a new object.”
- Type Safety (with TypeScript): TypeScript understands this syntax and will correctly infer the type of restOfObject, removing the types of the omitted properties.
Practical Use Cases
This pattern is invaluable in many scenarios:
- Stripping Sensitive Data:
interface User {
id: string;
username: string;
email: string;
passwordHash: string;
twoFactorSecret?: string;
// ... other fields
}
function getPublicUser(user: User) {
const { passwordHash: _, twoFactorSecret: _, ...publicUser } = user;
return publicUser; // Type of publicUser will be User without passwordHash & twoFactorSecret
}
- Removing Database/Internal Fields: Fields like _id, __v (Mongoose version key), or internal timestamps might be relevant in your backend but unnecessary or confusing to a client.
interface ProductDocument extends Document { // Mongoose Document
_id: Types.ObjectId;
__v?: number;
name: string;
price: number;
internalNotes?: string;
}
function cleanProductForClient(product: ProductDocument) {
// Using Mongoose .lean() is often better for performance, but if you have an instance:
const productObject = product.toObject(); // Get a plain JS object
const { _id: _, __v: _, internalNotes: _, ...clientProduct } = productObject;
return clientProduct; // clientProduct has name and price, types inferred correctly
}
(Note: For Mongoose, using .lean() when querying is often more efficient if you just need plain objects without Mongoose methods, and then you can apply this destructuring).
3. Cleaning Configuration Objects:
interface AppConfig {
apiKey: string;
apiSecret: string; // Server-only
clientConfig: {
theme: string;
maxItems: number;
};
}
const serverConfig: AppConfig = { ... }; // loaded config
const { apiSecret: _, ...clientSerializableConfig } = serverConfig;
// clientSerializableConfig can now be safely sent to the frontend.
// It has apiKey and clientConfig, but not apiSecret.
⚠️ A Note on Linters (no-unused-vars)
As mentioned in the original text, some ESLint configurations, specifically the no-unused-vars rule, might flag the use of _ as a variable that is defined but never used.
While using _ is a widely understood convention for intentional discarding, linters sometimes prefer explicit solutions. If you encounter this, you have a few options:
- Disable the Rule (less recommended globally): You could disable no-unused-vars, but this prevents it from catching actual typos or truly unused variables.
- Disable Rule for Line/Block: Add an ESLint comment to ignore the rule for that specific line or block.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { password: _, ...publicUserData } = userData;
(Using the @typescript-eslint version if you’re using their recommended config).
3. Omit the Alias (If Lint Rule Allows): Some linters might allow just const { password, …publicUserData } = userData; without using _, but this assigns the password value to a variable also named password, which might shadow another variable or just feel less explicit about discarding the value.
4. Use a Comment: As suggested, simply destructure the variable without aliasing and add a comment:
const { password, ...publicUserData } = userData; // password is not needed in publicUserData
Many teams simply accept the _ convention and configure their linters to allow variables named _ to be unused (often via argsIgnorePattern: ‘^_$’ in the no-unused-vars or @typescript-eslint/no-unused-vars rule configuration). Choose the approach that best fits your team’s coding standards and linter setup. The core syntax for omission via destructuring + rest remains the same.
Alternative Approaches (and why destructuring is often better)
Before this destructuring pattern became common, you might have omitted properties using:
- The delete operator:
const publicUserData = { ...userData }; // Copy the object
delete publicUserData.password; // Modify the copy
Downsides: delete directly mutates the object (though here we mutated a copy). It’s less functional/immutable than creating a new object from the start.
2. Manual copying/looping:
const publicUserData = {};
for (const key in userData) {
if (key !== 'password') {
publicUserData[key] = userData[key];
}
}
Downsides: Much more verbose, harder to read, boilerplate.
The destructuring + rest syntax offers a cleaner, more readable, and often more performant way compared to these older methods, especially for omitting just a few properties.
Conclusion
The const { propertyToOmit: _, …restOfObject } = sourceObject; syntax is a modern, efficient, and clear pattern for creating a new object that contains all properties of an original object except specified ones.
By leveraging object destructuring to “consume” the unwanted properties and the rest property to collect everything else into a new object, you get an immutable, concise, and readable way to handle object transformations that involve omission.
Embrace this pattern in your JavaScript and TypeScript projects — it’s a hallmark of clean, modern code!