Build MutableKeys<T>, a union of the non-readonly keys of an object type. Assignability checks ignore readonly, so detecting it takes the Equal trick.
MutableKeys<T> picks all the mutable (not readonly) keys of an object type into a union. This is trickier than it sounds: readonly is invisible to extends-based assignability checks, so the usual conditional-type probes can't see it. You need the strictest comparison tool the type system offers, the Equal trick, combined with key filtering via as-remapping.
For example:
type Keys = MutableKeys<{ readonly foo: string; bar: number }>;
// expected to be 'bar'Implement the advanced util type MutableKeys<T>, which picks all the mutable (not readonly) keys into a union.
For example:
type Keys = MutableKeys<{ readonly foo: string; bar: number }>;
// expected to be "bar"View on GitHub: https://tsch.js.org/5181
Change the following code to make the test cases pass (no type check errors).
π Lifetime-License is leaving on August 10, 2026
Get it now for $29
One-time payment. Lifetime access to all pro challenges.
The solution needs a helper:
type IsEqual<X, Y> =
(<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2
? true
: false
type MutableKeys<T> = keyof {
[K in keyof T as IsEqual<Pick<T, K>, Readonly<Pick<T, K>>> extends true
? never
: K]: T[K]
}The strategy: for each key, build two single-property types, one as-is and one force-readonly, then ask whether they're identical. If making the property readonly changed nothing, it was readonly already.
readonlyYour first instinct might be a conditional type, but TypeScript deliberately ignores readonly when checking assignability:
type A = { readonly x: number } extends { x: number } ? true : false // true
type B = { x: number } extends { readonly x: number } ? true : false // also trueBoth directions succeed, so no extends check can distinguish a readonly property from a mutable one. You need something stronger than assignability: strict identity.
Equal trickIsEqual<X, Y> is the same helper the test suite uses to grade your answer:
type IsEqual<X, Y> =
(<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2
? true
: falseIt wraps X and Y in two generic function signatures and asks whether one signature is assignable to the other. The compiler can only confirm that by proving the two conditional types T extends X ? 1 : 2 and T extends Y ? 1 : 2 would resolve the same way for every possible T, which forces it to compare X and Y for exact structural identity, modifiers included. Unlike plain extends, this check does see readonly:
[object Object]You don't need to memorize how the internals work. The takeaway is that this idiom is the type-level ===, and it's the standard tool whenever modifiers or any need to be told apart.
For each key K, Pick<T, K> extracts a single-property object type with its modifiers intact, and Readonly<...> produces the readonly version of it. For T = { a: number; readonly b: string }:
// K = 'a': Pick<T, 'a'> = { a: number }
// Readonly<Pick<T, 'a'>> = { readonly a: number }
// IsEqual β false β 'a' is mutable
// K = 'b': Pick<T, 'b'> = { readonly b: string }
// Readonly<Pick<T, 'b'>> = { readonly b: string }
// IsEqual β true β 'b' was already readonlyThe question 'is this property readonly?' becomes 'does Readonly change anything?', a comparison the Equal trick can answer.
as + keyofThe mapped type uses as-remapping to filter: keys where the probe says 'already readonly' are remapped to never, which removes them from the object entirely; mutable keys keep their names. Wrapping the whole thing in keyof then collects the surviving keys into a union:
// { a: number; readonly b: string }
// β mapped object: { a: number }
// β keyof: 'a'Why Pick<T, K> inside the probe instead of comparing T[K]? Because T[K] is just the value type. The readonly modifier lives on the property, so the comparison must happen between one-property object types.
{ a: undefined; readonly b: undefined } β 'a': both value types are identical (undefined), so only a modifier-aware comparison separates them.{ a: undefined; readonly b?: undefined; c: string; d: null } β 'a' | 'c' | 'd': b is readonly and optional. Pick preserves the ? on both sides of the comparison, so it doesn't interfere with the readonly check.{} β never: an empty mapped object has no keys, and keyof {} is never. No special case needed.The same skeleton with the branches swapped gives you ReadonlyKeys<T>, and the Equal-probe pattern generalizes to any question that plain assignability is too forgiving to answer.
This challenge is originally from here.