← Back
April 4, 2026NodeJS7 min read
TypeScript utility types I use every day — and how to build your own
Published April 4, 20267 min read
I used Partial and Omit for years but stopped there. Then I discovered that the built-in utility types are just thin wrappers around mapped types and conditional types — and that I could build my own to solve specific problems. Here are the utility types I reach for daily and how to build the ones the standard library doesn't have.
The essential built-ins with real examples
typescript
interface User {
id: string;
email: string;
name: string;
role: 'admin' | 'user';
createdAt: Date;
}
// Partial: all fields optional — perfect for PATCH/update operations
type UserPatch = Partial;
// Required: all fields required — strip optional markers
type RequiredUser = Required;
// Pick: select specific fields
type UserSummary = Pick;
type UserAuth = Pick;
// Omit: exclude specific fields
type UserCreate = Omit; // Remove DB-generated fields
type UserPublic = Omit; // Remove sensitive fields
// Record: build a map type
type UserMap = Record; // { [userId: string]: User }
type RolePermissions = Record; // { admin: [...], user: [...] }
// Extract / Exclude: filter union types
type AdminRole = Extract; // 'admin'
type NonAdmin = Exclude; // 'user'
Function utility types
typescript
async function createUser(email: string, name: string, role: string): Promise {
// ...
}
// Extract parameter types without importing the function's signature separately
type CreateUserParams = Parameters;
// [email: string, name: string, role: string]
// Extract return type
type CreateUserResult = Awaited>;
// User (unwraps Promise)
// Useful for wrapping or proxying functions
function withLogging unknown>(fn: T): T {
return ((...args: Parameters) => {
console.log('Calling', fn.name, 'with', args);
return fn(...args);
}) as T;
}
const loggedCreate = withLogging(createUser);
// loggedCreate has the same type as createUser
Building custom utility types
typescript
// DeepPartial: partial recursively (standard Partial is shallow)
type DeepPartial = T extends object
? { [K in keyof T]?: DeepPartial }
: T;
interface Config {
database: { host: string; port: number; credentials: { user: string; pass: string } };
cache: { ttl: number; maxSize: number };
}
type ConfigOverride = DeepPartial;
// database.credentials.pass is optional at any depth
// Nullable: make all fields nullable
type Nullable = { [K in keyof T]: T[K] | null };
// NonNullable fields only
type NonNullableFields = { [K in keyof T]-?: NonNullable };
// PickByType: select fields of a specific type
type PickByType = {
[K in keyof T as T[K] extends ValueType ? K : never]: T[K];
};
interface OrderForm {
productId: string;
quantity: number;
discount: number;
notes: string;
isGift: boolean;
}
type StringFields = PickByType;
// { productId: string; notes: string }
type NumberFields = PickByType;
// { quantity: number; discount: number }
Path utility type for nested objects
typescript
// Get all dot-separated paths in a nested object
type Paths = {
[K in keyof T & string]: T[K] extends object
? Paths | `${Prefix}${K}`
: `${Prefix}${K}`;
}[keyof T & string];
interface AppConfig {
db: { host: string; port: number };
cache: { ttl: number };
name: string;
}
type ConfigPaths = Paths;
// "db" | "db.host" | "db.port" | "cache" | "cache.ttl" | "name"
function getConfig(path: ConfigPaths): unknown {
// path is type-safe — only valid paths allowed
}
getConfig("db.host"); // OK
getConfig("db.port"); // OK
getConfig("db.invalid"); // Error! Not a valid path
Building custom utility types is a skill that pays off in complex codebases. Once you understand that Partial<T> is just { [K in keyof T]?: T[K] }, you can write your own variations. The key is thinking about: what transformation do I want to apply to every key? That becomes your mapped type. What condition determines whether to include a key? That becomes your conditional type.
Share this
← All Posts7 min read