Type Negation: How to Forbid Certain Properties
Tags: typescript, tutorial, javascript, webdev
Published: 2023-04-19 11:00:00

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.

```ts 
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?

```ts
// 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.

```ts 
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.

```ts
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`](https://www.typescriptlang.org/docs/handbook/basic-types.html#never).

Let's go back to our previous example. Now what happens when we specify an object with ID?

```ts
// 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.

![TypeScript Playground](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7c1tfcybxkypx4bsw1pu.jpg)

[<figcaption>TypeScript Playground</figcaption>](https://www.typescriptlang.org/play?target=99#code/C4TwDgpgBAShDOEBOA3CATA0hE8oF4oBvAWACgooBLdALinmCSoDsBzAbnIF9zyAzAK4sAxsCoB7FlBFIIAQ2AQAkkoC2AHgAqqiGqgQAHkpbo8EgEYArCGIB8ACnKUq6+jvUHjEU3jiJUDGxcKAB+KBYINCQod101cgBKOM9SCmp+BwByGizqaVc9RKhgAAskCQB3CIhqgFEkCqQHACJ4qDVBRgiJYBkpYHlWKHlpZQARFsSuMmd+lm7IyvbCImo6KBaAOh2WgBooHa3C-W4ZygB6C-nF2pWodAtjheRgBxPpvnS5YEEkaSW8RmvFmZBEUm6XWQBBkckUKnUDjWNHoLXGAHYAMwADgAbNj9hF5GoIKiAFISUosQl6IYAG1RVkpLAAAkY1PIwHSIFtwWoWlBuJ9QeCFn1eqVoYRZAolPEkUSSeTmTSOVQGZsmVS2YYOVyeXyBUKOEA)

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](https://www.linkedin.com/in/christian-zirkel/) or [GitHub](https://github.com/zirkelc). Thanks for reading!