Using keyof and typeof for types efficiently in TypeScript
In this article, we will learn how to extract and declare constant types by combining the type operators typeof
, keyof
and enums to benefit your codebase.
Prerequisites
For a better coding experience, you should install TypeScript in your IDE, such as VSCode. It will provide you with a lot of essential features such as error highlighting, IntelliSense, linting, etc... You should also install some extensions such as JavaScript and TypeScript Nightly, ESLint, etc.
Understanding typeof
In TypeScript, we can use typeof
to extract the type of a variable or a property, as shown in the below example:
const Name = {
firstName: 'Maya',
lastName: 'Shavin'
};
let myName: typeof Name;
In the above code, we declare the myName
variable with the type extracted from the variable Name
, which will be an object with two properties - firstName
and lastName
. When hovering on myName
in your IDE, TypeScript will show you the type for myName
concluded from Name
as in the following screenshot:
Note here with typeof
, if the variable to extract type is an Object, the type received will be a complete type structure, and each property has its type (such as type of myName
will have two fields - firstName
of type string
, and lastName
of kind string
).
Another valuable scenario for typeof
is combining it with ReturnType
to extract the type of the returned data from a function. To do so, we perform the following:
- Get the function type of the declared function using
typeof
- Use
ReturnType<T>
withT
is the extracted type in step 1 to get the type of the return value fromT
.
The following example demonstrates how we can extract the return type of decoupleName()
function decoupleName (name: string) {
const [firstName, ...remaining] = name.split(" ");
return {
firstName,
lastName: remaining.reduce((text, word) => text ? `${text} ${word}` : word, '')
}
}
type NameObj = ReturnType<typeof decoupleName>
TypeScript will automatically refer to the correct type of NameObj
, shown in the screenshot below:
Additionally, we can use typeof
in TypeScript to act as the type guard within a conditional block, just like in JavaScript, though in this case, it will mainly work for primitive types like string
, object
, function
, etc...
Now we understand typeof
and its usage. Next, we will explore keyof
.
Understanding keyof
While typeof
produces the type represented by a variable, keyof
takes an object type and produces a type that is the liberal union of that variable's keys, as seen below:
interface User {
firstName: string;
lastName: string;
};
type UserKeys = keyof User
The above equals the following declaration:
type UserKeys = "firstName" | "lastName"
However, unlike typeof
, you can't use keyof
directly on a variable. TypeScript will throw an error for the following code:
const Name = {
firstName: 'Maya',
lastName: 'Shavin'
};
type NameKeys = keyof Name; //error
To extract types from object variables, such as an object map of constant values, we need to combine keyof
and typeof
, which we will learn next.
Extract type from an object's keys (or properties)
Take the following ImageVariables
, which acts as a map for variables used in modifying an image, for instance:
export const ImageVariables = {
width: 'w',
height: 'h',
aspectRatio: 'ar',
rotate: 'a',
opacity: 'o',
} as const;
Note here we need the const
at the end of the object to indicate it as read-only. Otherwise, TypeScript won't allow us to extract the types from it due to the risk of modifying the object's internal properties on the go.
ImageVariables
contains the mapping from its keys to matching variable symbols used in transforming an image according to the Cloudinary mechanism. To generate a type based on the ImageVariables
's properties (or keys), we perform the following:
- Get the type representing
ImageVariables
usingtypeof
type ImageVariablesType = typeof ImageVariables
- Extract a new type based on the keys of the kind
ImageVariablesType
, usingkeyof
type ImageFields = keyof ImageVariablesType
Alternatively, we can combine the two steps above into one, as follows:
type ImageFields = keyof typeof ImageVariables
That's it. We now have ImageFields
type, which contains the accepted fields for ImageVariables
, like in the screenshot below:
We can now use this generated type as follows:
const transformImage = (field: ImageFields) => {
const variable = ImageVariables[field]
//do something with it
}
By declaring the type based on properties of ImageVariables
, the flow for any usage of transformImage
is secure, and we can ensure that the field
passed will always need to exist within ImageVariables
. Otherwise, TypeScript will detect and alert the user of any error.
Also, Intellisense will inform the user of the acceptable values, limiting the possibility of passing the wrong value.
On a side note, the type-check behaves similarly to using the hasOwnProperty()
check in runtime, though it only happens in compile time.
Sound straightforward enough. What if we want to extract the values of the keys from ImageVariables
into a new type? Let's look at it next.
Extract type from values of object's keys (or properties)
If we want to generate a type from the values of the keys of ImageVariables
, we can do the following:
type VariableValues = typeof ImageVariables[ImageFields]
Since we already declare ImageVariablesType
as type of ImageVariables
, we can rewrite the above as:
type VariableValues = ImageVariablesType[ImageFields]
With the above code, we now have a new type, VariableValues
which accepts the following values:
Generating type from values and keys of a named constant object is advantageous in many scenarios, such as when you have to work with various data mappings, and standard keys or values are mapping between them. Using keyof
and typeof
on the object mapping can help create the connection between related maps and types, thus avoiding potential bugs.
Alternatively, we can combine enums and typeof
to achieve the same goal.
Bonus: working with enums
Enums are a convenient and organized way to declare named constant types. It allows us to create a set of distinct constant values, and each enum field is either numeric or string-based. We can rewrite our ImageVariables
as follows:
enum EImageVariables {
width = 'w',
height = 'h',
aspectRatio = 'ar',
rotate = 'a',
opacity = 'o',
}
One advantage of using an enum is we can use the name of the enum as the type for accepted values declaration. Hence, instead of the following code:
type VariableValues = typeof ImageVariables[ImageFields]
function transform(value: VariableValues) {
//perform something
}
We can rewrite using EImageVariables
as follows:
function transform(value: EImageVariables) {
//perform something
}
The type-check performs the same for less code ๐. Nevertheless, for getting the type from the keys (or properties) of the declared enum EImageVariables
, we still need to use the combination keyof typeof
like for a regular constant object:
type ImageFields = keyof typeof EImageVariables
That's it. The above code yields the same result as when we use ImageVariables
.
Now let's take a recap of how we get the type from keys and values of a constant object:
export const ImageVariables = {
width: 'w',
height: 'h',
aspectRatio: 'ar',
rotate: 'a',
opacity: 'o',
} as const;
type ImageVariablesType = typeof ImageVariables;
type ImageFields = keyof ImageVariablesType;
type VariableValues = ImageVariablesType[ImageFields];
In comparison with using an enum:
enum EImageVariables {
width = 'w',
height = 'h',
aspectRatio = 'ar',
rotate = 'a',
opacity = 'o',
}
type EImageVariablesType = typeof EImageVariables;
type EImageFields = keyof EImageVariablesType;
//No need for getting the type of values
And same as constant objects, we can use the values of the enum's keys directly, such as EImageVariables.width
in our code. In the runtime, enums exist in your compiled code as JavaScript Objects and behave like one.
In general, enums are okay. In TypeScript, due to the non-efficient way it was implemented (hopefully, this has been fixed or will be short), many argue its impact on performance.
So should we use them? It depends. The choice is up to you.
Summary
We often ignore the type operators like typeof
or keyof
when using TypeScript since they are too primary. However, they play an essential role in building your type system. They can help develop advanced and complex types for your application when combined correctly and with other TypeScript syntax. Let's give them a try and see what they can bring you.
๐ If you'd like to catch up with me sometimes, follow me on Twitter | Facebook.
Like this post or find it helpful? Share it ๐๐ผ ๐