How To Handle This Type Error
Tags: typescript, puzzle, programming
Published: 2023-01-14 02:52:40

---
title: How To Handle This Type Error
published: true
description: TypeScript Puzzle with explanation.
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eshuco2erledei22bpqu.png
tags: TypeScript, Puzzle, Programming
---

# Can you spot the Type Error?

```ts
function getProperty<T>(obj: T, propertyName: string) {
  return obj[propertyName];
} 
```

I posed this question on Twitter recently and it received some interesting responses:

{% embed https://twitter.com/danfletcherdev/status/1613536297850929153?s=20&t=sjv7dFDz0XYa3y00bzQ4-A %}

The solution was intended to be rather simple, and in a way it is.

However there is some interesting nuance to this problem that I hadn't considered until people started commenting. Which then took me on a journey for a much more complicated solution.

For example, this comment, pointed out a flaw in my simplified version:

{% embed https://twitter.com/Nartc1410/status/1613601410586648577?s=20&t=pl1DBnR4PYHuwzN2qQ7Ntw %}

I thought it would be easier to unpack all of this in longform rather than a series of tweets.

# What is the Type Error?

I hope you tried to figure it out for yourself first, but here's the answer:


> Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'unknown'.
  No index signature with a parameter of type 'string' was found on type 'unknown'.


What this error is saying, is that you can't index the `obj` parameter using `propertyName`. Essentially TypeScript is helping you avoid a few mistakes.

## Mistake 1

`obj` might not be an object (remember arrays are objects in JavaScript too) which would cause a runtime error when we try to do `obj[propertyName]`.

For example, what would prevent `getProperty(123, 'name')`? Nothing. But TS prevents it with a Type Error, thankfully 🙏

## Mistake 2

Even if `obj` is an object, it might not have the key that we pass as the `propertyName` argument.

For example:

```ts
const prop = getProperty({name: 'Dan'}, 'age')
```
In this case, `prop` would end up `undefined`, since `'age'` isn't a property of `{name: 'Dan'}` (and TS would infer `any` as the return type from the function by the way -- also not good!)

# The Fix

Now that we can spot the error, and why it's an issue, how can we fix this?

One approach might be to:

## Narrow the type of T
```ts
function getProperty<T extends {}>(obj: T, propertyName: string) {
  return obj[propertyName];
} 
```
What we've done is *narrowed* the type of `T` so that it's more specific. Now it has to be of type `object`.

This solves the first issue mentioned above, however it only changes the message of the Type Error to be:
> Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{}'.
  No index signature with a parameter of type 'string' was found on type '{}'.

So instead what you could do is:
```ts
function getProperty<T extends Record<string, any>(obj: T, propertyName: string) {
  return obj[propertyName];
} 
```

`Record<string, any>` is basically saying that `T` is an object, where the keys are strings, but the values can be anything.

This will eliminate the Type Error and now the code will compile!

But this has a major problem...

```ts
function getProperty<T extends Record<string, any>>(obj: T, propertyName: string) {
  return obj[propertyName];
} 

// prop is now `any`
const prop = getProperty({name: 'Dan'}, 'age')

// Oops! prop is undefined, but no Type Error 😬
const shouldBeString: string = prop
```

As you can see we still have the second issue mentioned above, which is that the `getProperty` method doesn't guarantee that the property we pass exists on the object.

A much better approach is to use `unknown` instead of `any`:
```ts
function getProperty<T extends Record<string, unknown>>(obj: T, propertyName: string) {
  return obj[propertyName];
} 

// prop is now `unknown`
const prop = getProperty({name: 'Dan'}, 'age')

// Prop is still undefined
// But now, TypeScript will force us
// to narrow it's type before we use it.
// So this is a Type Error!
const shouldBeString: string = prop
```
By using `unknown` instead of `any` TS will force us to narrow the type before we use it.

The snippet above will be a Type Error since `unknown` can't be assigned to type `string`, so now you have to write a predicate or assertion to check that `thing` actually is of type `string`.

> 💡**Tip**: In general if you feel the need to use `any` in TypeScript, you should almost *always* use `unknown` instead.

This is useful to have, and I want to come back to this approach but there's another, simpler way to fix the issue.

## Use keyof

The solution that I was trying to tease out of people on Twitter was to use `keyof T` as the type constraint for `propertyName` instead of using `string`.

Here is what that looks like:
```ts
function getProperty<T>(obj: T, propertyName: keyof T) {
  return obj[propertyName];
} 
```

Quick refresher on the `keyof` operator:

In layman's terms, `keyof` is simply saying that a type must have a **property of** the type on the right hand side of the operator.

So for example if `T` is `{name: 'Dan', age: 32}` then `keyof T` is really just a union of string literals like this:
```ts
'name' | 'age'
```

Remember that a string literal such as `'name'` can be a type in TypeScript.

So by constraining `propertyName` to be `keyof T` we're saying that `propertyName` must match one of the keys on the given `obj`.

This solves both of the issues mentioned above!

```ts
function getProperty<T>(obj: T, propertyName: keyof T) {
  return obj[propertyName];
} 

// Now the Type Error is here, where we want it
// 'age' does not exist on type `{name: 'Dan'}`
const prop = getProperty({name: 'Dan'}, 'age')
```

The nice thing about the solution above, is that it also allows the return type to be inferred properly.

Here's a couple examples:
```ts
// type will be 'string' | 'number'
getProperty({name: 'Dan', age: 32}, 'name') 

// type will be 'string' | 'number'
getProperty({name: 'Dan', age: 32}, 'age') 
```
That's pretty neat right!?
<img src="https://media.giphy.com/media/n3p6JiIG0TzCU/giphy-downsized-large.gif">

### This has problems too!

First of all, it would be nice if we received a more specific type back. Rather than `'string' | 'number'` it would be nice if we got the actual type of the property we're pulling out of the object, right?

But that's not the only problem. There's a another issue here too.

This response from Twitter explains it well:
{% embed https://twitter.com/Nartc1410/status/1613601410586648577?s=20&t=pl1DBnR4PYHuwzN2qQ7Ntw %}

So with `keyof T` alone, this function doesn't always cause Type Errors when we might expect it too.

Here are a few examples where it works as expected, and others where it doesn't:

![A screenshot of TypeScript code detailing the examples discussed above](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4q6d0dcoctm0nga8wq92.png)

Why? Because JS is weird and everything that's not an object still kind of acts like one.

For example, try this stuff in your JS console:
```js
'123'['charAt'] // f charAt() { [native code] }
123['toExponential'] // f toExponential() { [native code] }
true['valueOf'] // f toValueOf() { [native code] }
```

So this implementation still has some weird behaviour.

But what if we combined the two approaches?

## Use both extends and keyof

We're almost at the final solution!

Let's try combining both approaches from above and see what happens. But first lets clean this function up a bit. The types are becoming a bit unruly:
```ts
// Start by extracting the type definition to a new type alias
type GetProperty = <T>(obj: T, propertyName: keyof T) 
  => typeof obj[keyof T]

// Then update the function declaration to use the type
const getProperty: GetProperty = (obj, propertyName) => {
  return obj[propertyName];
}
```

Ok great, now we can focus on just the types without the noise of the function itself getting in the way.

So lets add that `<T extends Record<string, unknown>>` bit to this implementation and see what we get:

```ts
type GetProperty = <T extends Record<string, unknown>>
  (obj: T, propertyName: keyof T) 
    => typeof obj[keyof T]

const getProperty: GetProperty = (obj, propertyName) => {
  return obj[propertyName];
}

// These now cause type errors!
getProperty('123', 'charAt')
getProperty(123, 'toExponential')
getProperty(true, 'valueOf')

// works as it did above but type is still 'string' | 'number'
const age = getProperty({name: 'Dan', age: 32}, 'age')
```

Now we've addressed every issue that's been pointed out except for one!

It would be nice if when we used the `getProperty` function that the result would be the type of the property on the original object.

For example I would expect that:
```ts
let age = getProperty({name: 'Dan', age: 32, admin: true}, 'age')
```

Would actually return a type `number` and not `'string' | 'number' | 'boolean'`.

In order to do that, we can add a second type parameter to our generic called `K` which can `extends` the `keyof T` - meaning that `K` must be a type in the key of `T`.

Like this:
```ts
type GetProperty = <T extends Record<string, unknown>, K extends keyof T>
  (obj: T, propertyName: keyof T) => typeof obj[keyof T]
```

Ok, so this hasn't really changed anything yet. We have to actually make use of `K`!

We want the second argument to this function to be `K` and the return to be `T[K]`, like this:

```ts
type GetProperty = <T extends Record<string, unknown>, K extends keyof T>
  (obj: T, propertyName: K) => T[K]
```

So now when we use this function it has none of the issues discussed above, plus we get a return type that's been narrowed to it's most specific type (like `number` for example):
```ts
type GetProperty = <T extends Record<string, unknown>, K extends keyof T>
  (obj: T, propertyName: K) => T[K]

const getProperty: GetProperty = (obj, propertyName) => {
  return obj[propertyName];
}

// works as it did above but now, type is a number!!
let age = getProperty({name: 'Dan', age: 32, admin: true}, 'age')
```

# Conclusion

I've been on a mission to level up my competence in TypeScript lately. So learn and practice something new every morning before work (doing the #100DaysOfCode challenge).

But sharing this journey in public and posing questions like the original Tweet it's really helped bring a lot of depth to my learning.

I honestly thought that the answer to my question was an easy one, and it turned out to have a way deeper answer than I anticipated.

I hope you enjoyed this little journey that I went on, and maybe even deepened your understanding of TypeScript too!

I couldn't have written this article alone so kudos to these gems:
- https://twitter.com/Nartc1410 (for the insights and poking holes in my logic)
- https://twitter.com/bitknight (for proof reading, insights and suggestions)
- https://twitter.com/rgolea (suggesting solutions)

---

Like what you read? Want to support me?

A cup of coffee goes a long way 🙏

Why not buy me one? https://ko-fi.com/danjfletcher