Chapter 10

Deriving Types

Explore TypeScript's advanced type derivation techniques: keyof, typeof, indexed access types, and as const for enums.

10

One of the most common pieces of advice for writing maintainable code is to "Keep code DRY", or more explicitly, "Don't Repeat Yourself".

One way to do this in JavaScript is to take repeating code and capture it in functions or variables. These variables and functions can be reused, composed and combined in different ways to create new functionality.

In TypeScript, we can apply this same principle to types.

In this section, we're going to look at deriving types from other types. This lets us reduce repetition in our code, and create a single source of truth for our types.

This allows us to make changes in one type, and have those changes propagate throughout our application without needing to manually update every instance.

We'll even look at how we can derive types from values, so that our types always represent the runtime behavior of our application.

Derived Types

A derived type is a type which relies on, or inherits from, a structure of another type. We can create derived types using some of the tools we've used so far.

We could use interface extends to make one interface inherit from another:

interface Album {
  title: string;
  artist: string;
  releaseYear: number;
}

interface AlbumDetails extends Album {
  genre: string;
}

AlbumDetails inherits all of the properties of Album. This means that any changes to Album will trickle down to AlbumDetails. AlbumDetails is derived from Album.

Another example is a union type.

type Triangle = {
  type: "triangle";
  sideLength: number;
};

type Rectangle = {
  type: "rectangle";
  width: number;
  height: number;
};

type Shape = Triangle | Rectangle;

A derived type represents a relationship. That relationship is one-way. Shape can't go back and modify Triangle or Rectangle. But any changes to Triangle and Rectangle will ripple through to Shape.

When well-designed, derived types can create huge gains in productivity. We can make changes in one place and have them propagate throughout our application. This is a powerful way to keep our code DRY and to leverage TypeScript's type system to its fullest.

This has tradeoffs. We can think of deriving as a kind of coupling. If we change a type that other types depend on, we need to be aware of the impact of that change. We'll discuss deriving vs decoupling in more detail at the end of the chapter.

But for now, let's look at some of the tools TypeScript provides for deriving types.

The keyof Operator

The keyof operator allows you to extract the keys from an object type into a union type.

Starting with our familiar Album type:

interface Album {
  title: string;
  artist: string;
  releaseYear: number;
}

We can use keyof Album and end up with a union of the "title", "artist", and "releaseYear" keys:

type AlbumKeys = keyof Album; // "title" | "artist" | "releaseYear"

Since keyof tracks the keys from a source, any changes made to the type will automatically be reflected in the AlbumKeys type.

interface Album {
  title: string;
  artist: string;
  releaseYear: number;
  genre: string; // added 'genre'
}

type AlbumKeys = keyof Album; // "title" | "artist" | "releaseYear" | "genre"

The AlbumKeys type can then be used to help ensure a key being used to access a value in an Album is valid as seen in this function:

function getAlbumDetails(album: Album, key: AlbumKeys) {
  return album[key];
}

If the key passed to getAlbumDetails is not a valid key of Album, TypeScript will show an error:

getAlbumDetails(album, "producer");
Argument of type '"producer"' is not assignable to parameter of type 'keyof Album'.2345
Argument of type '"producer"' is not assignable to parameter of type 'keyof Album'.

keyof is an important building block when creating new types from existing types. We'll see later how we can use it with as const to build our own type-safe enums.

The typeof Operator

The typeof operator allows you to extract a type from a value.

Say we have an albumSales object containing a few album title keys and some sales statistics:

const albumSales = {
  "Kind of Blue": 5000000,
  "A Love Supreme": 1000000,
  "Mingus Ah Um": 3000000,
};

We can use typeof to extract the type of albumSales, which will turn it into a type with the original keys as strings and their inferred types as values:

type AlbumSalesType = typeof albumSales;
type AlbumSalesType = { "Kind of Blue": number; "A Love Supreme": number; "Mingus Ah Um": number; }

Now that we have the AlbumSalesType type, we can create another derived type from it. For example, we can use keyof to extract the keys from the albumSales object:

type AlbumTitles = keyof AlbumSalesType; // "Kind of Blue" | "A Love Supreme" | "Mingus Ah Um"

A common pattern is to combine keyof and typeof to create a new type from an existing object type's keys and values:

type AlbumTitles = keyof typeof albumSales;

We could use this in a function to ensure that the title parameter is a valid key of albumSales, perhaps to look up the sales for a specific album:

function getSales(title: AlbumTitles) {
  return albumSales[title];
}

It's worth noting that typeof is not the same as the typeof operator used at runtime. TypeScript can tell the difference based on whether it's used in a type context or a value context:

// Runtime typeof
const albumSalesType = typeof albumSales; // "object"

// Type typeof
type AlbumSalesType = typeof albumSales;
type AlbumSalesType = { "Kind of Blue": number; "A Love Supreme": number; "Mingus Ah Um": number; }

Use the typeof keyword whenever you need to extract types based on runtime values, including objects, functions, classes, and more. It's a powerful tool for deriving types from values, and it's a key building block for other patterns that we'll explore later.

You Can't Create Runtime Types from Values

We've seen that typeof can create types from runtime values, but it's important to note that there is no way to to create a value from a type.

In other words, there is no valueof operator:

type Album = {
  title: string;
  artist: string;
  releaseYear: number;
};

const album = valueof Album; // Does not work!

TypeScript's types disappear at runtime, so there's no built-in way to create a value from a type. In other words, you can move from the 'value world' to the 'type world', but not the other way around.

Indexed Access Types

Indexed access types in TypeScript allow you to access a property of another type. This is similar to how you would access the value of a property in an object at runtime, but instead operates at the type level.

For example, we could use an indexed access type to extract the type of the title property from AlbumDetails:

interface Album {
  title: string;
  artist: string;
  releaseYear: number;
}

If we try to use dot notation to access the title property from the Album type, TypeScript will throw an error:

type AlbumTitle = Album.title;
Cannot access 'Album.title' because 'Album' is a type, but not a namespace. Did you mean to retrieve the type of the property 'title' in 'Album' with 'Album["title"]'?2713
Cannot access 'Album.title' because 'Album' is a type, but not a namespace. Did you mean to retrieve the type of the property 'title' in 'Album' with 'Album["title"]'?

In this case, the error message has a helpful suggestion: use Album["title"] to access the type of the title property in the Album type:

type AlbumTitle = Album["title"];
type AlbumTitle = string

Using this indexed access syntax, the AlbumTitle type is equivalent to string, because that's the type of the title property in the Album interface.

This same approach can be used to extract types from a tuple, where the index is used to access the type of a specific element in the tuple:

type AlbumTuple = [string, string, number];
type AlbumTitle = AlbumTuple[0];

Once again, the AlbumTitle will be a string type, because that's the type of the first element in the AlbumTuple.

Chaining Multiple Indexed Access Types

Indexed access types can be chained together to access nested properties. This is useful when working with complex types that have nested structures.

For example, we could use indexed access types to extract the type of the name property from the artist property in the Album type:

interface Album {
  title: string;
  artist: {
    name: string;
  };
}

type ArtistName = Album["artist"]["name"];

In this case, the ArtistName type will be equivalent to string, because that's the type of the name property in the artist object.

Passing a Union to an Indexed Access Type

If you want to access multiple properties from a type, you might be tempted to create a union type containing multiple indexed accesses:

type Album = {
  title: string;
  isSingle: boolean;
  releaseYear: number;
};

type AlbumPropertyTypes =
  | Album["title"]
  | Album["isSingle"]
  | Album["releaseYear"];

This will work, but you can do one better - you can pass a union type to an indexed access type directly:

type AlbumPropertyTypes = Album["title" | "isSingle" | "releaseYear"];
type AlbumPropertyTypes = string | number | boolean

This is a more concise way to achieve the same result.

Get An Object's Values With keyof

In fact, you may have noticed that we have another opportunity to reduce repetition here. We can use keyof to extract the keys from the Album type and use them as the union type:

type AlbumPropertyTypes = Album[keyof Album];
type AlbumPropertyTypes = string | number | boolean

This is a great pattern to use when you want to extract all of the values from an object type. keyof Obj will give you a union of all the keys in Obj, and Obj[keyof Obj] will give you a union of all the values in Obj.

Using as const For JavaScript-Style Enums

In our chapter on TypeScript-only features, we looked at the enum keyword. We saw that enum is a powerful way to create a set of named constants, but it has some downsides.

We now have all the tools available to us to see an alternative approach to creating enum-like structures in TypeScript.

First, let's use the as const assertion we saw in the chapter on mutability. This forces an object to be treated as read-only, and infers literal types for its properties:

const albumTypes = {
  CD: "cd",
  VINYL: "vinyl",
  DIGITAL: "digital",
} as const;

We can now derive the types we need from albumTypes using keyof and typeof. For instance, we can grab the keys using keyof:

type UppercaseAlbumType = keyof typeof albumTypes; // "CD" | "VINYL" | "DIGITAL"

We can also grab the values using Obj[keyof Obj]:

type AlbumType = (typeof albumTypes)[keyof typeof albumTypes]; // "cd" | "vinyl" | "digital"

We can now use our AlbumType type to ensure that a function only accepts one of the values from albumTypes:

function getAlbumType(type: AlbumType) {
  // ...
}

This approach is sometimes called a "POJO", or "Plain Old JavaScript Object". While it takes a bit of TypeScript magic to get the types set up, the result is simple to understand and easy to work with.

Let's now compare this to the enum approach.

Enums Require You To Pass The Enum Value

Our getAlbumType function behaves differently than if it accepted an enum. Because AlbumType is just a union of strings, we can pass a raw string to getAlbumType. But if we pass the incorrect string, TypeScript will show an error:

getAlbumType(albumTypes.CD); // no error
getAlbumType("vinyl"); // no error
getAlbumType("cassette");
Argument of type '"cassette"' is not assignable to parameter of type 'AlbumType'.2345
Argument of type '"cassette"' is not assignable to parameter of type 'AlbumType'.

This is a tradeoff. With enum, you have to pass the enum value, which is more explicit. With our as const approach, you can pass a raw string. This can make refactoring a little harder.

Enums Have To Be Imported

Another downside of enum is that they have to be imported into the module you're in to use them:

import { AlbumType } from "./enums";

getAlbumType(AlbumType.CD);

With our as const approach, we don't need to import anything. We can pass the raw string:

getAlbumType("cd");

Fans of enums will argue that importing the enum is a good thing, because it makes it clear where the enum is coming from and makes refactoring easier.

Enums Are Nominal

One of the biggest differences between enum and our as const approach is that enum is nominal, while our as const approach is structural.

This means that with enum, the type is based on the name of the enum. This means that enums with the same values that come from different enums are not compatible:

enum AlbumType {
  CD = "cd",
  VINYL = "vinyl",
  DIGITAL = "digital",
}

enum MediaType {
  CD = "cd",
  VINYL = "vinyl",
  DIGITAL = "digital",
}

getAlbumType(AlbumType.CD);
getAlbumType(MediaType.CD);
Argument of type 'MediaType.CD' is not assignable to parameter of type 'AlbumType'.2345
Argument of type 'MediaType.CD' is not assignable to parameter of type 'AlbumType'.

If you're used to enums from other languages, this is probably what you expect. But for developers used to JavaScript, this can be surprising.

With a POJO, where the value comes from doesn't matter. If two POJOs have the same values, they are compatible:

const albumTypes = {
  CD: "cd",
  VINYL: "vinyl",
  DIGITAL: "digital",
} as const;

const mediaTypes = {
  CD: "cd",
  VINYL: "vinyl",
  DIGITAL: "digital",
} as const;

getAlbumType(albumTypes.CD); // no error
getAlbumType(mediaTypes.CD); // no error

This is a tradeoff. Nominal typing can be more explicit and help catch bugs, but it can also be more restrictive and harder to work with.

Which Approach Should You Use?

The enum approach is more explicit and can help you refactor your code. It's also more familiar to developers coming from other languages.

The as const approach is more flexible and easier to work with. It's also more familiar to JavaScript developers.

In general, if you're working with a team that's used to enum, you should use enum. But if I were starting a project today, I would use as const instead of enums.

Exercises

Exercise 1: Reduce Key Repetition

Here we have an interface named FormValues:

interface FormValues {
  name: string;
  email: string;
  password: string;
}

This inputs variable is typed as a Record that specifies a key of either name, email, or password and a value that is an object with an initialValue and label properties that are both strings:

const inputs: Record<
  "name" | "email" | "password", // change me!
  {
    initialValue: string;
    label: string;
  }
> = {
  name: {
    initialValue: "",
    label: "Name",
  },
  email: {
    initialValue: "",
    label: "Email",
  },
  password: {
    initialValue: "",
    label: "Password",
  },
};

Notice there is a lot of duplication here. Both the FormValues interface and inputs Record contain name, email, and password.

Your task is to modify the inputs Record so its keys are derived from the FormValues interface.

Exercise 1: Reduce Key Repetition

Exercise 2: Derive a Type from a Value

Here, we have an object named configurations that comprises a set of deployment environments for development, production, and staging.

Each environment has its own url and timeout settings:

const configurations = {
  development: {
    apiBaseUrl: "http://localhost:8080",
    timeout: 5000,
  },
  production: {
    apiBaseUrl: "https://api.example.com",
    timeout: 10000,
  },
  staging: {
    apiBaseUrl: "https://staging.example.com",
    timeout: 8000,
  },
};

An Environment type has been declared as follows:

type Environment = "development" | "production" | "staging";

We want to use the Environment type across our application. However, the configurations object should be used as the source of truth.

Your task is to update the Environment type so that it is derived from the configurations object.

Exercise 2: Derive a Type from a Value

Exercise 3: Accessing Specific Values

Here were have an programModeEnumMap object that keeps different groupings in sync. There is also a ProgramModeMap type that uses typeof to represent the entire enum mapping:

export const programModeEnumMap = {
  GROUP: "group",
  ANNOUNCEMENT: "announcement",
  ONE_ON_ONE: "1on1",
  SELF_DIRECTED: "selfDirected",
  PLANNED_ONE_ON_ONE: "planned1on1",
  PLANNED_SELF_DIRECTED: "plannedSelfDirected",
} as const;

type ProgramModeMap = typeof programModeEnumMap;

The goal is to have a Group type that is always in sync with the ProgramModeEnumMap's group value. Currently it is typed as unknown:

type Group = unknown;

type test = Expect<Equal<Group, "group">>;
Type 'false' does not satisfy the constraint 'true'.2344
Type 'false' does not satisfy the constraint 'true'.

Your task is to find the proper way to type Group so the test passes as expected.

Exercise 3: Accessing Specific Values

Exercise 4: Unions with Indexed Access Types

This exercise starts with the same programModeEnumMap and ProgramModeMap as the previous exercise:

export const programModeEnumMap = {
  GROUP: "group",
  ANNOUNCEMENT: "announcement",
  ONE_ON_ONE: "1on1",
  SELF_DIRECTED: "selfDirected",
  PLANNED_ONE_ON_ONE: "planned1on1",
  PLANNED_SELF_DIRECTED: "plannedSelfDirected",
} as const;

type ProgramModeMap = typeof programModeEnumMap;

type PlannedPrograms = unknown;

type test = Expect<
  Equal<PlannedPrograms, "planned1on1" | "plannedSelfDirected">
Type 'false' does not satisfy the constraint 'true'.2344
Type 'false' does not satisfy the constraint 'true'.>;

This time, your challenge is to update the PlannedPrograms type to use an indexed access type to extract a union of the ProgramModeMap values that included "planned".

Exercise 4: Unions with Indexed Access Types

Exercise 5: Extract a Union of All Values

We're back with the programModeEnumMap and ProgramModeMap type:

export const programModeEnumMap = {
  GROUP: "group",
  ANNOUNCEMENT: "announcement",
  ONE_ON_ONE: "1on1",
  SELF_DIRECTED: "selfDirected",
  PLANNED_ONE_ON_ONE: "planned1on1",
  PLANNED_SELF_DIRECTED: "plannedSelfDirected",
} as const;

type ProgramModeMap = typeof programModeEnumMap;

This time we're interested in extracting all of the values from the programModeEnumMap object:

import { Equal, Expect } from "@total-typescript/helpers";
// ---cut---
type AllPrograms = unknown;

type test = Expect<
  Equal<
    AllPrograms,
    | "group"
    | "announcement"
    | "1on1"
    | "selfDirected"
    | "planned1on1"
    | "plannedSelfDirected"
  >
>;

Using what you've learned so far, your task is to update the AllPrograms type to use an indexed access type to create a union of all the values from the programModeEnumMap object.

Exercise 5: Extract a Union of All Values

Exercise 6: Create a Union from an as const Array

Here's an array of programModes wrapped in an as const:

export const programModes = [
  "group",
  "announcement",
  "1on1",
  "selfDirected",
  "planned1on1",
  "plannedSelfDirected",
] as const;

A test has been written to check if an AllPrograms type is a union of all the values in the programModes array:

import { Equal, Expect } from "@total-typescript/helpers";
type AllPrograms = unknown;
// ---cut---

type test = Expect<
  Equal<
    AllPrograms,
    | "group"
    | "announcement"
    | "1on1"
    | "selfDirected"
    | "planned1on1"
    | "plannedSelfDirected"
  >
>;

Your task is to determine how to create the AllPrograms type in order for the test to pass as expected.

Note that just using keyof and typeof in an approach similar to the previous exercise's solution won't quite work to solve this one! This is tricky to find - but as a hint: you can pass primitive types to indexed access types.

Exercise 6: Create a Union from an as const Array

Solution 1: Reduce Key Repetition

The solution is to use keyof to extract the keys from the FormValues interface and use them as the keys for the inputs Record:

const inputs: Record<
  keyof FormValues, // "name" | "email" | "password"
  {
    initialValue: string;
    label: string;
  } = {
    // object as before
  };

Now, if the FormValues interface changes, the inputs Record will automatically be updated to reflect those changes. inputs is derived from FormValues.

Solution 2: Derive a Type from a Value

The solution is to use the typeof keyword in combination with keyof to create the Environment type.

You could use them together in a single line:

type Environment = keyof typeof configurations;

Or you could first create a type from the configurations object and then update Environment to use keyof to extract the names of the keys:

type Configurations = typeof configurations;
type Configurations = { development: { apiBaseUrl: string; timeout: number; }; production: { apiBaseUrl: string; timeout: number; }; staging: { apiBaseUrl: string; timeout: number; }; }
type Environment = keyof Configurations;
type Environment = "development" | "production" | "staging"

Solution 3: Accessing Specific Values

Using an indexed access type, we can access the GROUP property from the ProgramModeMap type:

type Group = ProgramModeMap["GROUP"];
type Group = "group"

With this change, the Group type will be in sync with the ProgramModeEnumMap's group value. This means our test will pass as expected.

Solution 4: Unions with Indexed Access Types

In order to create the PlannedPrograms type, we can use an indexed access type to extract a union of the ProgramModeMap values that include "planned":

type Key = "PLANNED_ONE_ON_ONE" | "PLANNED_SELF_DIRECTED";
type PlannedPrograms = ProgramModeMap[Key];

With this change, the PlannedPrograms type will be a union of planned1on1 and plannedSelfDirected, which means our test will pass as expected.

Solution 5: Extract a Union of All Values

Using keyof and typeof together is the solution to this problem.

The most condensed solution looks like this:

type AllPrograms = (typeof programModeEnumMap)[keyof typeof programModeEnumMap];

Using an intermediate type, you could first use typeof programModeEnumMap to create a type from the programModeEnumMap object, then use keyof to extract the keys:

type ProgramModeMap = typeof programModeEnumMap;
type AllPrograms = ProgramModeMap[keyof ProgramModeMap];
type AllPrograms = "group" | "announcement" | "1on1" | "selfDirected" | "planned1on1" | "plannedSelfDirected"

Either solution results in a union of all values from the programModeEnumMap object, which means our test will pass as expected.

Solution 6: Create a Union from an as const Array

When using typeof and keyof with indexed access type, we can extract all of the values, but we also get some unexpected values like a 6 and an IterableIterator function:

type AllPrograms = (typeof programModes)[keyof typeof programModes];
type AllPrograms = "group" | "announcement" | "1on1" | "selfDirected" | "planned1on1" | "plannedSelfDirected" | 6 | (() => IterableIterator<"group" | "announcement" | "1on1" | "selfDirected" | "planned1on1" | "plannedSelfDirected">) | ... 23 more ... | ((index: number) => "group" | ... 5 more ... | undefined)

The additional stuff being extracted causes the test to fail because it is only expecting the original values instead of numbers and functions.

Recall that we can access the first element using programModes[0], the second element using programModes[1], and so on. This means that we could use a union of all possible index values to extract the values from the programModes array:

type AllPrograms = (typeof programModes)[0 | 1 | 2 | 3 | 4 | 5];

This solution makes the test pass, but it doesn't scale well. If the programModes array were to change, we would need to update the AllPrograms type manually.

Instead, we can use the number type as the argument to the indexed access type to represent all possible index values:

type AllPrograms = (typeof programModes)[number];

Now new items can be added to the programModes array without needing to update the AllPrograms type manually. This solution makes the test pass as expected, and is a great pattern to apply in your own projects.

Deriving Types From Functions

So far, we've only looked at deriving types from objects and arrays. But deriving types from functions can help solve some common problems in TypeScript.

Parameters

The Parameters utility type extracts the parameters from a given function type and returns them as a tuple.

For example, this sellAlbum function takes in an Album, a price, and a quantity, then returns a number representing the total price:

function sellAlbum(album: Album, price: number, quantity: number) {
  return price * quantity;
}

Using the Parameters utility type, we can extract the parameters from the sellAlbum function and assign them to a new type:

type SellAlbumParams = Parameters<typeof sellAlbum>;
type SellAlbumParams = [album: Album, price: number, quantity: number]

Note that we need to use typeof to create a type from the sellAlbum function. Passing sellAlbum directly to Parameters won't work on its own because sellAlbum is a value instead of a type:

type SellAlbumParams = Parameters<sellAlbum>;
'sellAlbum' refers to a value, but is being used as a type here. Did you mean 'typeof sellAlbum'?2749
'sellAlbum' refers to a value, but is being used as a type here. Did you mean 'typeof sellAlbum'?

This SellAlbumParams type is a tuple type that holds the Album, price, and quantity parameters from the sellAlbum function.

If we need to access a specific parameter from the SellAlbumParams type, we can use indexed access types:

type Price = SellAlbumParams[1]; // number

ReturnType

The ReturnType utility type extracts the return type from a given function:

type SellAlbumReturn = ReturnType<typeof sellAlbum>;
type SellAlbumReturn = number

In this case, the SellAlbumReturn type is a number, which derived from the sellAlbum function.

Awaited

Earlier in the book, we used the Promise type when working with asynchronous code.

The Awaited utility type is used to unwrap the Promise type and provide the type of the resolved value. Think of it as a shortcut similar to using await or .then() methods.

This can be particularly useful for deriving the return types of async functions.

To use it, you would pass a Promise type to Awaited and it would return the type of the resolved value:

type AlbumPromise = Promise<Album>;

type AlbumResolved = Awaited<AlbumPromise>;

Why Derive Types From Functions?

Being able to derive types from functions might not seem very useful at first. After all, if we control the functions then we can just write the types ourselves, and reuse them as needed:

type Album = {
  title: string;
  artist: string;
  releaseYear: number;
};

const sellAlbum = (album: Album, price: number, quantity: number) => {
  return price * quantity;
};

There's no reason to use Parameters or ReturnType on sellAlbum, since we defined the Album type and the return type ourselves.

But what about functions you don't control?

A common example is a third-party library. A library might export a function that you can use, but might not export the accompanying types. An example I recently came across was a type from the library @monaco-editor/react.

import { Editor } from "@monaco-editor/react";

// This is JSX component, for our purposes equivalent to...
<Editor
  onMount={(editor) => {
    // ...
  }}
/>;

// ...calling the function directly with an object
Editor({
  onMount: (editor) => {
    // ...
  },
});

In this case, I wanted to know the type of editor so I could reuse it in a function elsewhere. But the @monaco-editor/react library didn't export its type.

First, I extracted the type of the object the component expected:

type EditorProps = Parameters<typeof Editor>[0];

Then, I used an indexed access type to extract the type of the onMount property:

type OnMount = EditorProps["onMount"];

Finally, I extracted the first parameter from the OnMount type to get the type of editor:

type Editor = Parameters<OnMount>[0];

This allowed me to reuse the Editor type in a function elsewhere in my code.

By combining indexed access types with TypeScript's utility types, you can work around the limitations of third-party libraries and ensure that your types stay in sync with the functions you're using.

Exercises

Exercise 7: A Single Source of Truth

Here we have a makeQuery function that takes two parameters: a url and an optional opts object.

const makeQuery = (
  url: string,
  opts?: {
    method?: string;
    headers?: {
      [key: string]: string;
    };
    body?: string;
  },
) => {};

We want to specify these parameters as a tuple called MakeQueryParameters where the first argument of the tuple would be the string, and the second member would be the optional opts object.

Manually specifying the MakeQueryParameters would look something like this:

type MakeQueryParameters = [
  string,
  {
    method?: string;
    headers?: {
      [key: string]: string;
    };
    body?: string;
  }?,
];

In addition to being a bit annoying to write and read, the other problem with the above is that we now have two sources of truth: one is the MakeQueryParameters type, and the other is in the makeQuery function.

Your task is to use a utility type to fix this problem.

Exercise 7: A Single Source of Truth

Exercise 8: Typing Based on Return Value

Say we're working with a createUser function from a third-party library:

const createUser = (id: string) => {
  return {
    id,
    name: "John Doe",
    email: "example@email.com",
  };
};

For the sake of this exercise, assume we don't know the implementation of the function.

The goal is to create a User type that represents the return type of the createUser function. A test has been written to check if the User type is a match:

type User = unknown;

type test = Expect<
  Equal<
Type 'false' does not satisfy the constraint 'true'.2344
Type 'false' does not satisfy the constraint 'true'. User, { id: string; name: string; email: string; } > >;

Your task is to update the User type so the test passes as expected.

Exercise 8: Typing Based on Return Value

Exercise 9: Unwrapping a Promise

This time the createUser function from the third-party library is asynchronous:

const fetchUser = async (id: string) => {
  return {
    id,
    name: "John Doe",
    email: "example@email.com",
  };
};

type test = Expect<
  Equal<
Type 'false' does not satisfy the constraint 'true'.2344
Type 'false' does not satisfy the constraint 'true'. User,
Cannot find name 'User'.2304
Cannot find name 'User'. { id: string; name: string; email: string; } > >;

Like before, assume that you do not have access to the implementation of the fetchUser function.

Your task is to update the User type so the test passes as expected.

Exercise 9: Unwrapping a Promise

Solution 7: A Single Source of Truth

The Parameters utility type is key to this solution, but there is an additional step to follow.

Passing makeQuery directly to Parameters won't work on its own because makeQuery is a value instead of a type:

type MakeQueryParameters = Parameters<makeQuery>;
'makeQuery' refers to a value, but is being used as a type here. Did you mean 'typeof makeQuery'?2749
'makeQuery' refers to a value, but is being used as a type here. Did you mean 'typeof makeQuery'?

As the error message suggests, we need to use typeof to create a type from the makeQuery function, then pass that type to Parameters:

type MakeQueryParameters = Parameters<typeof makeQuery>;
type MakeQueryParameters = [url: string, opts?: { method?: string; headers?: { [key: string]: string; }; body?: string; } | undefined]

We now have MakeQueryParameters representing a tuple where the first member is a url string, and the second is the optional opts object.

Indexing into the type would allow us to create an Opts type that represents the opts object:

type Opts = MakeQueryParameters[1];

Solution 8: Typing Based on Return Value

Using the ReturnType utility type, we can extract the return type from the createUser function and assign it to a new type. Remember that since createUser is a value, we need to use typeof to create a type from it:

type User = ReturnType<typeof createUser>;
type User = { id: string; name: string; email: string; }

This User type is a match for the expected type, which means our test will pass as expected.

Solution 9: Unwrapping a Promise

When using the ReturnType utility type with an async function, the resulting type will be wrapped in a Promise:

type User = ReturnType<typeof fetchUser>;
type User = Promise<{ id: string; name: string; email: string; }>

In order to unwrap the Promise type and provide the type of the resolved value, we can use the Awaited utility type:

type User = Awaited<ReturnType<typeof fetchUser>>;

Like before, the User type is now a match for the expected type, which means our test will pass as expected.

It would also be possible to create intermediate types, but combining operators and type derivation gives us a more succinct solution.

Transforming Derived Types

In the previous section we looked at how to derive types from functions you don't control. Sometimes, you'll also need to do the same with types you don't control.

Exclude

The Exclude utility type is used to remove types from a union. Let's imagine that we have a union of different states our album can be in:

type AlbumState =
  | {
      type: "released";
      releaseDate: string;
    }
  | {
      type: "recording";
      studio: string;
    }
  | {
      type: "mixing";
      engineer: string;
    };

We want to create a type that represents the states that are not "released". We can use the Exclude utility type to achieve this:

type UnreleasedState = Exclude<AlbumState, { type: "released" }>;
type UnreleasedState = { type: "recording"; studio: string; } | { type: "mixing"; engineer: string; }

In this case, the UnreleasedState type is a union of the recording and mixing states, which are the states that are not "released". Exclude filters out any member of the union with a type of released.

We could have done it by checking for a releaseDate property instead:

type UnreleasedState = Exclude<AlbumState, { releaseDate: string }>;

This is because Exclude works by pattern matching. It will remove any type from the union that matches the pattern you provide.

This means we can use it to remove all strings from a union:

type Example = "a" | "b" | 1 | 2;

type Numbers = Exclude<Example, string>;
type Numbers = 1 | 2

NonNullable

NonNullable is used to remove null and undefined from a type. This can be useful when extracting a type from a partial object:

type Album = {
  artist?: {
    name: string;
  };
};

type Artist = NonNullable<Album["artist"]>;
type Artist = { name: string; }

This operates similarly to Exclude:

type Artist = Exclude<Album["artist"], null | undefined>;

But NonNullable is more explicit and easier to read.

Extract

Extract is the opposite of Exclude. It's used to extract types from a union. For example, we can use Extract to extract the recording state from the AlbumState type:

type RecordingState = Extract<AlbumState, { type: "recording" }>;
type RecordingState = { type: "recording"; studio: string; }

This is useful when you want to extract a specific type from a union you don't control.

Similarly to Exclude, Extract works by pattern matching. It will extract any type from the union that matches the pattern you provide.

This means that, to reverse our Extract example earlier, we can use it to extract all strings from a union:

type Example = "a" | "b" | 1 | 2 | true | false;

type Strings = Extract<Example, string>;
type Strings = "a" | "b"

It's worth noting the similarities between Exclude/Extract and Omit/Pick. A common mistake is to think that you can Pick from a union, or use Exclude on an object. Here's a little table to help you remember:

NameUsed OnActionExample
ExcludeUnionsExcludes membersExclude<'a' | 1, string>
ExtractUnionsExtracts membersExtract<'a' | 1, string>
OmitObjectsExcludes propertiesOmit<UserObj, 'id'>
PickObjectsExtracts propertiesPick<UserObj, 'id'>

Deriving vs Decoupling

Thanks to the tools in these chapters, we now know how to derive types from all sorts of sources: functions, objects and types. But there's a tradeoff to consider when deriving types: coupling.

When you derive a type from a source, you're coupling the derived type to that source. If you derive a type from another derived type, this can create long chains of coupling throughout your app that can be hard to manage.

When Decoupling Makes Sense

Let's imagine we have a User type in a db.ts file:

export type User = {
  id: string;
  name: string;
  imageUrl: string;
  email: string;
};

We'll say for this example that we're using a component-based framework like React, Vue or Svelte. We have a AvatarImage component that renders an image of the user. We could pass in the User type directly:

import { User } from "./db";

export const AvatarImage = (props: { user: User }) => {
  return <img src={props.user.imageUrl} alt={props.user.name} />;
};

But as it turns out, we're only using the imageUrl and name properties from the User type. It's a good idea to make your functions and components only require the data they need to run. This helps prevent you from passing around unnecessary data.

Let's try deriving. We'll create a new type called AvatarImageProps that only includes the properties we need:

import { User } from "./db";

type AvatarImageProps = Pick<User, "imageUrl" | "name">;

But let's think for a moment. We've now coupled the AvatarImageProps type to the User type. AvatarImageProps now not only depends on the shape of User, but its existence in the db.ts file. This means if we ever move the location of the User type, or split it into separate interfaces, we'll need to think about AvatarImageProps.

Let's try the other way around. Instead of deriving AvatarImageProps from User, we'll decouple them. We'll create a new type which just has the properties we need:

type AvatarImageProps = {
  imageUrl: string;
  name: string;
};

Now, AvatarImageProps is decoupled from User. We can move User around, split it into separate interfaces, or even delete it, and AvatarImageProps will be unaffected.

In this particular case, decoupling feels like the right choice. This is because User and AvatarImage are separate concerns. User is a data type, while AvatarImage is a UI component. They have different responsibilities and different reasons to change. By decoupling them, AvatarImage becomes more portable and easier to maintain.

What can make decoupling a difficult decision is that deriving can make you feel 'clever'. Pick tempts us because it uses a more advanced feature of TypeScript, which makes us feel good for applying the knowledge we've gained. But often, it's smarter to do the simple thing, and keep your types decoupled.

When Deriving Makes Sense

Deriving makes most sense when the code you're coupling shares a common concern. The examples in this chapter are good examples of this. Our as const object, for instance:

const albumTypes = {
  CD: "cd",
  VINYL: "vinyl",
  DIGITAL: "digital",
} as const;

type AlbumType = (typeof albumTypes)[keyof typeof albumTypes];

Here, AlbumType is derived from albumTypes. If we were to decouple it, we'd have to maintain two closely related sources of truth:

type AlbumType = "cd" | "vinyl" | "digital";

Because both AlbumType and albumTypes are closely related, deriving AlbumType from albumTypes makes sense.

Another example is when one type is directly related to another. For instance, our User type might have a UserWithoutId type derived from it:

type User = {
  id: string;
  name: string;
  imageUrl: string;
  email: string;
};

type UserWithoutId = Omit<User, "id">;

const updateUser = (id: string, user: UserWithoutId) => {
  // ...
};

Again, these concerns are closely related. Decoupling them would make our code harder to maintain and introduce more busywork into our codebase.

The decision to derive or decouple is all about reducing your future workload.

Are the two types so related that updates to one will need to ripple to the other? Derive.

Are they so unrelated that coupling them could result in more work down the line? Decouple.

Want to become a TypeScript wizard?

Unlock Pro Essentials
TypeScript Pro Essentials
PreviousTypeScript-only Features
NextAnnotations and Assertions