Key Headaches in TypeScript
Tags: typescript, javascript, tutorial
Published: 2020-06-27 01:19:49

After many years of doing "regular" JavaScript, I've recently (finally) had the chance to get my feet wet in TypeScript.  Despite some people boldly telling me that "I'd pick it up in 5 minutes"... I knew better.  

For the most part it _is_ fast-and-easy to pick up.  But switching to a new paradigm always gets hung up around the _edge cases_.  TypeScript has been no exception to this.  

I already wrote two long posts about the hurdles I had to jump through just to get React/TS to define default prop values under the same conventions that are common (and _easy_) with React/JS.  My latest conundrum has to do with the handling of object keys.
<br/><br/>

## The Problem
When I'm using JavaScript, I frequently have to deal with various objects.  If you've done any JS development, you know I'm not talking about "objects" in the same way that, say, a Java developer talks about "objects".  The majority of JS objects that I seem to encounter are more equivalent to _hashmaps_ - or, on a more theoretical level, _tuples_.  

For example, it's quite common for me to have two objects that might look like this:

```javascript
const user1 = {
  name: 'Joe',
  city: 'New York',
  age: 40,
  isManagement: false,
};

const user2 = {
  name: 'Mary',
  city: 'New York',
  age: 35,
  isManagement: true,
};
```

Nothing too complex there, right?  Those "objects" are just... _data structures_. 

So let's now imagine that I often need to find what any two users have in common (if anything).  Because my app requires this assessment frequently, I want to create a universal function that will accept _any_ two objects and tell me which key values those objects have in common.  

In JavaScript, I could quickly crank out a little utilitarian function like this:

```javascript
const getEquivalentKeys = (object1: {}, object2 = {}) => {
   let equivalentKeys = [];
   Object.keys(object1).forEach(key => {
      if (object1[key] === object2[key]) {
         equivalentKeys.push(key);
      }
   });
   return equivalentKeys;
}
```

[NOTE: I realize that this could be done even more efficiently with, say, a good `.map()` function.  But I think this is a bit clearer (meaning: more _verbose_) for the purposes of this illustration.]

With the function above, I can now do this:

```javascript
console.log(getEquivalentKeys(user1, user2));
// logs: ['city']
```

And the function result tells me that `user1` and `user2` share a common city.  Pretty dang _simple_, right??

So let's convert this to TypeScript:

```typescript
const getEquivalentKeys = (object1: object, object2: object): Array<string> => {
   let equivalentKeys = [] as Array<string>;
   Object.keys(object1).forEach((key: string) => {
      if (object1[key] === object2[key]) {
         equivalentKeys.push(key);
      }
   });
   return equivalentKeys;
}
```

This "looks" right to me, except... TS doesn't like it.  Specifically, TS doesn't like this line:

```typescript
if (object1[key] === object2[key]) {
```

TS says:

`Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{}'.` 

Hmm...

To be clear, I know that I could easily use an interface to define the `user` type and then declare it in the function signature.  But I want this function to work on _any_ objects.  And I understand _why_ TS is complaining about it - but I definitely don't _like_ it.  TS complains because it doesn't know what _type_ is supposed to index a generic `object`.
<br/><br/>

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/7rl4nabd6qcdb3rvbxat.jpg)
## Wrestling With Generics
Having already done Java & C# development, it immediately struck me that this is a use-case for _generics_.  So I tried this:

```typescript
const getEquivalentKeys = <T1 extends object, T2 extends object>(object1: T1, object2: T2): Array<string> => {
   let equivalentKeys = [] as Array<string>;
   Object.keys(object1).forEach((key: string) => {
      if (object1[key] === object2[key]) {
         equivalentKeys.push(key);
      }
   });
   return equivalentKeys;
}
```

But this leads to the same problem as the previous example.  TS still doesn't know that type `string` can be an index for `{}`. And I understand _why_ it complains - because this:

```typescript
const getEquivalentKeys = <T1 extends object, T2 extends object>(object1: T1, object2: T2): Array<string> => {
``` 
Is functionally equivalent to this:

```typescript
const getEquivalentKeys = (object1: object, object2: object): Array<string> => {
```

So I tried some more explicit casting, like so:

```typescript
const getEquivalentKeys = <T1 extends object, T2 extends object>(object1: T1, object2: T2): Array<string> => {
   let equivalentKeys = [] as Array<string>;
   Object.keys(object1).forEach((key: string) => {
      const key1 = key as keyof T1;
      const key2 = key as keyof T2;
      if (object1[key1] === object2[key2]) {
         equivalentKeys.push(key);
      }
   });
   return equivalentKeys;
}
```

Now TS complains about this line again:

```typescript
if (object1[key1] === object2[key2]) {
```

This time, it says that:

`This condition will always return 'false' since the types 'T1[keyof T1]' and 'T2[keyof T2]' have no overlap.`

This is where I find myself screaming at my monitor:
<br/>

> Yes, they _do_ have an overlap!!!

<br/>Sadly, my monitor just stares back at me in silence...

That being said, there _is_ one quick-and-dirty way to make this work:

```typescript
const getEquivalentKeys = <T1 extends any, T2 extends any>(object1: T1, object2: T2): Array<string> => {
   let equivalentKeys = [] as Array<string>;
   Object.keys(object1).forEach((key: string) => {
      if (object1[key] === object2[key]) {
         equivalentKeys.push(key);
      }
   });
   return equivalentKeys;
}
```

Voila!  TS has no more complaints.  But even though _TypeScript_ may not be complaining, _I'm_ complaining - a lot.  Because, by casting `T1` and `T2` as `any`, it basically destroys any of the wonderful magic that we're supposed to get with TS.  There's really no sense in using TS if I'm gonna start crafting functions like this, because _anything_ could be passed into `getEquivalentKeys()` and TS would be none the wiser.

Back to the drawing board...
<br/><br/>

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/z1nqfh1hp1aa7mc9j6v1.jpg)
## Wrestling With Interfaces
Generally speaking, when you want to explicitly tell TS about the _type_ of an object, you use interfaces.  So that leads to this:

```typescript
interface GenericObject {
   [key: string]: any,
}

const getEquivalentKeys = (object1: GenericObject, object2: GenericObject): Array<string> => {
   let equivalentKeys = [] as Array<string>;
   Object.keys(object1).forEach((key: string) => {
      if (object1[key] === object2[key]) {
         equivalentKeys.push(key);
      }
   });
   return equivalentKeys;
}
```

And... this works.  As in, it does exactly what we'd expect it to do.  It ensures that only _objects_ will be passed into the function.  

But I gotta be honest here - it really _annoys the crap outta me_.  Maybe, in a few months, I won't care too much about this anymore.  But right now, for some reason, it truly irks me to think that I have to _tell_ TS that an `object` can be indexed with a `string`.
<br/><br/>

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/yb94siibutr2hzsdhmzi.jpg)
## _Explaining_ To The Compiler
In my first article in this series, the user @miketalbot had a wonderful comment (emphasis: _mine_):
<br/>

> I'm a dyed in the wool C# programmer and would love to be pulling across the great parts of that to the JS world with TypeScript. But yeah, not if I'm going to spend hours of my life trying to _explain to a compiler_ my perfectly logical structure.

<br/>_Well said_, Mike.  Well said.
<br/><br/>

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/wdv3tdagfie32bnitq1e.jpg)
## Why Does This Bother Me??
One of the first things you learn about TS is that it's supposedly a _superset_ of JavaScript.  Now, I fully understand that, if you desire to truly leverage TS's strengths, there will be a lotta "base" JS code that the TS compiler won't like.  

But referencing an object's value by key (a _type:string_ key), is such a simple, basic, _core_ part of JS that I'm baffled to think that I must create a special `GenericObject` interface just to _explain to the compiler_ that: 
<br/>

> Yeah... this object can be indexed by a string.

<br/>I mean, that _works_.  But if that's the way I'm _supposed_ to do this it just makes me think:
<br/>

> Wait... _what_???

<br/>It's the same kinda annoyance I'd have if you told me that I have to _explain_ to TS that a `string` can contain letters _and_ numbers _and_ special characters.

Now that I've figured out how to get around it, I suppose it's just one of those things that you "get used to".  Or... maybe there's some simple technique in TS that would allow me to get around this (without _disabling_ TS's core strengths).  But if that magical solution exists, my paltry googling skills have yet to uncover it.