Keyof and Typeof page
Learn how to use keyof
and typeof
to create new types from types and objects.
Overview
In this section, you will:
- Use the
keyof
operator. - Use the
typeof
operator. - Use these two operators together.
Objective 1: Use keyof
and typeof
Index signatures
Before we discuss keyof
and typeof
, we need to learn about index signatures.
An index signature defines the structure of an object where you don’t know the property names beforehand, but you do know the type of the values those properties will hold. The index signature specifies the type for the keys (typically strings or numbers) and the type for their corresponding values.
Suppose you want to create an object that stores the number of times a word appears in a document.
You can use a string index signature since the word will be a string
, and the count will be a number
:
interface WordCount {
[word: string]: number;
}
const myWordCounts: WordCount = {
hello: 2,
world: 1,
typescript: 3,
};
In this example, myWordCounts
is an object that can have any number of properties where each key is a string
and each value is a number
.
The keyof
type operator
The keyof
operator takes an object type and produces a union of its keys. For example, take the following type:
type Dino = {
name: string;
type: "herbivore" | "carnivore";
age: number;
};
If we were to create a new type called Dino
using keyof
we would see "name" | "type" | "age"
.
type DinoKeys = keyof Dino; // "name" | "type" | "age"
If the type is declared using an index signature, the type of the index will be used.
type ArraylikeDinos = { [index: number]: Dino };
type Keys = keyof ArraylikeDinos; // number
Note: As you may remember from previously, TypeScript is a superset of JavaScript and has to conform to its rules. This can cause
keyof
to behave in a non-intuitive way in certain situations. Take the following:type VisitedParkMapping = { [parkName: string]: boolean }; type M = keyof VisitedParkMapping;
What is type
M
? Based on the previous, you may thinkM
isstring
; however, itsstring | number
. Huh? Where did thatnumber
come from? In JavaScript all object keys are turned into strings. This means that if we have the following objectconst a = { 0: "hello", 1: "world" };
Both
a[1]
anda["1"]
evaluate to the same thing ("world"
).
By itself, keyof
may not seem all too interesting. However, it becomes powerful when used in tandem with other TypeScript features (like Generics) which we will see in the future.
The typeof
type operator
The typeof
operator is a way to create a type from a value.
It can be used on values and properties of those values.
typeof
is useful for creating type queries and capturing types that aren’t strictly defined.
const tRex = {
name: "tyrannosaurus rex",
type: "carnivore",
weightInKilograms: 7_000,
};
let stegosaurus: typeof tRex;
type Dinosaur = typeof tRex;
Thanks to the use of typeof
, the variable stegosaurus
and the type Dinosaur
both now have the type:
{
name: string;
type: string;
weightInKilograms: number;
}
There are some shortcomings to this approach since it’s looking at a single value, and the type returned isn’t as specific as it could be. For example, take type
and weightInKilograms
, if we were to define the Dinosaur
type ourselves we would want to restrict and expand those properties into something like this:
interface Dinosaur {
name: string;
side: "carnivore" | "herbivore";
weightInKilograms: number | "unknown";
}
The typeof
is often used together with ReturnType
, a utility type provided by TypeScript, as a way to type out the return of a function.
Say, for example, we have a function defined somewhere and we need to give a type for a value that is returned from the function.
We could achieve this using ReturnType
:
const createDinosaur = (): Carnivore | Herbivore => {
/** implementation details */
return dino;
};
type Dinosaur = ReturnType<typeof createDinosaur>;
const dinoFight = (dino1: Dinosaur, dino2: Dinosaur): Dinosaur => {
/** implementation details */
};
Don’t worry about the angle brackets right now (
<>
). Those are how Generics are declared in TypeScript, which we will go more in-depth on those later. For now, just think of it as us telling TypeScript that we want the return type of the thing in the brackets.
You might be thinking using typeof
for something like this is overkill, instead, you could just jump into the module find the types and import them. While that might be true for this simple example, with more complex and generic return types it becomes more of a hassle. Additionally, the return type of a function may not be defined, opting to leverage TypeScript’s type inference like below.
const getDinoFacts = () => {
return {
name: "tyrannosaurus rex",
size: { weight: { amount: 7_000, unit: "kg" } },
info: [
"Tyrannosaurus Rex means 'Tyrant Lizard'",
"The largest T. Rex tooth found is 12 inches (30 cm) long",
],
};
};
type DinoFacts = ReturnType<typeof getDinoFacts>;
For the example above, given the typeof DinoFacts
, the ReturnType
is now:
{
name: string,
size: {
weight: {
amount: number;
unit: string;
}
},
facts: string[]
}
In TypeScript, you’ll encounter two different usages of typeof
—one from JavaScript and another specific to TypeScript. Understanding when and how each is used is crucial for effective type management and code clarity.
JavaScript typeof
: This version of typeof
is used within code expressions. When you apply typeof
to a variable, TypeScript treats it as the JavaScript operator, returning a string that represents the variable’s primitive type.
For example, typeof "hello"
results in "string"
, and typeof 42
results in "number"
. This is consistent with JavaScript’s behavior, where typeof is typically used to determine the type of a runtime value.
TypeScript typeof
: When typeof
is used in a type context—specifically, in type declarations or annotations—it behaves differently. Here, typeof
is used to capture and use the type of a variable rather than its value.
For instance, if you have const x = "hello"
, using type Y = typeof x
in TypeScript will set Y as the type "string"
, effectively using the type of x
rather than its value.
It’s important to note that typeof in TypeScript, particularly in type contexts, has its limitations. You can only use typeof on identifiers (such as variable names) and their properties. Attempting to use typeof on more complex expressions or certain values directly will not work as expected and can lead to errors.
const dinosaur = { name: "velociraptor", type: "carnivore" };
type Dinosaur = typeof dinosaur;
type DinoName = typeof dinosaur.name;
type DinoFacts = typeof getDinoFacts();
So to review, the type Dinosaur
will now have the typeof dinosaur
: {name: string; type: string;}
.
The type DinoName
will have the typeof dinosaur.name
. The name
property of dinosaur
has the type of string
, thus DinoName
’s type is string
.
So what type will DinoFacts
have? As mentioned before, typeof
cannot be called on everything; the getDinoFacts()
function is neither an identifier or property. As a result we get an error: ERROR: Expression expected
.
Setup 1
✏️ Create src/keyof-typeof/dinofacts.ts and update it to be:
export const dinosaurFacts = {
"t-rex": {
latinName: "Tyrannosaurus rex",
nickName: "T-rex",
habitat: "forest",
attributes: {
weight: { amount: 15_500, units: "lbs" },
height: { amount: 12, units: "ft" },
length: { amount: 40, units: "ft" },
},
},
velociraptor: {
latinName: "velociraptor",
nickName: "raptor",
habitat: "desert",
attributes: {
weight: { amount: 100, units: "lbs" },
height: { amount: 1.6, units: "ft" },
length: { amount: 6, units: "ft" },
},
},
};
type DinosaurFacts = any;
type Dinosaur = any;
export const getDinoFact = (
facts: DinosaurFacts,
dino: Dinosaur
): DinosaurFacts[Dinosaur] => {
return facts[dino];
};
Verify 1
✏️ Create src/keyof-typeof/dinofacts.test.ts and update it to be:
import assert from 'node:assert/strict';
import { describe, it } from "node:test";
import { getDinoFact, dinosaurFacts } from "./dinofacts";
describe("Keyof Typeof Exercise 1", () => {
it("gets dino facts", () => {
assert.deepEqual(
getDinoFact(dinosaurFacts, "t-rex"),
dinosaurFacts["t-rex"]
);
assert.deepEqual(
getDinoFact(dinosaurFacts, "velociraptor"),
dinosaurFacts.velociraptor
);
});
});
Exercise 1
Update the DinosaurFacts
and Dinosaur
type to gain type safety on the getDinoFact
function. The function should, given a dinosaur’s name (velociraptor
or t-rex
) and the dinosaurFacts
object, return the correct facts about the dinosaur.
- Replace
DinosaurFacts
’sany
with a type that represents thedinosaurFacts
. - Replace
Dinosaur
’sany
with a type that allows for any of the keys in the dinosaur fact object. - The
getDinofact
should, given afacts
object anddino
name, return the facts for that creature.
Solution 1
If you’ve implemented the solution correctly, the tests will pass when you run npm run test
!
Click to see the solution
✏️ Update src/keyof-typeof/dinofacts.ts to be:
export const dinosaurFacts = {
"t-rex": {
latinName: "Tyrannosaurus rex",
nickName: "T-rex",
habitat: "forest",
attributes: {
weight: { amount: 15_500, units: "lbs" },
height: { amount: 12, units: "ft" },
length: { amount: 40, units: "ft" },
},
},
velociraptor: {
latinName: "velociraptor",
nickName: "raptor",
habitat: "desert",
attributes: {
weight: { amount: 100, units: "lbs" },
height: { amount: 1.6, units: "ft" },
length: { amount: 6, units: "ft" },
},
},
};
type DinosaurFacts = typeof dinosaurFacts;
type Dinosaur = keyof DinosaurFacts;
export const getDinoFact = (
facts: DinosaurFacts,
dino: Dinosaur
): DinosaurFacts[Dinosaur] => {
return facts[dino];
};
Objective 2: Create a union type of an object’s keys
Using the keyof
and typeof
operators together
The keyof
and typeof
operators independently may not seem interesting, but their utility lies when used together with each other.
For example, we may want the keyof
a value, such as the carnivore
object below.
The problem is the keyof
operator only works on types; thankfully, the typeof
operator is to grant us the type of carnivore
.
const carnivore = {
name: "velociraptor",
type: "carnivore",
weightInKilograms: 7_000,
};
type CarnivoreKeys = keyof typeof carnivore;
let carnivoreKey: CarnivoreKeys;
carnivoreKey = "name";
carnivoreKey = "Some value";
carnivoreKey = "type";
Note that in the above example assigning "Some value"
to carnivoreKey
will generate an error:
ERROR: Type '"Some value"' is not assignable to type '"name" | "type" | "weightInKilograms"'
A strange but common occurrence of this is an enum.
Enums in TypeScript are types before the code is compiled but then objects during execution.
If we run into a situation where we want to get the keys of an enum, the only way to do so is to use keyof
and typeof
together:
enum DinosaurColors {
blue = "0x0000FF",
green = "0x00FF00",
red = "0xFF0000",
purple = "0xA020F0",
yellow = "0xFFFF00",
white = "0xFFFFFF",
black = "0x000000",
}
type DinosaurColorsKeys = keyof typeof DinosaurColors;
In the code above, keyof
and typeof
used together give DinosaurColorKeys
the typing:
“blue” | “green” | “red” | "purple | "yellow" | "white" | "black"
In this case, keyof typeof
definitely saves a lot of effort.
Setup 2
✏️ Create src/keyof-typeof/colorshex.ts and update it to be:
export enum Colors {
red = "0xFF0000",
green = "0x00FF00",
blue = "0x0000FF",
}
type ColorNames = any;
export const getColorValue = (color: ColorNames): any => {
return Colors[color];
};
Verify 2
✏️ Create src/keyof-typeof/colorshex.test.ts and update it to be:
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { getColorValue, Colors } from "./colorshex";
describe("Keyof Typeof Exercise 2", () => {
it("gets dino colors", () => {
assert.equal(getColorValue("red"), Colors.red);
assert.equal(getColorValue("green"), Colors.green);
assert.equal(getColorValue("blue"), Colors.blue);
});
});
Exercise 2
Update the ColorNames
type so that it represents the keys of the enum (eg 'red'
, 'blue'
, and 'green'
)and then add all the necessary types to the getColorValue
function signature.
- The
getColorValue
function should take one of color names, orColorNames
and return the hex string equivalent. - Replace
ColorAsEasyReadName
’sany
with a type so that it represents the keys of the enum (eg 'red', 'blue', and 'green'). - Fix the TypeScript errors by updating
getColorValue
’sReturnType
. Replace theany
with the proper type.
Solution 2
If you’ve implemented the solution correctly, the tests will pass when you run npm run test
!
Click to see the solution
✏️ Update src/keyof-typeof/colorshex.ts to be:
export enum Colors {
red = "0xFF0000",
green = "0x00FF00",
blue = "0x0000FF",
}
type ColorNames = keyof typeof Colors;
export const getColorValue = (color: ColorNames): string => {
return Colors[color];
};
Next steps
The next section will cover generics, which are useful for writing both maintainable and flexible TypeScript code.