Overview
Utility types and conditional types are TypeScript features for creating new types from existing types. They help React developers avoid duplication, keep component APIs consistent, and express relationships between props, state, events, API responses, and reusable helpers.
Utility types are built-in helpers such as Partial, Required, Pick, Omit, Record, Readonly, NonNullable, Extract, Exclude, Parameters, ReturnType, and Awaited.
type User = {
id: string;
name: string;
email: string;
role: "admin" | "user";
};
type UserPreview = Pick<User, "id" | "name">;
type EditableUser = Omit<User, "id" | "role">;
type UserPatch = Partial<EditableUser>;
Conditional types use type-level logic:
type ApiData<T> = T extends Promise<infer Result> ? Result : T;
For interviews, this topic matters because it shows whether a developer can design type-safe APIs without overcomplicating the codebase. A strong candidate should know when to use built-in utility types, when to write custom generic helpers, how conditional types use extends, how infer extracts types, and why distributive conditional types can surprise people.
The practical goal is to reduce repeated type definitions while keeping types readable and connected to real runtime behavior.
Core Concepts
Creating Types from Types
TypeScript lets you derive new types from existing types. This is useful because applications often need several related versions of the same shape.
type User = {
id: string;
name: string;
email: string;
createdAt: string;
updatedAt: string;
};
type CreateUserRequest = Pick<User, "name" | "email">;
type UpdateUserRequest = Partial<CreateUserRequest>;
type UserListItem = Pick<User, "id" | "name">;
This avoids duplicating fields manually:
type BadCreateUserRequest = {
name: string;
email: string;
};
Manual duplication is sometimes fine, especially across domain boundaries, but derived types help when the relationship is intentional and should stay synchronized.
Partial<T>
Partial<T> makes every property optional.
type UserForm = {
name: string;
email: string;
role: "admin" | "user";
};
type UserFormPatch = Partial<UserForm>;
Equivalent shape:
type UserFormPatch = {
name?: string;
email?: string;
role?: "admin" | "user";
};
React use case:
function updateDraft(patch: Partial<UserForm>) {
setDraft((current) => ({
...current,
...patch,
}));
}
Common mistake: using Partial<T> for a value that must actually be complete before rendering or submission. Partial is good for patches, staged forms, and test builders, but not for finalized data.
Required<T>
Required<T> makes every property required.
type DraftSettings = {
theme?: "light" | "dark";
pageSize?: number;
};
type SavedSettings = Required<DraftSettings>;
Use this when a value moves from an incomplete stage to a complete stage.
function saveSettings(settings: SavedSettings) {
localStorage.setItem("settings", JSON.stringify(settings));
}
Be careful: Required<T> changes the type, not the runtime value. You still need runtime defaults or validation before treating optional fields as present.
function normalizeSettings(draft: DraftSettings): SavedSettings {
return {
theme: draft.theme ?? "light",
pageSize: draft.pageSize ?? 25,
};
}
Pick<T, K> and Omit<T, K>
Pick<T, K> selects a subset of properties. Omit<T, K> removes properties.
type User = {
id: string;
name: string;
email: string;
passwordHash: string;
};
type PublicUser = Omit<User, "passwordHash">;
type UserCardProps = Pick<User, "id" | "name">;
React use case:
type UserCardProps = Pick<User, "id" | "name"> & {
onSelect: (id: string) => void;
};
function UserCard({ id, name, onSelect }: UserCardProps) {
return <button onClick={() => onSelect(id)}>{name}</button>;
}
Pick is useful when a component needs only a small part of a larger model. Omit is useful when adapting a broad type but excluding fields that should not be exposed.
Avoid deriving public API contracts blindly from database models. Sometimes explicit duplication is better when the concepts should evolve independently.
Record<K, T>
Record<K, T> creates an object type whose keys are K and whose values are T.
type Role = "admin" | "user" | "guest";
const roleLabels: Record<Role, string> = {
admin: "Administrator",
user: "User",
guest: "Guest",
};
This is useful for exhaustive lookup tables:
type Status = "idle" | "loading" | "success" | "error";
const statusLabels: Record<Status, string> = {
idle: "Idle",
loading: "Loading",
success: "Success",
error: "Error",
};
If a new status is added, TypeScript requires the table to be updated.
Record<string, T> is also common:
type UsersById = Record<string, User>;
But prefer a narrower key union when the keys are known.
Readonly<T>
Readonly<T> makes properties read-only at compile time.
type Config = Readonly<{
apiBaseUrl: string;
timeoutMs: number;
}>;
React use case:
type Props = Readonly<{
user: User;
onSelect: (id: string) => void;
}>;
This communicates that the component should not mutate its props.
Important limitation: Readonly<T> is shallow.
type State = Readonly<{
user: {
name: string;
};
}>;
function mutate(state: State) {
state.user.name = "Ava"; // The nested object can still be mutable.
}
For deep immutability, you need a custom type or library convention. Also remember that TypeScript readonly does not freeze values at runtime.
NonNullable<T>
NonNullable<T> removes null and undefined from a type.
type MaybeUser = User | null | undefined;
type UserOnly = NonNullable<MaybeUser>;
This is useful when a runtime check guarantees presence:
function requireUser(user: User | null): NonNullable<typeof user> {
if (user === null) {
throw new Error("Expected user");
}
return user;
}
React context example:
type AuthContextValue = {
user: User;
logout: () => void;
};
const AuthContext = createContext<AuthContextValue | null>(null);
function useAuth(): NonNullable<React.ContextType<typeof AuthContext>> {
const value = useContext(AuthContext);
if (value === null) {
throw new Error("useAuth must be used inside AuthProvider");
}
return value;
}
In many cases, the explicit return type can simply be AuthContextValue. Use NonNullable when it improves clarity, not to make simple code clever.
Exclude<T, U> and Extract<T, U>
Exclude<T, U> removes union members assignable to U. Extract<T, U> keeps only union members assignable to U.
type Status = "idle" | "loading" | "success" | "error";
type FinishedStatus = Exclude<Status, "idle" | "loading">;
type PendingStatus = Extract<Status, "idle" | "loading">;
They are especially useful with discriminated unions:
type RequestState =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: User[] }
| { status: "error"; error: string };
type SuccessState = Extract<RequestState, { status: "success" }>;
type ErrorState = Extract<RequestState, { status: "error" }>;
React use case:
function SuccessView({ state }: { state: SuccessState }) {
return <UserList users={state.data} />;
}
This keeps component props aligned with the union model.
Parameters<T> and ReturnType<T>
Parameters<T> extracts a function's parameter tuple. ReturnType<T> extracts a function's return type.
function createUser(input: CreateUserRequest) {
return {
id: crypto.randomUUID(),
...input,
};
}
type CreateUserArgs = Parameters<typeof createUser>;
type CreatedUser = ReturnType<typeof createUser>;
React custom hook example:
function useUsers() {
return {
users: [] as User[],
reload: async () => {},
};
}
type UseUsersResult = ReturnType<typeof useUsers>;
This is useful when tests, context values, or child components need to reference the return shape of a hook.
Avoid overusing ReturnType when an explicit domain type would be clearer.
Awaited<T>
Awaited<T> models what comes out of await or .then.
async function fetchUser() {
const response = await fetch("/api/me");
return response.json() as Promise<User>;
}
type FetchedUser = Awaited<ReturnType<typeof fetchUser>>;
For a React data hook:
function useUserData(user: Awaited<ReturnType<typeof fetchUser>>) {
return user.name;
}
Awaited recursively unwraps Promises, which is useful for API client return types.
ComponentProps
React's type ecosystem includes helpers that are built from TypeScript's type system. A common example is extracting component prop types.
type ButtonProps = React.ComponentProps<"button">;
function PrimaryButton(props: ButtonProps) {
return <button {...props} className={`primary ${props.className ?? ""}`} />;
}
For custom components:
function UserCard({ user, onSelect }: { user: User; onSelect: (id: string) => void }) {
return <button onClick={() => onSelect(user.id)}>{user.name}</button>;
}
type UserCardProps = React.ComponentProps<typeof UserCard>;
This is useful when wrapping intrinsic elements or reusing a component's props in tests and stories. Be careful not to create confusing dependency cycles between components and types.
Conditional Types
A conditional type chooses one type or another based on an assignability check.
type IsString<T> = T extends string ? true : false;
type A = IsString<string>;
type B = IsString<number>;
More practical:
type ApiResponse<T> = T extends Error
? { ok: false; error: T }
: { ok: true; data: T };
Conditional types are most useful with generics:
type MaybeArray<T> = T | T[];
type ElementType<T> = T extends Array<infer Item> ? Item : T;
type A = ElementType<string[]>;
type B = ElementType<number>;
They let you describe relationships between input types and output types.
infer
infer introduces a type variable inside the true branch of a conditional type.
type ArrayItem<T> = T extends Array<infer Item> ? Item : never;
type UserItem = ArrayItem<User[]>;
Promise example:
type PromiseResult<T> = T extends Promise<infer Result> ? Result : T;
type A = PromiseResult<Promise<User>>;
type B = PromiseResult<string>;
Function example:
type FirstArgument<T> = T extends (arg: infer First, ...rest: never[]) => unknown
? First
: never;
Most application code does not need many custom infer types. They are more common in libraries, reusable hooks, and API helper layers.
Distributive Conditional Types
Conditional types distribute over unions when the checked type is a naked generic type parameter.
type ToArray<T> = T extends unknown ? T[] : never;
type Result = ToArray<string | number>;
Result becomes:
string[] | number[]
not:
(string | number)[]
To prevent distribution, wrap each side in a tuple:
type ToArrayNonDistributed<T> = [T] extends [unknown] ? T[] : never;
type Result = ToArrayNonDistributed<string | number>;
Now Result is:
(string | number)[]
This distinction is a frequent advanced interview topic because it explains why utilities such as Exclude and Extract work on each union member.
Mapped Types
Mapped types create object types by iterating over keys.
type Flags<T> = {
[Key in keyof T]: boolean;
};
type UserFlags = Flags<User>;
Many utility types are based on mapped types:
type MyPartial<T> = {
[Key in keyof T]?: T[Key];
};
type MyReadonly<T> = {
readonly [Key in keyof T]: T[Key];
};
React use case:
type FormErrors<T> = {
[Key in keyof T]?: string;
};
type LoginForm = {
email: string;
password: string;
};
type LoginFormErrors = FormErrors<LoginForm>;
This keeps form error keys aligned with form fields.
Utility Types vs Explicit Types
Utility types are not always better than explicit types.
Derived type:
type UserCardProps = Pick<User, "id" | "name">;
Explicit type:
type UserCardProps = {
id: string;
name: string;
};
Use derived types when the relationship is meaningful and should stay synchronized. Use explicit types when the concepts should be allowed to evolve independently.
For example, an API request type should not always be derived from an internal database model. A component prop type should describe what the component needs, not everything a domain object happens to contain.
Common Mistakes
Common mistakes include:
- Using
Partial<T>for data that must be complete. - Using
Omit<T, K>to hide sensitive fields but still sending the original object at runtime. - Forgetting that utility types are compile-time only.
- Creating unreadable chains like
Partial<Omit<Pick<T, K>, X>>. - Overusing conditional types in application code where explicit types would be clearer.
- Forgetting that conditional types distribute over unions.
- Using
ReturnTypeeverywhere instead of naming important domain types. - Expecting
Readonly<T>to deeply freeze values at runtime.
Best Practices
Use these rules of thumb:
- Prefer built-in utility types before writing custom ones.
- Keep derived types close to the source type.
- Use
Pickfor small component prop slices. - Use
Omitcarefully for adaptation, not security. - Use
Recordfor exhaustive lookup maps. - Use
ExtractandExcludefor union filtering. - Use
ReturnType,Parameters, andAwaitedfor reusable function and API helpers. - Use conditional types mostly for reusable abstractions, not everyday business logic.
- Name complex derived types so call sites stay readable.