Type negation in TypeScript allows you to create types that explicitly exclude certain properties. Usually, we define types that specify what properties an object must have to satisfy that type. With type negation, we want to do the opposite: We specify which properties an object must not have. You can think of this as reserved properties.
Let's consider the following example. We have a generic createItem
function that inserts a new item into our NoSQL database (e.g. MongoDB, DynamoDB, etc.). The NoSQL database and its tables do not have a defined column schema, that means the item with all its properties is stored as is. However, in order to retrieve the items, we need to define at least one property as a primary key (e.g. in DynamoDB this is the hash key). This is usually an ID as an integer or UUID, which can be defined either by the database or the application.
function createItem<TItem extends object>(item: TItem): TItem {
// Database sets ID
const newItem = db.insert(item);
return newItem;
}
// Returns object with id { id: "0d92b425efc9", name: "John", ... }
const user = createItem({ name: "John", email: "john@doe.com" });
We can call this function with any JavaScript object and store it in our NoSQL database. But what happens if the object we pass in contains an id
property?
// What will happen?
//> Will it create a new item?
//> Will it overwrite the item if it exists?
const user = createItem({ id: "0d92b425efc9", name: "John", email: "john@doe.com" });
What actually happens depends on the database, of course. A new item could be created, or an existing item could be overwritten, or even an error could be thrown if we are not supposed to specify an external ID. Of course, we can simply add a check to the id
property and raise an error ourselves to prevent such cases.
function createItem<TItem extends object>(item: TItem): TItem {
if('id' in item) throw new Error("Item must not contain an ID");
// Database sets ID
const newItem = db.insert(item);
return newItem;
}
// Throws an error
const user = createItem({ id: "0d92b425efc9", name: "John", email: "john@doe.com" });
Let's go one step further and use TypeScript generics and types to prevent such cases from happening in the first place. We simply forbid the item to contain an id
property.
type ReservedKeys = {
id: string;
}
function createItem<TItem extends object>(
item: TItem extends ReservedKeys ? never : TItem
): TItem {
if('id' in item) throw new Error("Item must not contain an ID");
// Database sets ID
const newItem = db.insert(item);
return newItem;
}
In this example we define a ReservedKeys
type with forbidden keys. These are the properties that should not be allowed for the item. In the function signature, we then use TItem extends ReservedKeys
to check if the generic TItem
is a subset of ReservedKeys
. If it is, we set the element type to the special value never
.
Let's go back to our previous example. Now what happens when we specify an object with ID?
// What will happen?
//> TypeScript error: Argument of type '{ id: string; name: string; email: string; }' is not assignable to parameter of type 'never'
const user = createItem({ id: "0d92b425efc9" name: "John", email: "john@doe.com" });
TypeScript reports an error that the object we passed to the function doesn't match the expected type.
Of course, we should never rely on static type checking alone to avoid such errors. Checking the property id
at runtime within the implementation should always be present. The type negation is rather syntactic sugar to catch possible errors already at compile time and to have a function signature that matches the implementation.
I hope you found this post helpful. If you have any questions or comments, feel free to leave them below. If you'd like to connect with me, you can find me on LinkedIn or GitHub. Thanks for reading!
Top comments (4)
That works for a single key, but you need to use
Partial<ReservedKeys>
to support multiple reserved keys. In addition, you can even use the types to create a helpful error message:This will give you a helpful Error message whenever you use one or more of the reserved keys:
You can have a look at this example in the TS Playground.
That's a very good point, thank you!
It would be helpful if TS were to support these static type error message natively, for example with a generic
never<
error message>
type or some// @ts-throw-error
comment.Yes, that would be really helpful. A comment won't work, though, because the error is relative to other types.
Nice, just what I needed