Skip to main content

TypeScript Higher Order Types

Introduction​

Higher Order Types in TypeScript are type-level constructs that operate on other types to produce new types. Much like higher-order functions in functional programming (functions that take functions as arguments or return functions), higher-order types take types as inputs and produce types as outputs.

Understanding higher-order types is crucial for advanced TypeScript development because they enable you to:

  • Create flexible and reusable type definitions
  • Manipulate and transform existing types
  • Build complex type relationships
  • Improve type safety in your applications

In this guide, we'll explore the concept of higher-order types, examine several examples, and demonstrate practical applications that you can use in your projects.

Prerequisites​

Before diving into higher-order types, you should be familiar with:

  • Basic TypeScript syntax
  • Generics in TypeScript
  • Type aliases and interfaces
  • Union and intersection types

Understanding Higher Order Types​

Higher-order types in TypeScript are primarily implemented through generics and utility types. They allow you to create type transformations that can be applied to other types.

The Basic Pattern​

At its core, a higher-order type looks like this:

typescript
type HigherOrderType<T> = /* some operation on T */;

Here, T is a type parameter that represents any type. The higher-order type transforms T in some way to create a new type.

Common Higher Order Types​

1. Mapped Types​

Mapped types allow you to create new types by transforming the properties of an existing type.

typescript
type Optional<T> = {
[K in keyof T]?: T[K];
};

// Input
interface User {
id: number;
name: string;
email: string;
}

// Output
type OptionalUser = Optional<User>;
// Equivalent to:
// {
// id?: number;
// name?: string;
// email?: string;
// }

In this example, Optional<T> is a higher-order type that takes a type T and makes all of its properties optional.

2. Conditional Types​

Conditional types let you create types that depend on conditions:

typescript
type ExtractNumberProperties<T> = {
[K in keyof T]: T[K] extends number ? K : never;
}[keyof T];

// Input
interface Product {
id: number;
name: string;
price: number;
description: string;
inStock: boolean;
}

// Output
type NumberProps = ExtractNumberProperties<Product>; // "id" | "price"

This higher-order type extracts all the property names from type T where the property type is a number.

3. Type Inference with Conditional Types​

You can use the infer keyword within conditional types to extract types from other types:

typescript
type UnpackArray<T> = T extends Array<infer U> ? U : T;

// Input
type StringArray = string[];
type NumberArray = Array<number>;

// Output
type ExtractedString = UnpackArray<StringArray>; // string
type ExtractedNumber = UnpackArray<NumberArray>; // number
type NoChange = UnpackArray<boolean>; // boolean (not an array)

UnpackArray<T> is a higher-order type that extracts the element type from an array, or returns the original type if it's not an array.

Building Complex Higher Order Types​

Let's combine these concepts to build more powerful higher-order types:

Deep Partial Type​

A common use case is to create a type that makes all properties, including nested ones, optional:

typescript
type DeepPartial<T> = T extends object ? {
[K in keyof T]?: DeepPartial<T[K]>;
} : T;

// Input
interface Configuration {
server: {
port: number;
host: string;
ssl: {
enabled: boolean;
cert: string;
key: string;
}
};
database: {
url: string;
name: string;
}
}

// Output
type PartialConfig = DeepPartial<Configuration>;
// Now we can have incomplete configurations like:
const config: PartialConfig = {
server: {
port: 3000,
ssl: {
enabled: true
}
}
};

This recursive higher-order type handles nested objects and makes all properties optional at all levels.

Pick Properties by Value Type​

Let's create a type that picks properties of a specific type:

typescript
type PickByValueType<T, ValueType> = {
[K in keyof T as T[K] extends ValueType ? K : never]: T[K]
};

// Input
interface Form {
name: string;
email: string;
age: number;
isSubscribed: boolean;
birthDate: Date;
contactCount: number;
}

// Output
type StringFields = PickByValueType<Form, string>;
// { name: string; email: string; }

type NumberFields = PickByValueType<Form, number>;
// { age: number; contactCount: number; }

This higher-order type extracts only the properties of a given value type.

Practical Applications​

1. API Response Handling​

Higher-order types can help manage API responses with proper typing:

typescript
type ApiResponse<T> = {
data: T;
status: number;
message: string;
timestamp: Date;
};

type ApiError = {
error: string;
status: number;
code: string;
};

// Discriminated union for success or error
type ApiResult<T> =
| { success: true; response: ApiResponse<T> }
| { success: false; error: ApiError };

// Usage
async function fetchUser(id: number): Promise<ApiResult<User>> {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
const error: ApiError = await response.json();
return { success: false, error };
}

const data: ApiResponse<User> = await response.json();
return { success: true, response: data };
} catch (err) {
return {
success: false,
error: {
error: err.message,
status: 500,
code: 'UNKNOWN_ERROR'
}
};
}
}

// Now using the result with type safety
const result = await fetchUser(123);
if (result.success) {
console.log(result.response.data.name); // Safe access
} else {
console.log(result.error.message); // Safe access
}

2. Form State Management​

Higher-order types can be useful for form handling:

typescript
type FormField<T> = {
value: T;
touched: boolean;
error?: string;
validate: (value: T) => string | undefined;
};

type FormState<T> = {
[K in keyof T]: FormField<T[K]>;
};

// Input
interface UserForm {
username: string;
password: string;
age: number;
}

// Output
type UserFormState = FormState<UserForm>;
// Equivalent to:
// {
// username: FormField<string>;
// password: FormField<string>;
// age: FormField<number>;
// }

// Usage
const initialForm: UserFormState = {
username: {
value: '',
touched: false,
validate: (value) => value ? undefined : 'Username is required'
},
password: {
value: '',
touched: false,
validate: (value) => value.length >= 8 ? undefined : 'Password must be at least 8 characters'
},
age: {
value: 0,
touched: false,
validate: (value) => value >= 18 ? undefined : 'Must be 18 or older'
}
};

Advanced Composition of Higher Order Types​

Let's combine multiple higher-order types to create more powerful compositions:

typescript
// Make some properties required and others optional
type RequireOnly<T, K extends keyof T> =
Required<Pick<T, K>> & Partial<Omit<T, K>>;

// Create readonly version of specific properties
type ReadonlyPart<T, K extends keyof T> =
Readonly<Pick<T, K>> & Omit<T, K>;

// Example usage
interface User {
id: number;
name: string;
email: string;
avatar?: string;
bio?: string;
}

// A user where only name and email are required
type NewUser = RequireOnly<User, 'name' | 'email'>;

// A user where id is readonly (can't be modified)
type ExistingUser = ReadonlyPart<User, 'id'>;

const newUser: NewUser = {
name: 'John Doe',
email: 'john@example.com'
// id, avatar, and bio are optional
};

const existingUser: ExistingUser = {
id: 123,
name: 'Jane Smith',
email: 'jane@example.com'
};

// This would throw a type error:
// existingUser.id = 456; // Error: Cannot assign to 'id' because it is a read-only property

TypeScript Built-in Higher Order Types​

TypeScript provides several built-in higher-order types (utility types) that you can use right away:

typescript
interface Example {
a: string;
b: number;
c: boolean;
}

// Partial<T> - Makes all properties optional
type PartialExample = Partial<Example>; // { a?: string; b?: number; c?: boolean; }

// Required<T> - Makes all properties required
type RequiredExample = Required<Example>; // { a: string; b: number; c: boolean; }

// Readonly<T> - Makes all properties readonly
type ReadonlyExample = Readonly<Example>; // { readonly a: string; readonly b: number; readonly c: boolean; }

// Pick<T, K> - Picks specific properties from T
type PickExample = Pick<Example, 'a' | 'c'>; // { a: string; c: boolean; }

// Omit<T, K> - Removes specific properties from T
type OmitExample = Omit<Example, 'b'>; // { a: string; c: boolean; }

// Record<K, T> - Creates a type with properties from K with values of type T
type RecordExample = Record<'x' | 'y', number>; // { x: number; y: number; }

// Extract<T, U> - Extracts types from T that are assignable to U
type ExtractExample = Extract<'a' | 'b' | 'c', 'a' | 'f'>; // 'a'

// Exclude<T, U> - Excludes types from T that are assignable to U
type ExcludeExample = Exclude<'a' | 'b' | 'c', 'a'>; // 'b' | 'c'

// NonNullable<T> - Removes null and undefined from T
type NonNullableExample = NonNullable<string | number | undefined | null>; // string | number

// ReturnType<T> - Gets the return type of a function type
type ReturnTypeExample = ReturnType<() => string>; // string

Creating a Type DSL (Domain Specific Language)​

By composing higher-order types, you can create a domain-specific language for your application's type needs:

typescript
// API-specific higher-order types
namespace Api {
export type Resource<T> = {
data: T;
links: {
self: string;
related?: string[];
}
};

export type Collection<T> = {
items: T[];
total: number;
page: number;
pageSize: number;
links: {
self: string;
next?: string;
prev?: string;
}
};

export type Error = {
code: string;
message: string;
details?: unknown;
};

export type Result<T> =
| { kind: 'success'; data: T }
| { kind: 'error'; error: Error };

export type Paginated<T> = Result<Collection<T>>;
export type Single<T> = Result<Resource<T>>;
}

// Client code
interface User {
id: string;
name: string;
email: string;
}

// Typed API responses
async function getUsers(): Promise<Api.Paginated<User>> {
// Implementation
return {
kind: 'success',
data: {
items: [{ id: '1', name: 'John', email: 'john@example.com' }],
total: 1,
page: 1,
pageSize: 10,
links: { self: '/api/users' }
}
};
}

async function getUser(id: string): Promise<Api.Single<User>> {
// Implementation
return {
kind: 'success',
data: {
data: { id: '1', name: 'John', email: 'john@example.com' },
links: { self: `/api/users/${id}` }
}
};
}

Performance Considerations​

While higher-order types are powerful, they can impact TypeScript's compilation performance. Here are some tips:

  1. Avoid deeply nested conditional types: They can slow down the type checker.
  2. Cache intermediate types: Define intermediate types rather than inlining complex type expressions.
  3. Be specific with constraints: Always add appropriate constraints to your generics.
  4. Consider splitting complex types: Break down very complex higher-order types into smaller pieces.

Summary​

Higher-order types in TypeScript are powerful abstractions that allow you to:

  • Transform types in a flexible, reusable way
  • Create complex type relationships
  • Build domain-specific type systems
  • Enhance type safety without duplicating code

By mastering higher-order types, you'll write more maintainable and type-safe TypeScript code, reduce code duplication, and create powerful abstractions that can evolve with your application.

Exercises​

  1. Create a higher-order type that makes all properties of an object nullable (can be the original type or null).
  2. Implement a DeepReadonly type that makes all properties (including nested ones) readonly.
  3. Create a FunctionProperties<T> type that extracts only the method properties from an object.
  4. Implement a PickByPrefixes<T, Prefix> type that selects properties from T where the property name starts with Prefix.
  5. Create a higher-order type to convert a union type to an intersection type.

Additional Resources​

Understanding higher-order types is a journey, not a destination. The more you practice and experiment, the more powerful your TypeScript code will become!



If you spot any mistakes on this website, please let me know at feedback@compilenrun.com. I’d greatly appreciate your feedback! :)