Keyof and Typeof page

Learn how to use keyof and typeof to create new types from types and objects!

Overview

In this part, we will:

  • Learn how keyof works
  • Learn how typeof works
  • See how they can be used together

Keyof

The keyof operators takes an object type and produces a union of its keys. Take for example 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 think M is string; however, its string | number. Huh? Where did that number come from? In JavaScript all object keys are turned into strings. This means that if we have the following object

const a = { 0: "hello", 1: "world" };

Both a[1] and a["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 Mapped Types and Generics which we will see in the future.

Typeof

typeof 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; // {name: string; type: string; weightInKilograms: number;}

type Dinosaur = typeof tRex; // {name: string; type: string; weightInKilograms: number;}

There are some shortcomings to this approach since its looking at a single value, and the type returned isn’t as specific as it could be. Take for example 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";
}

typeof is often used in conjunction with ReturnType, a utility type (learn more about utility types: Utility Types) 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

// Some function in a module
const createDinosaur = (): Carnivore | Herbivore => {
  // implementation details
  return dino;
};

// Some other module
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, we will go more indepth 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",
    ],
  };
};

/**
 *{name: string, size: {weight: {amount: number; unit: string;}}, facts: string[]}
 */
type DinoFacts = ReturnType<typeof getDinoFacts>;

There are two typeofs to be aware of in TypeScript – the JavaScript one and the TypeScript one. The difference between them is the context in which they are used. If it is being used in an expression context, (used as a part of your code) it is the JavaScript typeof and will return a string with one of nine JavaScript types. And if it's used in a type context (as part of your type declarations) it's the TypeScript typeof and will refer to the type of whatever values follow it.

typeof cannot be called on everything; there are some restrictions. typeof can only be called on identifiers and any of their properties.

const dinosaur = { name: "velociraptor", type: "carnivore" };

type Dinosaur = typeof dinosaur; // {name: string; type: string;}
type DinotName = typeof dinosaur.name; // string

type DinoFacts = typeof getHothFacts(); // ERROR: Expression expected

Like keyof, typeof by itself may not seem interesting, but its utility lies when used in conjunction with other TypeScript features like ReturnType and even keyof.

Keyof Typeof

typeof and keyof can be used together when you want to use keyof but only have a value to use rather than a concrete type.

const carnivore = {
  name: "velociraptor",
  type: "carnivore",
  weightInKilograms: 7_000,
};

type CarnivoreKeys = keyof typeof carnivore;

let carnivoreKey: CarnivoreKeys;

carnivoreKey = "name";
carnivoreKey = "Some value"; // ERROR: Type '"Some value"' is not assignable to type '"name" | "type" | "weightInKilograms"'
carnivoreKey = "type";

A strange, but the common occurrence of this is an enum (learn more about enums: Types). Enums in TypeScript are types before the code is compiled and an object 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; // “blue” | “green” | “red” …

Exercises

Exercise 1

Update the DinosaurFactObject and Dinosaur type to gain type safety on the getDinoFact function. The function should, given a dinosaurs name (velociraptor or t-rex) and the dinosaurFacts object return the correct facts about the dinosaur.

NOTE: Don't worry about the DinosaurFactObject[Dinosaur] type in the return of the function signature. That's called an index-signature which we will get into later on.

/**
 * EX 1: Update the `DinosaurFactObject` and `Dinosaur` type to gain type safety on the `getDinoFact`
 * function. The function should, given a dinosaurs name (`velociraptor` or `t-rex`) and the `dinosaurFacts` object
 * return the correct facts about the dinosaur.
 */
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" },
    },
  },
};

/**
 * Replace `any` with a type that represents the `dinosaurFacts` object above
 */
type DinosaurFactObject = any;

/**
 * Replace `any` with a type that allows for any of the keys in the dinosaur fact object
 */
type Dinosaur = any;

/**
 * This function should, given a facts object and dinosaur name, return the facts for that creature.
 *
 * >NOTE: Don't worry about the `DinosaurFactObject[Dinosaur]` type in the return of the function signature. That's called
 * and index-signature which we will get into later on.
 */
export const getDinoFact = (
  facts: DinosaurFactObject,
  dino: Dinosaur
): DinosaurFactObject[Dinosaur] => {
  return facts[dino];
};

Verify Your Solution

✏️ Run the following to verify your solution:

npm run 5c-keyof-typeof-ex1

The Solution

Click to see the solution

Update the DinosaurFactObject and Dinosaur type to gain type safety on the getDinoFact function

/**
 * EX 1: Update the `DinosaurFactObject` and `Dinosaur` type to gain type safety on the `getDinoFact`
 * function. The function should, given a dinosaurs name (`velociraptor` or `t-rex`) and the `dinosaurFacts` object
 * return the correct facts about the dinosaur.
 */
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" },
    },
  },
};

/**
 * Replace `any` with a type that represents the `dinosaurFacts` object above
 */
type DinosaurFactObject = typeof dinosaurFacts;

/**
 * Replace `any` with a type that allows for any of the keys in the dinosaur fact object
 */
type Dinosaur = keyof DinosaurFactObject;

/**
 * This function should, given a facts object and dinosaur name, return the facts for that creature.
 *
 * > NOTE: Don't worry about the `DinosaurFactObject[Dinosaur]` type in the return of the function signature. That's called
 * an index-signature which we will get into later on.
 */
export const getDinoFact = (
  facts: DinosaurFactObject,
  dino: Dinosaur
): DinosaurFactObject[Dinosaur] => {
  return facts[dino];
};

Another way to achieve the same solution.

export type DinosaurFactObject = typeof dinosaurFacts;

export type Dinosaur = keyof typeof dinosaurFacts;

Exercise 2

Update the ColorsAsEasyReadName 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 a one of the easily readable names and return the hex string equivalent

/**
 * EX 2: Update the `ColorsAsEasyReadName` 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 a one of the easily readable names and return the hex string equivalent
 */
export enum ColorsToHex {
  red = "0xFF0000",
  green = "0x00FF00",
  blue = "0x0000FF",
}

/**
 * Replace `any` with a type so that it represents the keys of the enum (eg 'red', 'blue', and 'green')
 */
type ColorsAsEasyReadName = any;

/**
 * Fix the ts errors by updating the type above and replace the `any` with the proper type
 */
export const getColorValue = (color: ColorsAsEasyReadName): any => {
  return ColorsToHex[color];
};

Verify Your Solution

✏️ Run the following to verify your solution:

npm run 5d-keyof-typeof-ex2

The Solution

Click to see the solution

Update the ColorsAsEasyReadName 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.

/**
 * EX 2: Update the `ColorsAsEasyReadName` 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 a one of the easily readable names and return the hex string equivalent
 */
export enum ColorsToHex {
  red = "0xFF0000",
  green = "0x00FF00",
  blue = "0x0000FF",
}

/**
 * Replace `any` with a type so that it represents the keys of the enum (eg 'red', 'blue', and 'green')
 */
type ColorsAsEasyReadName = keyof typeof ColorsToHex;

/**
 * Fix the ts errors by updating the type above and replace the `any` with the proper type
 */
export const getColorValue = (color: ColorsAsEasyReadName): string => {
  return ColorsToHex[color];
};