Lesson overview · Free interview practice

Utility Types

Explore built-in utility types and their uses.

Topics in the full lesson
  • Introduction to Utility Types
  • Partial
  • Readonly
  • Pick
  • Omit
  • Record
  • Exclude
  • Extract
  • Other Utility Types
  • Creating Custom Utility Types
  • Best Practices
  • Exercises

Practice Problems

Work through a question before revealing its explanation. These questions and answers are free; Premium adds the full lesson walkthrough, examples and implementation detail.

Completion marks record your own progress, not an automatically checked result. The task type does not determine whether it is optional.

TheoryMedium

`Partial<T>` and `Required<T>`

Question

What do Partial<T> and Required<T> do? Show their implementations and the canonical use cases.

Take a moment to think about this before revealing the answer

Explain your reasoning or try an implementation before comparing answers.

TheoryMedium

Filter and Introspection Utilities

Question

TypeScript has utility types for filtering unions and introspecting functions. Show Exclude/Extract/NonNullable and ReturnType/Parameters.

Take a moment to think about this before revealing the answer

Explain your reasoning or try an implementation before comparing answers.

TheoryMedium

`Pick<T, K>`, `Omit<T, K>`, and `Record<K, T>`

Question

What do Pick, Omit, and Record do? Show the implementations and the common pitfalls.

Take a moment to think about this before revealing the answer

Explain your reasoning or try an implementation before comparing answers.

CodingMedium

Mapped Types: The Foundation of Utility Types

Implement and check your solution in the stated environment before revealing the answer.

Question

Implement four shallow mapped types from scratch: MyPartial<T>, MyRequired<T>, MyReadonly<T> and Mutable<T>. Then explain what a mapped type does and which mapping modifier each implementation uses.

Role: core-practice. You should already understand generic type parameters, keyof, indexed access with T[K] and optional/readonly properties. This is type-level work, so JavaScript execution is not a substitute for checking it with the compiler.

The contract

Use a mapped type over keyof T, not aliases to built-in utilities:

UtilityChangeKeep
MyPartial<T>Make every property optionalExisting readonly flags and value types
MyRequired<T>Remove optional flagsExisting readonly flags, with the object/tuple distinction below
MyReadonly<T>Make every property readonlyExisting optional flags and value types
Mutable<T>Remove readonly flagsExisting optional flags and value types

Support ordinary object types with string, number and symbol keys, plus TypeScript's homomorphic array and tuple behavior. For example, MyReadonly<number[]> is readonly number[]. These are shallow transforms, not recursive ones. A nested mutable object stays mutable even when its containing property is readonly.

With the options below, optional name?: string does not permit an explicit name: undefined. Removing its optional flag gives name: string. For object properties, an explicitly declared string | undefined union survives removing optionality.

Optional tuple elements behave differently in TypeScript 5.9.3. MyRequired<readonly [value?: string | undefined]> must match built-in Required and produce readonly [value: string], which rejects [undefined]. An already-required tuple element declared as string | undefined keeps that union. Follow the pinned compiler's mapped-type behavior rather than inventing a different deep or tuple transformation.

Do not redefine global names like Partial, use any to bypass checks or change the compiler options. Runtime validation, deep freezing, key remapping and special handling for class instances or built-ins are not required.

Set up before opening the answer

Use TypeScript 5.9.3. Confirm that tsc --version prints Version 5.9.3. In a fresh folder, save the following three files.

solution.ts:

export type MyPartial<T> = never
export type MyRequired<T> = never
export type MyReadonly<T> = never
export type Mutable<T> = never

The never aliases are deliberate placeholders. Replace their right-hand sides.

tsconfig.json:

{
  "compilerOptions": {
    "strict": true,
    "exactOptionalPropertyTypes": true,
    "noUncheckedIndexedAccess": true,
    "noEmit": true,
    "target": "ES2022",
    "module": "CommonJS",
    "lib": ["ES2022"],
    "types": []
  },
  "files": ["solution.ts", "checks.ts"]
}

checks.ts:

import type { MyPartial, MyRequired, MyReadonly, Mutable } from './solution'

type Equal<A, B> =
  (<T>() => T extends A ? 1 : 2) extends
  (<T>() => T extends B ? 1 : 2) ? true : false
type Expect<T extends true> = T

declare const tag: unique symbol
interface User {
  readonly id: number
  name?: string
  explicit?: string | undefined
  details: { active: boolean }
  7: boolean
  [tag]: string
}

type PartialMatches = Expect<Equal<MyPartial<User>, Partial<User>>>
type RequiredMatches = Expect<Equal<MyRequired<User>, Required<User>>>
type ReadonlyMatches = Expect<Equal<MyReadonly<User>, Readonly<User>>>
type MutableMatches = Expect<Equal<Mutable<User>, {
  id: number
  name?: string
  explicit?: string | undefined
  details: { active: boolean }
  7: boolean
  [tag]: string
}>>
type ReadonlyArrayCase = Expect<Equal<MyReadonly<number[]>, readonly number[]>>
type MutableArrayCase = Expect<Equal<Mutable<readonly number[]>, number[]>>
type TupleCase = Expect<Equal<MyRequired<readonly [number, string?]>, readonly [number, string]>>
type PartialTuple = Expect<Equal<MyPartial<[number, string]>, [number?, string?]>>
type EmptyCase = Expect<Equal<MyPartial<{}>, {}>>

const partial: MyPartial<User> = {}
// @ts-expect-error MyPartial must retain the source readonly modifier.
partial.id = 2
// @ts-expect-error Optional does not mean explicit undefined with these options.
partial.name = undefined
partial.explicit = undefined
// @ts-expect-error Values must keep their original type.
partial[7] = 'no'

const complete: MyRequired<User> = {
  id: 1, name: 'Ada', explicit: undefined, details: { active: true },
  7: true, [tag]: 'tag',
}
// @ts-expect-error Removing optional must require name.
const missing: MyRequired<User> = { id: 1, explicit: undefined, details: { active: true }, 7: true, [tag]: 'tag' }
// @ts-expect-error Removing optional does not add undefined to name.
complete.name = undefined
// @ts-expect-error MyRequired must preserve readonly.
complete.id = 2

const readonly: MyReadonly<{ value: number; nested: { count: number } }> = {
  value: 1, nested: { count: 1 },
}
// @ts-expect-error The top-level property must be readonly.
readonly.value = 2
readonly.nested.count = 2
const mutable: Mutable<{ readonly id: number; name?: string }> = { id: 1 }
mutable.id = 2
// @ts-expect-error Mutable removes readonly, not value types.
mutable.id = 'two'
const numbers: MyReadonly<number[]> = [1]
// @ts-expect-error A readonly array must not expose push.
numbers.push(2)
const tuple: MyRequired<readonly [number, string?]> = [1, 'yes']
// @ts-expect-error The optional tuple position is now required.
const shortTuple: MyRequired<readonly [number, string?]> = [1]

type OptionalExplicitTuple = MyRequired<readonly [value?: string | undefined]>
type OptionalTupleMatchesBuiltin = Expect<Equal<
  OptionalExplicitTuple, Required<readonly [value?: string | undefined]>
>>
type OptionalTupleRemovesUndefined = Expect<Equal<OptionalExplicitTuple, readonly [value: string]>>
const objectValue: MyRequired<{ value?: string | undefined }> = { value: undefined }
const tupleValue: OptionalExplicitTuple = ['text']
const requiredTupleValue: MyRequired<readonly [value: string | undefined]> = [undefined]
// @ts-expect-error Removing an optional tuple flag also removes undefined in 5.9.3.
const undefinedTupleValue: OptionalExplicitTuple = [undefined]
// @ts-expect-error The built-in has the same pinned optional-tuple behavior.
const undefinedBuiltinTuple: Required<readonly [value?: string | undefined]> = [undefined]

Run tsc -p tsconfig.json. The unfinished aliases should fail the positive checks. A correct implementation exits successfully with no output. Each @ts-expect-error marks a line that must be rejected. If such a line starts compiling, TypeScript reports an unused directive.

These files are compile-time tests, not a program to execute. Stop once the checks pass and you can explain why changing a top-level modifier does not recursively transform nested objects.

Take a moment to think about this before revealing the answer

Explain your reasoning or try an implementation before comparing answers.

CodingHard

Custom Utility Types: Building Your Own

Implement and check your solution in the stated environment before revealing the answer.

Question

Implement DeepPartial<T>, Mutable<T>, RequireOne<T, K> and PromiseValue<T>. Explain which mapped-type, conditional-type and union patterns you used.

Role: advanced-extension. First be comfortable with mapping modifiers, keyof, indexed access, conditional types, infer, unions/intersections and a recursive base case. Start with the shallow mapped-types practice if those ideas are still new.

Keep the four contracts distinct

  • DeepPartial<T> recursively makes object properties optional while preserving readonly flags and value types. Leave primitives and callable function types unchanged. Distribute over unions. For arrays, use homomorphic optional mapping, so elements are recursively transformed and may also be undefined. Tuple positions become optional and readonly arrays/tuples stay readonly.
  • Mutable<T> removes readonly at one level only. Preserve optional flags. Readonly arrays become mutable arrays. A nested readonly object remains readonly.
  • RequireOne<T, K> requires at least one, not exactly one, of a finite set of known literal candidate keys in K. Other candidate keys become optional. Multiple candidates are allowed. Properties outside K keep their original requirements and modifiers. K must be a key subset of T, and K = never produces never. Presence follows the property's value type, so an explicitly declared undefined can be a valid required value.
  • PromiseValue<T> extracts exactly one layer from a type assignable to Promise<V>. Non-matching types produce never. It distributes over unions. A plain PromiseLike<V> does not suffice. Do not turn it into recursive Awaited<T>.

Use finite, reasonably sized data-record shapes for DeepPartial, including arrays/tuples and callable leaves. Dates, maps, sets, promises inside those records, class instances and recursive depth management are outside that transform's scope.

For RequireOne, use a non-union record with known finite property keys and a finite union of string literals, number literals or unique-symbol keys for K. Broad index signatures and open-ended template-pattern key domains are outside the exercise. K extends keyof T does not enforce this finiteness rule. The small reference can accept {} for RequireOne<Record<string, number>, string>, so do not treat it as a non-empty dictionary type. You do not need to reject every out-of-scope type. No runtime transformation or validation is required.

Write DeepPartial, Mutable and PromiseValue yourself. You may compose built-in Omit, Pick, Exclude, Required and Partial for RequireOne. Do not use any or broad assertions to silence failures.

Set up before opening the answer

Use TypeScript 5.9.3, not Node's type stripping. Confirm tsc --version prints Version 5.9.3. In a fresh folder, save these files.

solution.ts:

export type DeepPartial<T> = never
export type Mutable<T> = never
export type RequireOne<T, K extends keyof T> = never
export type PromiseValue<T> = never

tsconfig.json:

{
  "compilerOptions": {
    "strict": true,
    "exactOptionalPropertyTypes": true,
    "noUncheckedIndexedAccess": true,
    "noEmit": true,
    "target": "ES2022",
    "module": "CommonJS",
    "lib": ["ES2022"],
    "types": []
  },
  "files": ["solution.ts", "checks.ts"]
}

checks.ts:

import type { DeepPartial, Mutable, RequireOne, PromiseValue } from './solution'

type Equal<A, B> =
  (<T>() => T extends A ? 1 : 2) extends
  (<T>() => T extends B ? 1 : 2) ? true : false
type Expect<T extends true> = T

interface Config {
  readonly id: number
  api: { url: string; auth: { token: string } }
  callback: (value: number) => string
}
const update: DeepPartial<Config> = { api: { auth: {} }, callback: String }
const empty: DeepPartial<Config> = {}
const returned: string | undefined = update.callback?.(1)
// @ts-expect-error Recursive optionality must not erase value types.
const wrongNested: DeepPartial<Config> = { api: { url: 4 } }
// @ts-expect-error Readonly survives at each mapped level.
update.id = 2
// @ts-expect-error Optional still rejects explicit undefined unless declared.
const wrongUndefined: DeepPartial<Config> = { api: undefined }
// @ts-expect-error The callback signature remains unchanged.
const wrongCallback: DeepPartial<Config> = { callback: (value: string) => value }

type FunctionCase = Expect<Equal<
  DeepPartial<(value: number) => string>, (value: number) => string
>>
type Callable = ((value: number) => string) & { readonly version: number }
type CallableCase = Expect<Equal<DeepPartial<Callable>, Callable>>
type PrimitiveCase = Expect<Equal<DeepPartial<string | null | undefined>, string | null | undefined>>
type UnionCase = Expect<Equal<
  DeepPartial<{ a: number } | { b: string }>, { a?: number } | { b?: string }
>>
type ArrayCase = Expect<Equal<DeepPartial<{ id: number }[]>, ({ id?: number } | undefined)[]>>
type ReadonlyArrayCase = Expect<Equal<
  DeepPartial<readonly { id: number }[]>, readonly ({ id?: number } | undefined)[]
>>
type TupleCase = Expect<Equal<
  DeepPartial<readonly [{ id: number }, string]>, readonly [{ id?: number }?, string?]
>>
type NestedReadonlyCase = Expect<Equal<
  DeepPartial<{ nested: { readonly id: number } }>,
  { nested?: { readonly id?: number } }
>>
const array: DeepPartial<{ id: number }[]> = [{}, undefined]
const tuple: DeepPartial<readonly [{ id: number }, string]> = [{}]
// @ts-expect-error Readonly tuples stay readonly.
tuple[0] = {}

const mutable: Mutable<{ readonly nested: { readonly id: number }; label?: string }> = {
  nested: { id: 1 },
}
mutable.nested = { id: 2 }
// @ts-expect-error Mutable is shallow.
mutable.nested.id = 3
type MutableArrayCase = Expect<Equal<Mutable<readonly number[]>, number[]>>
type MutableTupleCase = Expect<Equal<Mutable<readonly [number, string?]>, [number, string?]>>

interface Contact {
  readonly requestId: number
  email?: string
  sms?: string
  note?: string
}
type Delivery = RequireOne<Contact, 'email' | 'sms'>
const byEmail: Delivery = { requestId: 1, email: 'a@example.test' }
const bySms: Delivery = { requestId: 1, sms: '123' }
const both: Delivery = { requestId: 1, email: 'a@example.test', sms: '123' }
// @ts-expect-error At least one selected key is required.
const neither: Delivery = { requestId: 1 }
// @ts-expect-error Keys outside K keep their original requirements.
const noId: Delivery = { email: 'a@example.test' }
// @ts-expect-error An optional string candidate cannot be undefined.
const undefinedEmail: Delivery = { requestId: 1, email: undefined }
// @ts-expect-error Unselected readonly properties stay readonly.
byEmail.requestId = 2
// @ts-expect-error K must be a key of T.
type UnknownKey = RequireOne<Contact, 'fax'>
type NoCandidates = Expect<Equal<RequireOne<Contact, never>, never>>
const explicitUndefined: RequireOne<{ email?: string | undefined }, 'email'> = {
  email: undefined,
}

declare const selected: unique symbol
type LiteralCandidates = RequireOne<{
  readonly [selected]?: string
  1?: string
  outside: boolean
}, typeof selected | 1>
const symbolCandidate: LiteralCandidates = { [selected]: 'yes', outside: true }
const numericCandidate: LiteralCandidates = { 1: 'yes', outside: true }
// @ts-expect-error A finite literal-key union still needs at least one candidate.
const noLiteralCandidate: LiteralCandidates = { outside: true }
// @ts-expect-error Selecting a readonly candidate does not remove its modifier.
symbolCandidate[selected] = 'changed'

type PromiseCase = Expect<Equal<PromiseValue<Promise<string>>, string>>
type NestedPromiseCase = Expect<Equal<PromiseValue<Promise<Promise<number>>>, Promise<number>>>
type MixedCase = Expect<Equal<PromiseValue<Promise<string> | number>, string>>
type PlainCase = Expect<Equal<PromiseValue<string>, never>>
type ThenableCase = Expect<Equal<PromiseValue<PromiseLike<number>>, never>>
type NeverCase = Expect<Equal<PromiseValue<never>, never>>
// @ts-expect-error PromiseValue extracts the declared fulfillment type.
const wrongValue: PromiseValue<Promise<number>> = 'no'

Run tsc -p tsconfig.json. The starter's never placeholders should fail. A completed solution should pass both the positive assertions and the marked negative cases without changing them. These are compile-time tests, so do not execute checks.ts.

Stop when those four bounded transformations work. Deep support for every built-in type and exactly-one selection would be different exercises.

Take a moment to think about this before revealing the answer

Explain your reasoning or try an implementation before comparing answers.

Explore the full Utility Types material

Premium includes the complete lessons and implementation references. Free practice questions remain available without a subscription.

All course tracks & premium content
From basics to advanced masterclasses
Built for JS/TS developers like you
Real-world tips & common pitfalls
Upgrade to Premium