Video

Watch the full video on YouTube:

Custom Utility Types

Introduction

In this twelfth chapter of our TypeScript series, we’ll review the syntax used by built-in utility types and create our own. We’ve seen this syntax vaguely when inspecting types like Pick, now we’ll go through it in detail.

We’ll create three utility types: FunctionsOnly, NonFunctionsOnly and EventsGenerator, applying concepts like conditional types, property remapping, template literals, and the never type.

Utility Type Syntax

If we open the definition of Pick, we can see this syntax:

type Pick<T, K extends keyof T> = {
  [P in K]: T[P]
}

Here P is shorthand for property. For each P within the keys of T, we map the attribute to its corresponding value T[P]. This is the foundation for creating our own types.

Base Model: Pet

We’ll use a Pet type we already have, and extend it with methods:

interface Pet {
  id: number
  name: string
  owner: string
}

type ExtendedPet = Pet & {
  makeSound: () => void
  getSound: () => string
  previousOwner: () => string | null
}

ExtendedPet has both data properties and functions. This lets us demonstrate how to filter by data type.

FunctionsOnly

The first type extracts all properties whose data type is a function.

Type Definition

type FunctionSignature = (...args: any[]) => any

type FunctionsOnly<T> = {
  [P in keyof T as T[P] extends FunctionSignature ? P : never]: T[P]
}

Step-by-step explanation:

  1. [P in keyof T] - we iterate over all properties of type T
  2. as - activates property remapping, letting us evaluate each property
  3. T[P] extends FunctionSignature ? P : never - we ask: “does the data type of this property’s value conform to a function signature?”
  4. If yes, we keep the property (P). If no, we use never to exclude it entirely
  5. On the right side of the colon, we acquire the data type of that property’s value

Practical Example

type PetMethods = FunctionsOnly<ExtendedPet>
// Result:
// {
//   makeSound: () => void
//   getSound: () => string
//   previousOwner: () => string | null
// }

The id, name and owner properties disappeared because they’re not functions.

NonFunctionsOnly

We can do the inverse: create a type that ignores functions and returns all other properties. We just swap P for never and vice versa in the ternary:

type NonFunctionsOnly<T> = {
  [P in keyof T as T[P] extends FunctionSignature ? never : P]: T[P]
}

Explanation: Does the data type of this property’s value conform to a function signature? If yes, we use never (we exclude it). If no, then we include it.

Practical Example

type PetData = NonFunctionsOnly<ExtendedPet>
// Result:
// {
//   id: number
//   name: string
//   owner: string
// }

EventsGenerator

The second utility type automatically generates event handlers with the onChange, onSubmit format, React-style.

Type Definition

type EventsGenerator<T> = {
  [P in keyof T as `on${Capitalize<P & string>}`]:
    (newValue: T[P]) => void
}

Step-by-step explanation:

  1. `on${Capitalize<P & string>}` - we use compound literals (template literals) to create new properties
  2. Capitalize is a built-in TypeScript utility type that converts the first letter to uppercase
  3. P & string - TypeScript doesn’t know P is a string, so we intersect with string
  4. Each property’s value is a function that receives the expected data type and returns void

Practical Example

type PetHandlers = EventsGenerator<ExtendedPet>
// Result:
// {
//   onId: (newValue: number) => void
//   onName: (newValue: string) => void
//   onOwner: (newValue: string) => void
//   onMakeSound: (newValue: () => void) => void
//   onGetSound: (newValue: () => string) => void
//   onPreviousOwner: (newValue: () => string | null) => void
// }

Each property was renamed with the on prefix and the first letter capitalized.

Improving the Return Type

Handlers shouldn’t receive the original data type directly. We can use NonNullable to remove undefined and make the function return nothing:

type EventsGenerator<T> = {
  [P in keyof T as `on${Capitalize<P & string>}`]:
    (newValue: NonNullable<T[P]>) => void
}

Compound Literals and String Templates

Template literals aren’t just for prefixes. You can create more complex patterns:

type EventName = "click" | "hover" | "focus"

type HandlerName = `on${Capitalize<EventName>}`
// "onClick" | "onHover" | "onFocus"

And combine with other utility types:

type OptionalEvents<T> = {
  [P in keyof T as `on${Capitalize<P & string>}`]?:
    (newValue: NonNullable<T[P]>) => void
}

Summary

In this episode, we learned:

  1. FunctionsOnly: Filters properties keeping only functions
  2. NonFunctionsOnly: Filters functions keeping only data properties
  3. EventsGenerator: Generates event handlers with the “on” prefix
  4. Property remapping with as: Remaps properties in mapped types
  5. Conditional types: Evaluate if a type conforms to a particular format using extends
  6. never: A type that lets us exclude properties from objects
  7. Compound literals: Template literals to create new string literals
  8. Capitalize: Built-in utility type for capitalizing strings
  9. NonNullable: Utility type to remove undefined and null

Next Episode

In the next episode, we’ll continue exploring advanced TypeScript types. Don’t forget to subscribe so you don’t miss it.

TypeScript Course EP11: Generics in PracticeNestJS API Development Guide 09: Request and Response - @Req, @Res, @Headers and @Ip Decorators