Challenge 4

The instructions for this challenge are:

Implement the built-in ReadonlyKeys<T> generic without using it.

Constructs a union of all the keys of T that are readonly.

Example code that should compile correctly is provided:

interface Todo {
    readonly title: string
    readonly description: string
    completed: boolean
}

type Keys = GetReadonlyKeys<Todo> // expected to be "title" | "description"

The initial type is:

type GetReadonlyKeys<T> = any

This is the first extreme challenge and my approach takes a few shortcuts:

  1. Instead of copying the implementation for challenge 4 to provide Pick, using the built-in.
  2. Using the Readonly utility type (not explicitly disallowed in the challenge). We will implement this later anyway.
  3. Using the utility function provided by the type repo Equal to handle part of the solution. It would be possible to reimplement this from scratch (I believe it may be a later challenge).

I took these shortcuts because it would be possible to bring in the other types in a modular style later if I should choose so.

The essential principle here follows on from the previous challenges. We want to map the indexes of the type and judge whether to include the index in the emitted type depending on whether the key is marked readonly or not. Another slight difference is that we want to emit a union type instead of a filtered object. This latter part is easy as we can simply generate the filtered object and then use keyof on it.

The key realisation that helps solve this problem is that using Pick and Readonly (along with Pick) we can generate two types that describe an object containing only one of the key -> value pairs from the original object. This gives us a few variations:

// original object 
type OriginalObject = { property1: string, readonly property2: string };

// pick only - property1
type PickedProperty1 = Pick<OriginalObject, property1> // { property1: string };
// pick and readonly - property1
type PickedReadonlyProperty1 = Readonly<Pick<OriginalObject, property1>> // { readonly property1: string };
// pick only - property2
type PickedProperty2 = Pick<OriginalObject, property2> // { readonly property2: string };
// pick and readonly - property2
type PickedReadonlyProperty2 = Readonly<Pick<OriginalObject, property1>> // { readonly property2: string };

PickedReadonlyProperty2 is the same as PickedProperty2 so all that remains now is to have some means of comparing the two. The typescript challenge repository has an Equal type that allows two types to be compared (not implementing this myself moves the challenge from extreme to medium in my opinion). If the two types are the same then e.g. Equal<PickedProperty2,PickedReadonlyProperty2> can only have a value of true and we can use a conditional type to determine if this is the case. The normal methods for filtering mapped indexes allows us to solve the problem for there onwards.

The eventual solution looks like this:

type GetReadonlyKeys<T> = keyof {
    [P in keyof T as Equal<
        Pick<T, K>,
        Readonly<Pick<T, K>>
    > extends true ? P : never]: T[P]
}

Blog

Miscellaneous thoughts.