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.
Theory questions
`Partial<T>` and `Required<T>`
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.
Filter and Introspection Utilities
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.
`Pick<T, K>`, `Omit<T, K>`, and `Record<K, T>`
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.
Coding practice
Mapped Types: The Foundation of Utility Types
Implement and check your solution in the stated environment before revealing the answer.
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:
| Utility | Change | Keep |
|---|---|---|
MyPartial<T> | Make every property optional | Existing readonly flags and value types |
MyRequired<T> | Remove optional flags | Existing readonly flags, with the object/tuple distinction below |
MyReadonly<T> | Make every property readonly | Existing optional flags and value types |
Mutable<T> | Remove readonly flags | Existing 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.
Custom Utility Types: Building Your Own
Implement and check your solution in the stated environment before revealing the answer.
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 beundefined. 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 inK. Other candidate keys become optional. Multiple candidates are allowed. Properties outsideKkeep their original requirements and modifiers.Kmust be a key subset ofT, andK = neverproducesnever. Presence follows the property's value type, so an explicitly declaredundefinedcan be a valid required value.PromiseValue<T>extracts exactly one layer from a type assignable toPromise<V>. Non-matching types producenever. It distributes over unions. A plainPromiseLike<V>does not suffice. Do not turn it into recursiveAwaited<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.