DEV_NET_CORE
GET_STARTED
ReactTypeScript for React

Utility types and conditional types

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.

Code
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:

Code
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.

Code
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:

Code
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.

Code
type UserForm = {
  name: string;
  email: string;
  role: "admin" | "user";
};

type UserFormPatch = Partial<UserForm>;

Equivalent shape:

Code
type UserFormPatch = {
  name?: string;
  email?: string;
  role?: "admin" | "user";
};

React use case:

Code
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.

Code
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.

Code
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.

Code
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.

Code
type User = {
  id: string;
  name: string;
  email: string;
  passwordHash: string;
};

type PublicUser = Omit<User, "passwordHash">;
type UserCardProps = Pick<User, "id" | "name">;

React use case:

Code
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.

Code
type Role = "admin" | "user" | "guest";

const roleLabels: Record<Role, string> = {
  admin: "Administrator",
  user: "User",
  guest: "Guest",
};

This is useful for exhaustive lookup tables:

Code
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:

Code
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.

Code
type Config = Readonly<{
  apiBaseUrl: string;
  timeoutMs: number;
}>;

React use case:

Code
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.

Code
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.

Code
type MaybeUser = User | null | undefined;
type UserOnly = NonNullable<MaybeUser>;

This is useful when a runtime check guarantees presence:

Code
function requireUser(user: User | null): NonNullable<typeof user> {
  if (user === null) {
    throw new Error("Expected user");
  }

  return user;
}

React context example:

Code
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.

Code
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:

Code
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:

Code
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.

Code
function createUser(input: CreateUserRequest) {
  return {
    id: crypto.randomUUID(),
    ...input,
  };
}

type CreateUserArgs = Parameters<typeof createUser>;
type CreatedUser = ReturnType<typeof createUser>;

React custom hook example:

Code
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.

Code
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:

Code
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.

Code
type ButtonProps = React.ComponentProps<"button">;

function PrimaryButton(props: ButtonProps) {
  return <button {...props} className={`primary ${props.className ?? ""}`} />;
}

For custom components:

Code
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.

Code
type IsString<T> = T extends string ? true : false;

type A = IsString<string>;
type B = IsString<number>;

More practical:

Code
type ApiResponse<T> = T extends Error
  ? { ok: false; error: T }
  : { ok: true; data: T };

Conditional types are most useful with generics:

Code
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.

Code
type ArrayItem<T> = T extends Array<infer Item> ? Item : never;

type UserItem = ArrayItem<User[]>;

Promise example:

Code
type PromiseResult<T> = T extends Promise<infer Result> ? Result : T;

type A = PromiseResult<Promise<User>>;
type B = PromiseResult<string>;

Function example:

Code
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.

Code
type ToArray<T> = T extends unknown ? T[] : never;

type Result = ToArray<string | number>;

Result becomes:

Code
string[] | number[]

not:

Code
(string | number)[]

To prevent distribution, wrap each side in a tuple:

Code
type ToArrayNonDistributed<T> = [T] extends [unknown] ? T[] : never;

type Result = ToArrayNonDistributed<string | number>;

Now Result is:

Code
(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.

Code
type Flags<T> = {
  [Key in keyof T]: boolean;
};

type UserFlags = Flags<User>;

Many utility types are based on mapped types:

Code
type MyPartial<T> = {
  [Key in keyof T]?: T[Key];
};

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

React use case:

Code
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:

Code
type UserCardProps = Pick<User, "id" | "name">;

Explicit type:

Code
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 ReturnType everywhere 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 Pick for small component prop slices.
  • Use Omit carefully for adaptation, not security.
  • Use Record for exhaustive lookup maps.
  • Use Extract and Exclude for union filtering.
  • Use ReturnType, Parameters, and Awaited for 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.

Interview Practice

PreviousTypes, unions, intersections, and discriminated unionsNext UpControlled inputs and event handling