TypeScript explained in JavaScript: keyof
1st post in "TypeScript explained in JavaScript" series
Spencer Miskoviak
May 10, 2020
Photo by Samantha Lam
This is the first post in a series aimed at exploring powerful but potentially confusing aspects of TypeScript using more familiar patterns in JavaScript to provide analogies. This series will assume you have some familiarity with Javascript but only the basics of TypeScript. If you've gone through TypeScript Tooling in 5 minutes and were able to understand most of it, you should be in the perfect place!
This series will start with keyof
since it will be a necessary building block
for many other concepts.
JavaScript
But before talking about that, let's start with some JavaScript:
const user = {
firstName: "Type",
lastName: "Script"
};
const properties = Object.keys(user);
console.log(properties); // [ "firstName", "lastName" ]
Here we are defining a user
object and giving that user a first and last
name. We are then using the
Object.keys
method available since ES5. This returns an array of the object's properties (or keys).
In this example that will be ["firstName", "lastName"]
.
How could this be used in practice? Let's say you have a helper method that accepts an object and property name and will return the value for the given object and key. However, you only want to allow valid keys that exist for the given object. To handle this, it may look something like the following:
const getProperty = (obj, key) => {
const keys = Object.keys(obj);
if (keys.indexOf(key) === -1) {
throw new Error(`Unexpected key "${key}" does not exist in keys: ${keys}`);
}
return obj[key];
};
console.log(getProperty(user, "firstName"));
// "Type"
console.log(getProperty(user, "lastName"));
// "Script"
console.log(getProperty(user, "middleName"));
// Unexpected key "middleName"" does not exist in keys: firstName,lastName
This getProperty
helper only accepts a key
argument that exists in the passed
obj
. However, this is a runtime check, meaning this error isn't seen until
the code actually runs. Could we move this earlier, to compile time before
even running the code?
TypeScript
This is where the keyof
operator comes in. It conceptually behaves identical
to the Object.keys
method, but is a type instead of a literal value. To
start, let's define a User
interface that describes the user
object.
interface User {
firstName: string;
lastName: string;
}
const user: User = {
firstName: "Type",
lastName: "Script"
};
Now translating the JavaScript Object.keys(user)
from above to TypeScript
would now be keyof User
.
type Keys = keyof User; // "firstName" | "lastName"
This returns the union type of all the properties in the User
interface.
The pipe operator (|
) can be thought of as "or", so this says that
Keys
will be firstName
or lastName
. This is exactly what we want to enforce
at compile time. So building off this, the getProperty
method can be updated
to take advantage of this.
const getProperty = <Obj, Key extends keyof Obj>(obj: Obj, key: Key) =>
obj[key];
This getProperty
method now has two generic types: Obj
and Key
that
correspond to the two arguments. However,
this definition is constraining Key
by saying it must extend the value
of keyof Obj
. In the case of passing user
to this method, keyof Obj
is
firstName
or lastName
. This means Key
must be firstName
or lastName
.
A later post will cover extends
in more depth.
Now using it catches the same error, but now at compile time! This means you can get this feedback directly in your editor, one of the great values of TypeScript.
console.log(getProperty(user, "firstName"));
// "Type"
console.log(getProperty(user, "lastName"));
// "Script"
console.log(getProperty(user, "middleName"));
// Argument of type '"middleName"' is not assignable to parameter of type '"firstName" | "lastName"'
Definition
Now that we have a rough analogy of behavior, what's the formal definition?
For any type
T
,keyof T
is the union of known, public property names ofT
Or, for the type User
, keyof User
is the union of known, public properties
of User
, which are: "firstName" | "lastName"
.
The keyof
operator (also known as the index type query operator) was originally introduced in
TypeScript 2.1.
Conclusion
In summary, you can roughly relate TypeScript's keyof
behavior to JavaScript's
Object.keys
behavior. The keyof
operator is one of the building blocks for more
complex typings used in conjunction with other concepts such as conditional
types or mapped types that
are be covered in later posts.
Tags:
course
Practical Abstract Syntax Trees
Learn the fundamentals of abstract syntax trees, what they are, how they work, and dive into several practical use cases of abstract syntax trees to maintain a JavaScript codebase.
Check out the course