Yuzhe's Blog

yuzhes

PartialByKeys

PartialByKeys

Challenge Link

Challenge

Implement a generic PartialByKeys<T, K> which takes two type arguments T and K.

K specifies the set of properties of T that should be set to optional. When K is not provided, it should make all properties optional just like the normal Partial<T>.

interface User {
  name: string
  age: number
  address: string
}

type UserPartialName = PartialByKeys<User, 'name'>
// { name?: string; age: number; address: string }

Solution

type PartialByKeys<T, K extends keyof T = keyof T> =
  Omit<T, K> & Partial<Pick<T, K>> extends infer O
    ? { [P in keyof O]: O[P] }
    : never

The solution splits T into two parts, recombines them, then flattens the result into a plain object type.

Step 1 — Split: Omit<T, K> keeps every property not in K as required. Partial<Pick<T, K>> isolates the K properties and makes them optional.

Step 2 — Merge: The & intersection combines both halves back into a single type.

Step 3 — Flatten: The raw intersection type Omit<T, K> & Partial<Pick<T, K>> is structurally identical to the desired object but won’t satisfy TypeScript’s strict Equal<> check because it’s an intersection, not a plain object. The extends infer O ? { [P in keyof O]: O[P] } : never idiom resolves this by re-mapping every key of O into a fresh object type.

Default constraint: K extends keyof T = keyof T serves two roles. The default = keyof T makes K optional so PartialByKeys<User> degrades to Partial<User>. The constraint extends keyof T ensures that passing an unknown key like 'unknown' is a compile-time error (as the @ts-expect-error test case verifies).

Deep Dive

Why Does the Intersection Need Flattening?

When TypeScript evaluates Equal<A, B>, it checks structural equivalence at a very precise level. An intersection { age: number; address: string } & { name?: string } and a plain object { age: number; address: string; name?: string } are assignable to each other, but they are not identical in the type system’s internal representation. The Equal utility used in type-challenges relies on the CheckNonNullable trick under the hood, which can distinguish these two forms.

The flattening idiom T extends infer O ? { [K in keyof O]: O[K] } : never forces TypeScript to re-evaluate the type as a single object literal, collapsing the intersection. This is one of the most commonly used “canonicalization” patterns in advanced TypeScript.

The Omit + Partial<Pick<>> Pattern

This pattern appears whenever you need to selectively apply a modifier to a subset of keys. The same structure can be adapted for other modifiers:

GoalPattern
Make K optionalOmit<T, K> & Partial<Pick<T, K>>
Make K requiredOmit<T, K> & Required<Pick<T, K>>
Make K readonlyOmit<T, K> & Readonly<Pick<T, K>>

Each variant follows the same three-step rhythm: split, modify, merge.

Alternative: Mapped Type with as Remapping

If you want to avoid built-in utilities, a single mapped type with as key remapping can achieve the same result:

type PartialByKeys<T, K extends keyof T = keyof T> = {
  [P in keyof T as P extends K ? never : P]: T[P]
} & {
  [P in K]?: T[P]
} extends infer O ? { [P in keyof O]: O[P] } : never

Here, the first mapped type uses as P extends K ? never : P to filter out the K keys (mapping them to never effectively removes them), and the second mapped type explicitly adds K keys back as optional. Both approaches produce identical results; the Omit/Pick version is more readable for most audiences.

Default Type Parameters

K extends keyof T = keyof T is a constrained default: the default value keyof T itself satisfies the constraint extends keyof T, so no special handling is needed. When no K is supplied, the type behaves exactly like Partial<T> because Omit<T, keyof T> is {} and Partial<Pick<T, keyof T>> is Partial<T>, and {} & Partial<T> flattens to Partial<T>.

Key Takeaways