Deep readonly
Write DeepReadonly<T>: every property is readonly at every depth, including arrays and the objects inside them. Function types stay as they are, so a method is still callable.
Start from
// Make every property readonly, at every depth.
type DeepReadonly<T> = any;Hints
Each hint gives away a little more. Open one only when you need it.
Hint 1
Three cases, in this order: a function (leave it), an object (map it), anything else (leave it).
Hint 2
The mapped type calls itself on each value: { readonly [K in keyof T]: DeepReadonly<T[K]> }. Mapped over an array type, it gives a readonly array.
Solution
From here on the page gives the method away. If you are still solving, go back to the hints.
TypeScript
type DeepReadonly<T> = T extends (...args: any[]) => any
? T
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;The checks it must pass
type _1 = Expect<Equal<DeepReadonly<{ a: number; b: string }>, { readonly a: number; readonly b: string }>>;
type _2 = Expect<Equal<DeepReadonly<{ a: { b: { c: number } } }>, { readonly a: { readonly b: { readonly c: number } } }>>;
type _3 = Expect<Equal<DeepReadonly<{ list: { id: number }[] }>, { readonly list: readonly { readonly id: number }[] }>>;
type _4 = Expect<Equal<DeepReadonly<{ run: () => void }>, { readonly run: () => void }>>;
declare const frozen: DeepReadonly<{ a: { b: number } }>;
// @ts-expect-error
frozen.a.b = 2;What an interviewer looks for
Recursive types protect state and configuration from mutation. Interviewers watch whether you handle functions and arrays, and whether you know readonly is checked only at compile time.