Defining a new variable type:
type NestedObj = { [key: string]: NestedObj } | { [key: string]: number }
This variable type is a combination of two object types, each with a string index signature. It specifies that a `NestedObj` must be an object where all properties are either of type `number` or all are of type `NestedObj`. There is no restriction on the number of properties allowed within this object. Here are some examples to illustrate this behavior:
let n: NestedObj;
n = {}; // valid
n = { a: 0, b: 1, c: 2 }; // valid
n = { a: {}, b: {}, c: {} }; // valid
n = { a: { d: 0 }, b: { e: 1 }, c: { f: {} } }; // valid
n = { a: 0, b: 1, c: "abc" }; // invalid, string not allowed
n = { a: 0, b: 1, c: {} }; // invalid, mixing numbers and objects
n = { a: { d: 0 }, b: { e: 1 }, c: { f: { g: "abc" } } }; // invalid, nested string
It's important to note that the requirement of having "exactly ONE key" in the object is not enforced by this type definition.
To proceed, you can choose to either accept the given type as it is and create a type guard function for it, or modify the requirement and data structure to make it enforceable. For example, consider using [string, ...string[], number]
instead of {a:{b:{c:0}}}
. This way, you can validate the content more easily without introducing complex logic into the type guard function.
Creating a custom type guard function to check if an input conforms to the defined `NestedObj`, regardless of the number of keys:
const isNestedObj = (obj: any): obj is NestedObj => {
if (!obj) return false;
if (typeof obj !== "object") return false;
const values = Object.values(obj);
if (values.every(v => typeof v === "number")) return true;
return values.every(isNestedObj);
}
The above function ensures that `obj` is a non-null object, checks its property values using `Object.values()`, and verifies if they are all numbers or follow the `NestedObj` structure recursively. Testing this function yields consistent results based on the previous examples provided.
Note that if you encounter circular objects, like { a: {} }
, the function may fail due to recursion limits. To address this, you'd need to enhance `isNestedObj()` further to handle such scenarios properly.
Enforcing the "exactly one key" rule within a type guard function while keeping the original type definition might lead to unexpected outcomes:
const isNestedObj = (obj: any): obj is NestedObj => {
if (!obj) return false;
if (typeof obj !== "object") return false;
const values = Object.values(obj);
if (values.length !== 1) return false; // enforcing exactly one key
if (values.every(v => typeof v === "number")) return true;
return values.every(isNestedObj);
}
const o = Math.random() < 0.99 ? { a: 1, b: 2 } : "abc";
if (!isNestedObj(o)) {
o // "abc"
console.log(o.toUpperCase()) // potential runtime error
}
In the scenario presented above, even though {a: 1, b: 2}
matches the `NestedObj` type, the updated `isNestedObj({a: 1, b: 2})` function erroneously returns `false`. Due to how TypeScript interprets this mismatch, it may incorrectly infer the type of `o`. Careful consideration should be taken when tweaking type guards to avoid such pitfalls.
To resolve this issue, explore potential workarounds suggested by the TypeScript community or reconsider the type definitions to maintain consistency and clarity in your code.