Build Reusable Utility Types with Generics
How to create reusable TypeScript utility types using conditional types, mapped types, template literals, and generic constraints for type-safe APIs.
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
Overview
TypeScript’s type system supports conditional types, mapped types, and template literal types. Combined with generics, you can build utility types that derive new types from existing ones — Partial<T>, Pick<T, K>, Omit<T, K> are built-in examples. Custom utility types reduce duplication, enforce invariants at compile time, and make APIs more expressive without runtime overhead.
When to Use
- Deriving form types from API response types (omit server-only fields)
- Making specific properties required or optional based on context
- Creating type-safe builders that enforce property assignment order
- Extracting function parameter or return types for reuse
- Building API client types from schema definitions
When NOT to Use
- Simple applications with few types — built-in utilities (
Partial,Pick,Omit) are sufficient - When a type alias is clearer — don’t create a utility type if a direct definition is more readable
- Runtime validation — utility types are compile-time only; use Zod for runtime checks
Solution
Make specific properties required
type RequireFields<T, K extends keyof T> = T & {
[P in K]-?: T[P];
};
interface User {
id?: string;
name?: string;
email?: string;
role: string;
}
type UserWithRequiredEmail = RequireFields<User, "id" | "email">;
// { id: string; name?: string; email: string; role: string }
Make specific properties optional
type OptionalFields<T, K extends keyof T> = Omit<T, K> & {
[P in K]?: T[P];
};
interface CreateUserDTO {
name: string;
email: string;
role: string;
avatar: string;
}
type UpdateUserDTO = OptionalFields<CreateUserDTO, "name" | "email" | "role" | "avatar">;
// All fields optional — equivalent to Partial<CreateUserDTO>
Pick properties by value type
type PickByValue<T, ValueType> = {
[P in keyof T as T[P] extends ValueType ? P : never]: T[P];
};
interface Config {
port: number;
host: string;
debug: boolean;
retries: number;
logFile: string;
}
type StringConfig = PickByValue<Config, string>;
// { host: string; logFile: string }
type NumberConfig = PickByValue<Config, number>;
// { port: number; retries: number }
Omit properties by value type
type OmitByValue<T, ValueType> = {
[P in keyof T as T[P] extends ValueType ? never : P]: T[P];
};
type NonBooleanConfig = OmitByValue<Config, boolean>;
// { port: number; host: string; retries: number; logFile: string }
Deep partial
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
interface AppConfig {
server: {
port: number;
host: string;
};
database: {
url: string;
poolSize: number;
};
logging: {
level: string;
file: string;
};
}
type PartialConfig = DeepPartial<AppConfig>;
// All properties optional at all levels
const config: PartialConfig = {
server: { port: 3000 }, // host is optional
};
Deep readonly
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};
type ReadonlyConfig = DeepReadonly<AppConfig>;
// All properties readonly at all levels
Template literal types
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<"click">; // "onClick"
type FocusEvent = EventName<"focus">; // "onFocus"
type EventHandler<T extends string> = {
[K in EventName<T>]?: (event: { type: T; target: HTMLElement }) => void;
};
type ButtonHandlers = EventHandler<"click" | "focus" | "blur">;
// { onClick?: ...; onFocus?: ...; onBlur?: ... }
API response wrapper
type ApiResponse<T, E = string> =
| { success: true; data: T; timestamp: number }
| { success: false; error: E; timestamp: number };
type UserResponse = ApiResponse<{ id: string; name: string }>;
type ListResponse<T> = ApiResponse<T[], "not_found" | "server_error">;
Builder pattern with type safety
type Builder<T> = {
[K in keyof T]: (value: T[K]) => Builder<Omit<T, K>>;
} & { build: () => T };
function createBuilder<T>(): Builder<T> {
const state = {} as T;
const proxy = {} as Builder<T>;
for (const key in state) {
proxy[key] = ((value: any) => {
(state as any)[key] = value;
return proxy;
}) as any;
}
proxy.build = () => state;
return proxy;
}
interface User {
id: string;
name: string;
email: string;
}
const user = createBuilder<User>()
.id("123")
.name("Alice")
.email("alice@example.com")
.build();
Extract route parameters
type RouteParams<T extends string> = T extends `${string}:${infer Param}/${infer Rest}`
? { [K in Param]: string } & RouteParams<Rest>
: T extends `${string}:${infer Param}`
? { [K in Param]: string }
: {};
type UserRoute = "/users/:userId/posts/:postId";
type UserRouteParams = RouteParams<UserRoute>;
// { userId: string; postId: string }
function navigate<T extends string>(
route: T,
params: RouteParams<T>
): string {
let path = route;
for (const [key, value] of Object.entries(params)) {
path = path.replace(`:${key}`, value as string);
}
return path;
}
navigate("/users/:userId/posts/:postId", { userId: "123", postId: "456" });
// "/users/123/posts/456"
Mutable version of a type
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
interface ReadonlyUser {
readonly id: string;
readonly name: string;
readonly email: string;
}
type EditableUser = Mutable<ReadonlyUser>;
// { id: string; name: string; email: string } — no readonly
Function parameter and return types
type FirstParam<T> = T extends (first: infer P, ...rest: any[]) => any ? P : never;
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type AllParams<T> = T extends (...args: infer P) => any ? P : never;
function createUser(name: string, age: number, email: string): { id: string } {
return { id: "1" };
}
type Name = FirstParam<typeof createUser>; // string
type Result = ReturnType<typeof createUser>; // { id: string }
type Params = AllParams<typeof createUser>; // [string, number, string]
Variants
Using with Zod for schema-derived types
import { z } from "zod";
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
role: z.enum(["admin", "user", "guest"]),
});
type User = z.infer<typeof UserSchema>;
type CreateUserInput = OptionalFields<Omit<User, "id">, "role">;
// { name: string; email: string; role?: "admin" | "user" | "guest" }
type AdminUser = RequireFields<User, "role"> & { role: "admin" };
Conditional type with infer for array element
type ArrayElement<T> = T extends (infer E)[] ? E : never;
type StringElement = ArrayElement<string[]>; // string
type NumberElement = ArrayElement<number[]>; // number
type NotArray = ArrayElement<string>; // never
Promise unwrapping
type Awaited<T> = T extends Promise<infer U> ? U : T;
type Result = Awaited<Promise<string>>; // string
type Nested = Awaited<Promise<Promise<number>>>; // number (in TypeScript 4.5+)
Best Practices
-
For a deeper guide, see Complete Guide to TypeScript Advanced Types.
-
Name utility types descriptively —
RequireFields<T, K>is clearer thanT & { [P in K]-?: T[P] } -
Add generic constraints (
K extends keyof T) to catch invalid usage at compile time -
Keep utility types small and composable — combine simple utilities instead of building complex ones
-
Test utility types with
expect-typeor simple type assertions — ensure they produce the expected shape -
Document edge cases — what happens with optional properties, unions, or nested objects
-
Prefer built-in utilities when available —
Partial,Required,Pick,Omit,Readonlycover most cases
Common Mistakes
- Overcomplicating types: a 50-line utility type that could be a 5-line type alias. Readability matters.
- Not testing utility types: a type that compiles doesn’t mean it produces the right shape. Use
type assertionsto verify. - Ignoring
never: when a conditional type doesn’t match, it resolves tonever. This can silently break mapped types. - Forgetting
-?: removing optionality requires-?modifier. Without it,?properties stay optional. - Not constraining generics:
K extends keyof Tprevents passing invalid keys. Without the constraint, TypeScript can’t catch typos.
FAQ
What is a conditional type?
A type that selects between two types based on a condition: T extends U ? X : Y. If T extends U, the result is X; otherwise Y. With infer, you can extract types from within other types.
What is a mapped type?
A type that transforms each property of an existing type: { [P in keyof T]: NewType }. You can add/remove readonly and ? modifiers with +/- prefixes.
Can utility types be used with enums?
Yes, but string literal unions are preferred. keyof typeof MyEnum gives the enum keys as a union type.
How do I debug a utility type?
Use type assertions to check the result:
type Test = RequireFields<User, "email">;
const test: Test = { id: "1", email: "a@b.com", role: "user" }; // Should compile
What is the difference between keyof T and keyof T & string?
keyof T includes number | symbol if T has those key types. keyof T & string restricts to string keys only, which is useful for template literal types.
Related Resources
Exhaustive Type Checking with Discriminated Unions
How to use TypeScript discriminated unions for exhaustive type checking, ensuring all cases are handled at compile time with never type assertions.
RecipeForm Validation with react-hook-form and Zod
How to build type-safe forms in React using react-hook-form with Zod schema validation, including nested fields, async validation, and dynamic fields.
RecipeWhen to Use useMemo and useCallback
How and when to use React's useMemo and useCallback hooks for performance optimization, and when they add unnecessary overhead.
GuideComplete Guide to TypeScript Advanced Types
Master TypeScript advanced types: conditional types, mapped types, template literal types, infer, distributive types, and type-level programming patterns.