# Type Checking Object Indexes in TypeScript

Typescript is a powerful tool, but it has its limitations; sometimes we as humans know better than the computer does on how to use it. This is why we have type predicates - as a sort of escape hatch.

Ben Lokash

Nov 16, 2022

·

5 min read

[technology](/content/blog_filter-technology/index.html)

How can we use an arbitrary `string` or `number` to index an object’s properties in a type-safe manner? Read on to find out. Or skip to the end and copy-paste. We won’t be offended.

Let’s say we have the following object:

type-checkings objects

```javascript
const accountingCategory = {
  4: 'Inventory',
  8: 'Capital Asset',
  15: 'Expense Item',
}
```

This object maps the numeric id of an “accounting category” onto its associated label. We want to write a function that will convert some arbitrary number into its associated label. Say we pass in the number `4` \- then the function returns the string `'Inventory'`. But say we pass in the number `16` \- in this case we want our function to return the number `16`. Seems simple enough - we can write it using just one line:

```javascript
const getAccountingCategory = (categoryId: number) =>
  accountingCategories[categoryId] ?? categoryId
```

However, Typescript isn’t happy with this.

```
Element implicitly has an 'any' type because expression of type 'number' can't be used to index type '{ 4: string; 8: string; 15: string; }'.
  No index signature with a parameter of type 'number' was found on type '{ 4: string; 8: string; 15: string; }'.ts(7053)
```

Typescript is correct to warn us here. `categoryId` could be any number, but our `accountingCategories` object is only equipped to handle `4` , `8` or `15`.

We should be able to [narrow](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) the type of `categoryId` with a simple condition:

```javascript
const getAccountingCategory = (categoryId: number) => {
  if (categoryId in accountingCategories) {
    return accountingCategories[categoryId]
  }
  return categoryId
}
```

However, typescript still reports the same error. It is not able to recognize that we have already checked whether or not `categoryId` is a key of `accountingCategories`. We expect `categoryId` to have been narrowed to `4 | 8 | 15` , but it still thinks that `categoryId` can be any number.

No matter how we implement the condition…

```javascript
if (accountingCategories.hasOwnProperty(categoryId)) {
```

```javascript
// must cast categoryId to string because Object.keys returns string[]
if (Object.keys(accountingCategories).includes(categoryId.toString()) {
```

… we still get the same error.

Turns out there is a simple way to fix this, with something called _Index Signatures_. From the [TypeScript documentation:](https://www.typescriptlang.org/docs/handbook/2/objects.html#index-signatures)

> Sometimes you don’t know all the names of a type’s properties ahead of time, but you do know the shape of the values. In those cases, you can use an index signature to describe the types of possible values.

We can change the type of `accountingCategories` to allow it to be indexed by any number:

```javascript
const accountingCategories: { [index: number]: string } = {
  4: 'Inventory',
  8: 'Capital Asset',
  15: 'Expense Item',
}
```

Now our original function…

```javascript
const getAccountingCategory = (categoryId: number) =>
  accountingCategories[categoryId] ?? categoryId
```

…won’t cause any Typescript errors.

This solution is sufficient for eliminating the error, but it is not 100% correct. It introduces some curious behaviour. Consider the following lines:

```javascript
const x = accountingCategories[15]
const y = accountingCategories[23]
```

Since `accountingCategories` is constant and was defined with key `15` set to value `'Expense Item'`, it seems reasonable to expect the type of `x` to be `"Expense Item"`. And since `23` was not defined, we would expect the type of `y` to be `undefined`. However, Typescript is not able to figure it out, and they are both typed as `string | undefined`.

We can do better. We were on the right track when we tried to narrow the type of `categoryId` down. Let’s go back to that version of our function.

We know that `accountingCategories` can only be indexed by `4`, `8`, or `15`. We need to find a way to narrow the type of `categoryId` down to `4 | 8 | 15`. Then Typescript will allow us to use it to index `accountingCategories`.

We can implement the desired behaviour using a [_type predicate_](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates) _._ Type predicates allow us to manually define the logic that Typescript will use to narrow the type of a variable.

```javascript
function isAccountingCategory(categoryId: number): categoryId is 4 | 8 | 15 {
  return [4, 8, 15].includes(categoryId)
}

const getAccountingCategory = (categoryId: number) => {
  if (isAccountingCategory(categoryId)) {
    return accountingCategories[categoryId]
  }
  return categoryId
}
```

This works exactly as we expect. Inside the `if` block, the type of `categoryId` is `4 | 8 | 15` and we can use it to index `accountingCategory` without issue. However, we have repeated `4`, `8`, `15` several times. We can DRY this code out a bit.

Let’s start by defining the type of the index of `accountingCategories`

```javascript
type AccountingCategory = 4 | 8 | 15

const accountingCategories: { [index in AccountingCategory]: string } = {
  4: 'Inventory',
  8: 'Capital Asset',
  15: 'Expense Item',
}
```

So, technically we have written `4`, `8`, `15` twice, but at least it is type-checked both ways.

If we try to add a key to `accountingCategories` that isn’t part of `AccountingCategory` we will get an error:

```
Object literal may only specify known properties, and '16' does not exist in type '{ 4: string; 8: string; 15: string; }'.ts(2322)
```

And if we try to add another value to `AccountingCategory` without adding a matching key to `accountingCategories`:

```
Property '16' is missing in type '{ 4: string; 8: string; 15: string; }' but required in type '{ 4: string; 8: string; 15: string; 16: string; }'.ts(2741)
```

Let’s incorporate the `AccountingCategory` type and `accountingCategory` into our type predicate

```javascript
function isAccountingCategory(categoryId: number): categoryId is AccountingCategory {
  return accountingCategory.hasOwnProperty(categoryId)
}
```

Side note: It is tempting to use the `in` keyword instead of `hasOwnProperty`:

```javascript
function isAccountingCategory(categoryId: number): categoryId is AccountingCategory {
  return categoryId in accountingCategories
}
```

Doesn’t that look clean! However, the `in` keyword returns true for properties in the [prototype chain as well as in the specified object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/in):

```javascript
'constructor' in accountingCategories // true
'__proto__' in accountingCategories // true
'hasOwnProperty' in accountingCategories // true
```

This means that unfortunately there are very few cases where it would be correct to use `in` over `hasOwnProperty`.

Finally, we can use `isAccountingCategory` to narrow the type of `categoryId`:

```javascript
const getAccountingCategory = (categoryId: number) => {
  if (isAccountingCategory(categoryId)) {
    return accountingCategories[categoryId]
  }
  return categoryId
}
```

Inside the `if` statement, the type of `categoryId` has been correctly narrowed to our `AccountingCategory` type. We can use it to index `accountingCategories` without any issues.

Type inference in Typescript is extremely powerful and it is best practice to rely on it as much as possible. However, limitations exist and sometimes we as humans know better than the computer. This is why we have type predicates - as a sort of escape hatch. However, you must be careful when implementing them. Typescript won’t be able to check that your predicate makes sense - if it were able to do that we wouldn’t need them in the first place! Just remember to be mindful of the typical Javascript gotchas - like the fact that `0` and `''` are falsy.

Full code snippet:

```javascript
type AccountingCategory = 4 | 8 | 15

const accountingCategories: { [index in AccountingCategory]: string } = {
  4: 'Inventory',
  8: 'Capital Asset',
  15: 'Expense Item',
}

function isAccountingCategory(categoryId: number): categoryId is AccountingCategory {
  return accountingCategory.hasOwnProperty(categoryId)
}

written by

Ben LokashSenior Software Engineer, Quantum Mob

###### Enjoyed this post?

###### Newsletter Sign-up

Receive summaries directly in your inbox.

Email

Sign up

We are a Toronto-based end-to-end digital innovation firm with a passion for building beautiful & functional products that deliver results.

[Hire us](/content/contact/index.html)

You might also like

\\
\\
Blog\\
\\
Your Organization Needs to Be Digital-First, Now!](/content/blog/how-to-make-jenkins-build-nodejs-ruby-and-maven-on-docker/index.html)

\\
\\
Blog\\
\\
The Benefits of React Native](/content/blog/covid-19-business-implications-challenges-and-resolutions/index.html)

\\
\\
Blog\\
\\
Discovering Accessibility in Design](/content/blog/technologies-you-should-consider-in-your-2023-tech-stack/index.html)

\\
\\
Launch a new business\\
\\
Validate an idea and launch a new product.](/content/contact?intent=project&projectIntent=business/index.html) \\
\\
Improve existing software\\
\\
Fix performance and improve user experiences.](/content/contact?intent=project&projectIntent=software/index.html)

312 Adelaide St. W, Suite 800

Toronto, ON M5V 1R2

- [Careers](/content/about/index.html)
- [Thought Leadership](/content/insights/index.html)
- [Privacy Policy](/content/privacy/index.html)
- [Contact](/content/contact/index.html)

- [hello@qmo.io](mailto:hello@qmo.io)
- [1 (877) 797-1927](tel:18777971927)
