#5181β€’Hard

Mutable Keys

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'

Challenge Instructions: Mutable Keys

Hard

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.

Loading...

Detailed Explanation

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.

Why assignability can't see readonly

Your 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 true

Both directions succeed, so no extends check can distinguish a readonly property from a mutable one. You need something stronger than assignability: strict identity.

The Equal trick

IsEqual<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
    : false

It 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.

Probing one key at a time

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 readonly

The question 'is this property readonly?' becomes 'does Readonly change anything?', a comparison the Equal trick can answer.

Filtering and collecting with as + keyof

The 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.

Edge cases the tests cover

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.

Share this challenge

Learn the Concepts