"in" operator considered harmful in Typescript
Tags: typescript, javascript, pattern, bestpractices
Published: 2023-10-18 22:45:56

Let say you have two types of users in your app, regular users and superUsers.

```ts
type RegularUser = {
  id: string;
  username: string;
  email: string;
}

type SuperUser = {
  id: string;
  username: string;
  email: string;
  isSuperUser: true; //for demonstration only, don't do this
}

```

You then sum them in a union type `User`

```ts
type User = RegularUser | SuperUser;
```

Now, when you receive a `User`, how do you discriminate between `RegularUser` and `SuperUser` ?

```ts
function isSuperUser(user: User){
  return user.isSuperUser; //error
}
```
That won't work as TS doesn't allow to access properties that are not present on all types of a union type.

The usually recommended way is to use the `in` Javascript operator.

```ts
function isSuperUser(user: User){
  return "isSuperUser" in user;
}
```

The `in` operator ensure at runtime that `user` has the `isSuperUser` property.

One interesting Typescript refinement (which in this case is called _narrowing_), is that the compiler is clever enough to know which member of the union we have, in the part of the code _garded_ by the `in` check.

```ts

function regularUserAction(user: RegularUser){
  //do something with RegularUser
}

function superUserAction(user: SuperUser){
  //do something with SuperUser
}

function userTypeActionSwitching(user: User){
  if ("isSuperUser" in user){
    //user is narrowed to SuperUser
    superUserAction(user);//compiler happy
  } else {
    //user is narrowed to RegularUser
    userAction(user);
  }
}
```

End of story ? I don't think so.

Let say, we now decide to refactor our code and use a _tagged union_ (also called _discriminated union_) instead.
Good idea !

```ts
type RegularUser = {
  id: string;
  name: string;
  email: string;
  type: "regular";//here is the tag 
}

type SuperUser = {
  id: string;
  name: string;
  email: string;
  type: "super";//another tag
}
```

We will probably have to rewrite parts of our code to take into account those changes. Fortunately we used a type system to ensure that we didn't forget anything. We will see a lot of red lines in our editor that will prevent to ship bogus code, right ?
Wrong !

Let's look at the consumer of our `User` union

```ts
function isSuperUser(user: User){
  return "isSuperUser" in user;//No Error !
}
```

Although neither `RegularUser` nor `SuperUser` has an `isSuperUser` field anymore, the compiler doesn't mind.

Why is that ?

What is happening with the `in` operator is that it gives us the warranty that the code running after the check will have the tested property.

```ts
function getUserName(user: User){
    if ("userName" in user){
        return user.userName; //no error reported
    }
    return "";
}
```
Here, altough we made a typo writing `userName` instead of `username`, the compiler won't complain. If `user` ends up not having a `userName` field at runtime, there won't be a problem since the first return will never run.
However, this is probably not what we want, and we wrote dead code that will never run.
The Typescript Langage Service (the software that is providing our editors with data enabling autocomplete) is even fooling us by showing a `userName` entry while typing. For him there is a `userName` field, since we asserted it with the `in` check.

We all know that _type assertions_ which we sometimes have to use, with the `as` operator, are dangerous.

But while type assertions doesn't allow everything

```ts
function wontWork(){
    const obj = {
        foo: "foo",
        bar: "bar"
    } as {
        foo: string,
        baz: string //baz instead of bar ,compiler will complain
    }
    return obj.baz;
}
```

With the `in` operator, the compiler doesn't care

```ts
function willWork(){
    const obj = {
        foo: "foo",
        bar: "bar"
    };
    if ("baz" in obj){ //baz instead of bar, but no error
        return obj.baz;
    }
}
```

Using the `in` operator is like using the dreaded `as unknown as`

```ts
function willWorkButDangerous(){
    const obj = {
        foo: "foo",
        bar: "bar"
    } as unknown as {  
        foo: string,
        baz: string
    };
    return obj.baz; //securities disabled, no error
}
```

As with `any`, you know that when you use `as unknown, as`, you are on your own.

Were you aware that it was the same with `in` ?

## Conclusion

My opinion, which you are welcome to challenge, is that the `in` operator is an anti-pattern in Typescript.

It is true that code which is guarded by a `in` operator won't be able to run if `in` checks for property that doesn't exist.

But it can lead to buggy programs with parts of the code that end up being dead without the compiler warning us.

This is not a mistake by the TS team since Javascript and Typescript have a _structural_ (or duck typed) type system which guarantees lower bound (presence of properties) but not upper bound (no superfluous properties) correctness.