Quick Overview
A type alias may reference itself, which is what allows a tree node or a JSON value to be described at all. Mapping over the keys recursively is how `readonly` reaches every level instead of only the first.
Built-in Readonly<T> only marks top-level properties as read-only. For deeply nested object graphs (like state trees or JSON configs), you need Deep Readonly recursive types.
A type alias is recursive when it refers to itself inside its own definition. This is ideal for modeling JSON values or folder tree structures:
// Recursive JSON Value Type
type JSONValue =
| string
| number
| boolean
| null
| JSONValue[]
| { [key: string]: JSONValue };
const configData: JSONValue = {
appName: "DevLoom",
versions: [1, 2, 3],
nested: {
enabled: true,
},
};
Map through object properties recursively to enforce immutability at all nesting levels:
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
type AppState = {
user: {
profile: {
name: string;
};
};
};
const state: DeepReadonly<AppState> = {
user: { profile: { name: "Alex" } },
};
// state.user.profile.name = "New Name"; // Error: Cannot assign to read-only property!
T[K] extends object before recursing!Use recursive types for complex state trees. Next, let's enter Phase 4 and explore Classes, Access Modifiers, and Implements!
TypeScript erases entirely at build time, so a `string` annotation produces no runtime check whatsoever. A value arriving from an API is whatever the server actually sent, regardless of what the annotation claims.
`let x = "GET"` infers `string` because the binding can change; `const x = "GET"` infers the literal `"GET"`. That widening rule is why object properties need `as const` to stay narrow.
`string[]` is any number of strings; `[string, number]` is exactly two, in that order. `readonly` removes the mutating methods at compile time, so `push` becomes a type error rather than a runtime surprise.