TypeScript Guide EP12: Creating Custom Utility Types
Video
Watch the full video on YouTube:
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:
[P in keyof T]- we iterate over all properties of typeTas- activates property remapping, letting us evaluate each propertyT[P] extends FunctionSignature ? P : never- we ask: “does the data type of this property’s value conform to a function signature?”- If yes, we keep the property (
P). If no, we useneverto exclude it entirely - 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:
`on${Capitalize<P & string>}`- we use compound literals (template literals) to create new propertiesCapitalizeis a built-in TypeScript utility type that converts the first letter to uppercaseP & string- TypeScript doesn’t knowPis a string, so we intersect withstring- 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:
- FunctionsOnly: Filters properties keeping only functions
- NonFunctionsOnly: Filters functions keeping only data properties
- EventsGenerator: Generates event handlers with the “on” prefix
- Property remapping with
as: Remaps properties in mapped types - Conditional types: Evaluate if a type conforms to a particular format using
extends never: A type that lets us exclude properties from objects- Compound literals: Template literals to create new string literals
Capitalize: Built-in utility type for capitalizing stringsNonNullable: Utility type to removeundefinedandnull
Next Episode
In the next episode, we’ll continue exploring advanced TypeScript types. Don’t forget to subscribe so you don’t miss it.
