PartialByKeys
PartialByKeys
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] }
: neverThe 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:
| Goal | Pattern |
|---|---|
| Make K optional | Omit<T, K> & Partial<Pick<T, K>> |
| Make K required | Omit<T, K> & Required<Pick<T, K>> |
| Make K readonly | Omit<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] } : neverHere, 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
- Intersection flattening (
T extends infer O ? { [K in keyof O]: O[K] } : never) is the canonical idiom to turn an intersection type into a plain object type. Always apply it when your result must satisfy strict equality checks. Omit<T, K> & Partial<Pick<T, K>>is the idiomatic pattern for selectively making a subset of keys optional (or applying any other modifier). SwapPartialforRequiredorReadonlyto adapt the pattern.- Constrained default type parameters (
K extends keyof T = keyof T) provide both a fallback value and a compile-time guard in a single declaration — the default makes the parameter optional, and the constraint rejects invalid inputs. @ts-expect-errorin test suites is a positive assertion: it verifies that the type system correctly rejects an invalid call, confirming your constraint is working.
