Skip to main content
Advertisement

5.3 Built-in Utility Types

What Are Utility Types?

TypeScript ships with a set of utility types that encode common type-transformation patterns. They are all generic and derive a new type from an existing one.

interface User {
id: number;
name: string;
email: string;
password: string;
role: "admin" | "user";
createdAt: Date;
}

// Deriving various types from the same source
type PartialUser = Partial<User>; // all fields optional
type PublicUser = Omit<User, "password">; // password removed
type UserFormData = Pick<User, "name" | "email" | "password">; // subset
type ReadonlyUser = Readonly<User>; // all fields read-only

With utility types you avoid duplicating type definitions and manage everything from one place while producing any variation you need.


Partial<T>

Makes every property optional (?).

interface Post {
id: number;
title: string;
content: string;
published: boolean;
tags: string[];
}

// All fields become optional
type PartialPost = Partial<Post>;
// {
// id?: number;
// title?: string;
// content?: string;
// published?: boolean;
// tags?: string[];
// }

// Useful in update functions
function updatePost(id: number, updates: Partial<Post>): Post {
// Fetch existing post from DB and merge updates
const existing = getPostFromDb(id);
return { ...existing, ...updates };
}

// Only some fields need to be supplied
updatePost(1, { title: "New Title" }); // OK
updatePost(1, { title: "New Title", published: true }); // OK
updatePost(1, { nonExistent: "value" }); // Error

Implementing Partial

// How TypeScript implements it internally
type MyPartial<T> = {
[K in keyof T]?: T[K];
};

// Test
type MyPartialPost = MyPartial<Post>;
// Same result as Partial<Post>

Required<T>

Makes every property required. Uses the -? modifier to strip optionality.

interface Config {
host?: string;
port?: number;
debug?: boolean;
timeout?: number;
}

// All fields become required
type StrictConfig = Required<Config>;
// {
// host: string;
// port: number;
// debug: boolean;
// timeout: number;
// }

// Merge defaults with user config and return a complete config
const DEFAULT_CONFIG: Required<Config> = {
host: "localhost",
port: 3000,
debug: false,
timeout: 5000,
};

function resolveConfig(userConfig: Config): Required<Config> {
return { ...DEFAULT_CONFIG, ...userConfig };
}

const config = resolveConfig({ port: 8080 });
config.host; // string (never undefined)
config.port; // 8080
config.debug; // false
config.timeout; // 5000

Implementing Required

type MyRequired<T> = {
[K in keyof T]-?: T[K]; // -? removes the optional modifier
};

Readonly<T>

Makes every property readonly, preventing mutation.

interface Point {
x: number;
y: number;
}

const origin: Readonly<Point> = { x: 0, y: 0 };
origin.x = 1; // Error: Cannot assign to 'x' because it is a read-only property

// Immutable configuration object
const APP_CONFIG = Object.freeze({
apiUrl: "https://api.example.com",
version: "1.0.0",
maxRetries: 3,
}) as Readonly<{
apiUrl: string;
version: string;
maxRetries: number;
}>;

// Declare function parameters readonly to prevent accidental mutation
function processUsers(users: Readonly<User[]>): string[] {
// users.push(...) // Error: push is not available on a Readonly array
return users.map((u) => u.name);
}

Implementing Readonly

type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};

Pick<T, K>

Creates a new type containing only the keys K from type T.

interface Article {
id: number;
title: string;
content: string;
summary: string;
author: string;
publishedAt: Date;
viewCount: number;
tags: string[];
}

// List view — exclude the heavy content field
type ArticleListItem = Pick<Article, "id" | "title" | "summary" | "publishedAt" | "author">;
// {
// id: number;
// title: string;
// summary: string;
// publishedAt: Date;
// author: string;
// }

// SEO metadata
type ArticleMeta = Pick<Article, "title" | "summary" | "tags">;

// Usage
function renderArticleCard(article: ArticleListItem): string {
return `<h2>${article.title}</h2><p>${article.summary}</p>`;
}

Implementing Pick

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

// Test
type MyArticleListItem = MyPick<Article, "id" | "title" | "summary">;

Omit<T, K>

Creates a new type that excludes the keys K from type T.

interface User {
id: number;
name: string;
email: string;
password: string;
createdAt: Date;
}

// Exclude sensitive fields
type PublicUser = Omit<User, "password">;
// {
// id: number;
// name: string;
// email: string;
// createdAt: Date;
// }

// id and createdAt are generated server-side on creation
type CreateUserDto = Omit<User, "id" | "createdAt">;
// {
// name: string;
// email: string;
// password: string;
// }

function createUser(dto: CreateUserDto): User {
return {
...dto,
id: Math.random(),
createdAt: new Date(),
};
}

Implementing Omit

// Omit is implemented as Pick + Exclude
type MyOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

// Or directly with a mapped type
type MyOmit2<T, K extends keyof T> = {
[P in keyof T as P extends K ? never : P]: T[P];
};

Exclude<T, U>

Removes from union type T any member that is assignable to U.

type Status = "pending" | "active" | "inactive" | "deleted";

// Active statuses — exclude "deleted"
type ActiveStatus = Exclude<Status, "deleted">;
// "pending" | "active" | "inactive"

// Remove multiple members
type WorkingStatus = Exclude<Status, "inactive" | "deleted">;
// "pending" | "active"

// Remove null/undefined (similar to NonNullable)
type NonNull = Exclude<string | null | undefined, null | undefined>;
// string

// Remove function types
type PrimitiveOnly = Exclude<string | number | (() => void), Function>;
// string | number

Implementing Exclude

// Implemented with a distributive conditional type
type MyExclude<T, U> = T extends U ? never : T;

// How it works (distribution)
// MyExclude<"a" | "b" | "c", "b">
// = MyExclude<"a", "b"> | MyExclude<"b", "b"> | MyExclude<"c", "b">
// = "a" | never | "c"
// = "a" | "c"

Extract<T, U>

Extracts from union type T only the members assignable to U. The opposite of Exclude.

type AllTypes = string | number | boolean | null | undefined | object;

// Extract only primitives
type Primitives = Extract<AllTypes, string | number | boolean>;
// string | number | boolean

// Extract common members
type A = "admin" | "user" | "moderator";
type B = "user" | "moderator" | "guest";
type Common = Extract<A, B>;
// "user" | "moderator"

// Extract types that match a specific structure
type Actions =
| { type: "INCREMENT"; payload: number }
| { type: "DECREMENT"; payload: number }
| { type: "RESET" }
| { type: "SET_USER"; payload: string };

type PayloadActions = Extract<Actions, { payload: unknown }>;
// { type: "INCREMENT"; payload: number }
// | { type: "DECREMENT"; payload: number }
// | { type: "SET_USER"; payload: string }

Implementing Extract

type MyExtract<T, U> = T extends U ? T : never;

NonNullable<T>

Removes null and undefined from type T.

type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>; // string

type MaybeUser = User | null | undefined;
type DefiniteUser = NonNullable<MaybeUser>; // User

// Useful for processing API responses
function assertDefined<T>(
value: T | null | undefined,
message: string
): NonNullable<T> {
if (value === null || value === undefined) {
throw new Error(message);
}
return value as NonNullable<T>;
}

const maybeUser: User | null = getUserOrNull();
const user = assertDefined(maybeUser, "User not found.");
// user is typed as User (not null)

Implementing NonNullable

type MyNonNullable<T> = T extends null | undefined ? never : T;
// In TypeScript 4.8+ it can also be expressed as T & {}
type MyNonNullable2<T> = T & {};

Record<K, V>

Creates an object type with keys of type K and values of type V.

// Basic usage
type UserMap = Record<number, User>;
type RolePermissions = Record<"admin" | "user" | "moderator", string[]>;
type Cache = Record<string, unknown>;

// Combined with an enum
enum HttpMethod {
GET = "GET",
POST = "POST",
PUT = "PUT",
DELETE = "DELETE",
PATCH = "PATCH",
}

type MethodHandlers = Record<HttpMethod, (req: Request) => Response>;

// Status color map
type StatusColor = Record<
"success" | "error" | "warning" | "info",
{ bg: string; text: string; border: string }
>;

const statusColors: StatusColor = {
success: { bg: "#d4edda", text: "#155724", border: "#c3e6cb" },
error: { bg: "#f8d7da", text: "#721c24", border: "#f5c6cb" },
warning: { bg: "#fff3cd", text: "#856404", border: "#ffeeba" },
info: { bg: "#d1ecf1", text: "#0c5460", border: "#bee5eb" },
};

Implementing Record

type MyRecord<K extends string | number | symbol, V> = {
[P in K]: V;
};

ReturnType<T> and Parameters<T>

Extract the return type and parameter types from a function type.

// ReturnType
function fetchUser(id: number): Promise<User> {
return fetch(`/api/users/${id}`).then((r) => r.json());
}

type FetchUserReturn = ReturnType<typeof fetchUser>;
// Promise<User>

function createRange(start: number, end: number): number[] {
return Array.from({ length: end - start }, (_, i) => start + i);
}

type RangeResult = ReturnType<typeof createRange>; // number[]

// Parameters
type FetchUserParams = Parameters<typeof fetchUser>;
// [id: number]

type CreateRangeParams = Parameters<typeof createRange>;
// [start: number, end: number]

// Use case: preserving types when wrapping a function
function memoize<T extends (...args: any[]) => any>(
fn: T
): (...args: Parameters<T>) => ReturnType<T> {
const cache = new Map<string, ReturnType<T>>();
return (...args: Parameters<T>): ReturnType<T> => {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key)!;
}
const result = fn(...args);
cache.set(key, result);
return result;
};
}

const memoizedRange = memoize(createRange);
memoizedRange(1, 10); // number[] — type is preserved
// ConstructorParameters<T>: extract constructor parameter types
class Vector {
constructor(public x: number, public y: number, public z: number) {}
}

type VectorArgs = ConstructorParameters<typeof Vector>;
// [x: number, y: number, z: number]

// InstanceType<T>: extract the instance type of a class
type VectorInstance = InstanceType<typeof Vector>;
// Vector

// Awaited<T>: unwrap a Promise (TypeScript 4.5+)
type UserData = Awaited<Promise<User>>; // User
type NestedData = Awaited<Promise<Promise<string>>>; // string

Implementing Utility Types (Learning Exercise)

// Implementing all utility types seen so far
namespace MyUtility {
export type Partial<T> = { [K in keyof T]?: T[K] };
export type Required<T> = { [K in keyof T]-?: T[K] };
export type Readonly<T> = { readonly [K in keyof T]: T[K] };
export type Pick<T, K extends keyof T> = { [P in K]: T[P] };
export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
export type Exclude<T, U> = T extends U ? never : T;
export type Extract<T, U> = T extends U ? T : never;
export type NonNullable<T> = T extends null | undefined ? never : T;
export type Record<K extends keyof any, V> = { [P in K]: V };
export type ReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : never;
export type Parameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never;
}

Practical Examples

DTO Transformation Pattern

// Domain model
interface UserEntity {
id: number;
name: string;
email: string;
passwordHash: string;
salt: string;
createdAt: Date;
updatedAt: Date;
deletedAt: Date | null;
}

// Response DTO: strip sensitive fields
type UserResponseDto = Omit<UserEntity, "passwordHash" | "salt" | "deletedAt">;

// Create DTO: strip server-generated fields, add plain password
type CreateUserDto = Omit<UserEntity, "id" | "passwordHash" | "salt" | "createdAt" | "updatedAt" | "deletedAt"> & {
password: string;
passwordConfirm: string;
};

// Update DTO: id is required, everything else is optional
type UpdateUserDto = Partial<Omit<CreateUserDto, "password" | "passwordConfirm">> & {
id: number;
};

// Usage
const createDto: CreateUserDto = {
name: "Alice",
email: "alice@example.com",
password: "secure123",
passwordConfirm: "secure123",
};

const updateDto: UpdateUserDto = {
id: 1,
name: "Alice Updated", // only name is updated
};

Config Object Merge Pattern

// Safely merge default config with user config
interface ServerConfig {
host: string;
port: number;
ssl: boolean;
timeout: number;
maxConnections: number;
cors: {
origin: string[];
credentials: boolean;
};
}

type UserServerConfig = Partial<ServerConfig>;

const defaults: Required<ServerConfig> = {
host: "localhost",
port: 3000,
ssl: false,
timeout: 30000,
maxConnections: 100,
cors: {
origin: ["*"],
credentials: false,
},
};

function createServerConfig(userConfig: UserServerConfig): Required<ServerConfig> {
return {
...defaults,
...userConfig,
cors: {
...defaults.cors,
...(userConfig.cors ?? {}),
},
};
}

const config = createServerConfig({
port: 8080,
ssl: true,
});
// host: "localhost", port: 8080, ssl: true, ...rest are defaults

Update Payload Type

// Safely update any entity
type UpdatePayload<T extends { id: number }> = Pick<T, "id"> & Partial<Omit<T, "id">>;

interface Product {
id: number;
name: string;
price: number;
stock: number;
category: string;
}

type UpdateProductPayload = UpdatePayload<Product>;
// { id: number; name?: string; price?: number; stock?: number; category?: string; }

function updateProduct(payload: UpdateProductPayload): Promise<Product> {
return fetch(`/api/products/${payload.id}`, {
method: "PATCH",
body: JSON.stringify(payload),
}).then((r) => r.json());
}

updateProduct({ id: 1, price: 29.99 }); // update price only
updateProduct({ id: 2, stock: 0, name: "Out of Stock" }); // update stock and name
updateProduct({ id: 3 }); // id only (no changes)

Pro Tips

Combining Utility Types

// Pattern 1: Partial + Required to make only some fields required
type PartialExcept<T, K extends keyof T> = Partial<T> & Required<Pick<T, K>>;

interface Form {
username: string;
email: string;
bio?: string;
avatar?: string;
website?: string;
}

// username and email are required, everything else is optional
type CreateProfileForm = PartialExcept<Form, "username" | "email">;

const form1: CreateProfileForm = { username: "alice", email: "a@b.com" }; // OK
const form2: CreateProfileForm = { email: "a@b.com" }; // Error: username required

// Pattern 2: Omit + Record for type replacement
type ReplaceField<T, K extends keyof T, V> = Omit<T, K> & Record<K, V>;

type UserWithStringId = ReplaceField<User, "id", string>;
// id is now string; everything else stays the same

// Pattern 3: Nested utilities
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

const config2: DeepReadonly<ServerConfig> = {
host: "localhost",
port: 3000,
ssl: false,
timeout: 30000,
maxConnections: 100,
cors: { origin: ["*"], credentials: false },
};
config2.cors.origin = []; // Error: readonly

Implementing DeepPartial

// Make every property optional at all nesting levels
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;

interface AppState {
user: {
profile: {
name: string;
avatar: string;
};
settings: {
theme: "light" | "dark";
language: string;
notifications: {
email: boolean;
push: boolean;
};
};
};
posts: {
list: Post[];
selectedId: number | null;
};
}

// Deeply nested structures can now be partially updated
type PartialAppState = DeepPartial<AppState>;

const patch: PartialAppState = {
user: {
settings: {
notifications: {
email: false, // turn off email notifications only
},
},
},
};

Designing API Layer Types with Utility Types

// Central pattern for managing all API types in one place
interface UserEntity {
id: number;
name: string;
email: string;
passwordHash: string;
createdAt: Date;
}

// Derive all API layer types from a single source
const UserTypes = {
Response: {} as Omit<UserEntity, "passwordHash">,
Create: {} as Omit<UserEntity, "id" | "passwordHash" | "createdAt"> & { password: string },
Update: {} as Partial<Omit<UserEntity, "id" | "passwordHash" | "createdAt">> & { id: number },
List: {} as Pick<UserEntity, "id" | "name" | "email">[],
};

type UserResponse = typeof UserTypes.Response;
type CreateUser = typeof UserTypes.Create;
type UpdateUser = typeof UserTypes.Update;
type UserList = typeof UserTypes.List;

Summary Table

Utility TypeDescriptionInternal Implementation
Partial<T>All properties optional{ [K in keyof T]?: T[K] }
Required<T>All properties required{ [K in keyof T]-?: T[K] }
Readonly<T>All properties read-only{ readonly [K in keyof T]: T[K] }
Pick<T, K>Select a subset of properties{ [P in K]: T[P] }
Omit<T, K>Exclude a subset of propertiesPick<T, Exclude<keyof T, K>>
Exclude<T, U>Remove types from a unionT extends U ? never : T
Extract<T, U>Extract types from a unionT extends U ? T : never
NonNullable<T>Remove null/undefinedT extends null | undefined ? never : T
Record<K, V>Key-value map type{ [P in K]: V }
ReturnType<T>Extract function return typeConditional type + infer
Parameters<T>Extract function parameter typesConditional type + infer
Awaited<T>Unwrap Promise typeRecursive conditional type

Up Next...

Section 5.4 dives deep into Conditional Types. You will learn the T extends U ? X : Y syntax, understand distributive conditional types that automatically fan out over union members, master the infer keyword for extracting types inside conditions, and directly implement complex utility types using conditional types.

Advertisement