The keyof
operator produces a union type of all known, public property names of a given type. You can use it together with lookup types (aka indexed access types) to statically model dynamic property access in the type system.
For this lesson, I've created both a simple Todo interface and a Todo object with some properties.
Let's pretend we want to write a prop function that takes an object and a key, and returns the corresponding value. The idea is that we can use our function like this. We can say prop of Todo, and we're going to get the ID property. Of course, we can do the same thing for text, and also for completed.
Right now, all of the variables we just created have the type any. That is because the signature of the prop function takes any object, any key and returns any value. How could we type the prop function a little better?
First, we are going to find a good solution for the specific Todo type, and then later, we're going to develop a generic solution. Let's go ahead and require object to be a Todo, and let's require key to be any of the strings ID, or text, or completed, because those are the only valid property keys that we have to find in our Todo interface.
This type information alone is enough for TypeScript to give us better return type information. The ID variable is typed to be a string, or a number, or a Boolean. The same goes for text and completed.
This is because the prop function now returns either a string, or a number, or a Boolean. We only accept these three keys -- ID, or text, or completed -- which means we can only get one of the following values.
If we pass in the string ID, we would get back a number, whereas if we pass in text, we would get back a string, or if we pass in completed, we would get back a Boolean. That is now encoded in the type system.
Also note that this prevents us from accessing properties that don't exist. If I were to do something like this, I would get a type error saying that argument of type you did is not assignable to a parameter of type ID, or text, or completed. We're trying to use a key that doesn't exist, so we get a type error, which is great.
It's a little cumbersome to spell out all the property names, and it's also not that maintainable, because if we add another property later, we will have to fix that union type here.
What we can do instead is use the keyof operator. Keyof some type represents all property names of that type as a union type. Here, keyof Todo stands for ID, or text, or completed. Our prop function is still specific to Todo's. Let's go ahead and make it generic so we can use it with other types as well.
We're going to create two new type parameters called T and K. T is the type of the object, and K is the type of the key. Now, there is one restriction that we have to impose, and that is that K must extend keyof T. This means that K must be any of the keys that the type T defines.
We get even smarter type inference. ID is now typed to be a number, text is now typed to be a string, and completed is now typed to be a Boolean. All of this works because the prop function is now inferred to have a return type that is written as T and then K in square brackets. This is called a lookup type or a indexed access type. We can also explicitly add a type annotation here.
Using a lookup type, we can find out what type the property K has within the type T. Here's an example. We can create a type alias called Todo ID, which is equal to the type of the ID property within the Todo type.
We basically look up the type of the ID property within the Todo type. If we check line two, we can see that the ID property is of type number. Therefore, Todo ID now stands for the type number. We can do this for other properties as well.
For example, let's do it with the text property. Todo text stands for a string. We can even union several property keys together, which makes this thing really interesting. Todo text or completed, stands for either a string or a Boolean, because that is the union of the two property types up here.
Notice that our prop function is no longer specific to our Todo type. We've replaced every occurrence of Todo with T and K, so we can use this function with any type we like.