A guide on implementing a Type Guard check for an Indexed Property

I am working with a nestedObj type that utilizes an indexed signature like this:

type nestedObj = {[key: string]: nestedObj} | {[key: string]: number}

How can I go about creating a type guard for this nestedObj type?

  const isNestedObj = (obj: any): obj is nestedObj => {
      if (obj === null || obj === undefined)
         return false;

      ??????
      
      return true;
  }

========= EDIT ===========

Apologies, here is some additional information. The nested Obj will be in the format:

const testObj1 = { a: { b: { c: 42 } } };
const testObj2 = { ab: { bc: { cd: { d: 2 } } };
const testObj3 = { xy: 3};

The object should only have exactly ONE key.
It is also necessary to verify if obj is a valid non-null object.

I tried another approach and ended up with something like this

const isNestedObj = (obj: any): obj is nestedObj => {
    // terminating conditions
    if (typeof obj !== 'object' || obj === null || obj === undefined)
        return false
    
    // check if there is exactly one key
    const objKeys = Object.keys(obj);
    if (objKeys.length !== 1)
        return false
    
    // if the value of the key is a number, return true
    if (typeof obj[objKeys[0]] === 'number')
        return true

    else return isNestedObj(obj[objKeys[0]]);
}

This solution seems to be effective for me, but I'm unsure if it's the most optimal one

Answer №1

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.

Answer №2

You have the option to search for objects by traversing their keys:

type ComplexObj = {[key: string]: ComplexObj} | {[key: string]: number}

const isComplexObj = (obj: any): obj is ComplexObj => {
    if (obj === null || obj === undefined) {
        return false;
    }
    return Object.keys(obj).find(k => typeof obj[k] === "object") !== undefined;
}

function checkType(obj: any) {
  if (isComplexObj(obj)) {
    type t = typeof obj['something']; // ComplexObj | number
  } else {
    type t = typeof obj['something']; // any
  }
}

Alternatively, you could consider when an object is not nested:

type SimplenestedObj = {[key: string]: number};
type ComplexObj = {[key: string]: ComplexObj} | SimplenestedObj;

const isNotNested = (obj: any): obj is SimplenestedObj => {
    if (obj === null || obj === undefined) {
        return false;
    }
    const keys = Object.keys(obj);
    const allKeysAreNumbers = keys.filter(k => typeof obj[k] === "number").length === keys.length;
    return allKeysAreNumbers;
}

function checkType(obj: any) {
  if (isNotNested(obj)) {
    type t = typeof obj['something']; // number
  } else {
    type t = typeof obj['something']; // any
  }
}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

"Looking for a way to create a new line in my particular situation. Any tips

Here is the code snippet I am working with: let list: Array<string> =[]; page.on('request', request => list.push('>>', request.method(), request.url()+'\\n')); page.on('respon ...

Validating object values prior to adding a key

How can we add a new key-value pair called partnerCam to the res.items objects when partnerTermStart and partnerTermEnd are not null? If partnerTermStart and partnerTermEnd have values, then we should insert a new key called partnerCam with a value calcul ...

The typescript-eslint-parser does not officially support this version of TypeScript

I recently acquired an outdated AngularJs application that still relies on the legacy tools: bower and grunt. Upon executing grunt serve --reload, I encounter the following warning message: WARNING: You are currently running a version of TypeScript which ...

Issue with iOS 10: Changes to class not reflected in CSS/styles

Currently, I am utilizing Ionic with Angular to develop an application for Android and iOS. Surprisingly, everything functions smoothly on Android, but there seems to be an issue with iOS. I am employing a class change on an element using ng-class. I can ...

programmatically convert a solid link to a specific node into a dashed one using d3

Graph Type Radial Tidy Tree Current Output Initially, I receive a JSON response from the server and use recursion to flatten the JSON. I then utilize d3.tree to visualize the graph displayed below. The Legislation node is designed so that upon double-cl ...

What's the trick to inserting a "dot" beneath a DatePicker?

Could someone assist me in adding a small "dot" below certain dates on MUIX DatePicker, similar to the example shown here? Thank you. ...

How can the value of a number in Angular be changed without altering its original value?

Imagine having the initial number 100. If I enter 50 in another input, it should add 50 to 100. However, if I then change the value from 50 to 80, the total should be 180 and not 230. The goal is always to add numbers to the original sum, not the new valu ...

Navigating an object in TypeScript: the right approach

Curious if there might be a bug in TypeScript? Just seeking clarification on whether my code is incorrect or if there is an actual issue with the language. interface Something { key1: string; key2: number; key3: boolean; } const someObject: S ...

What is the reason behind TypeScript's decision not to raise an error in case of a mismatched function argument type?

Here's a simple illustration to showcase my point: type Info = { id: number; } type ImportantInfo = { id: number; value: 5; } type Task = (data: Info) => void; const task: Task = data => null; const data: ImportantInfo = { i ...

Phaser 3 game app on iOS generated with Capacitor lacks audio functionality

I have developed a basic test app using Phaser 3 (written in Typescript and transpiled with rollup) and am utilizing Capacitor to convert it into an iOS application on my Mac. This excerpt highlights the key functionality of the app: function preload () { ...

Adjust the color of the icon depending on the value

I am new to Angular2 and I'm looking for a way to dynamically change the CSS of an icon based on specific values. Here is the code in HTML: <li><i class="fa fa-check" [style.color]="'switch(types)'"></i>{{ types }}</l ...

Tips for choosing and unchoosing rows in angular 6

I am looking to extract the values from selected rows and store them in an array. However, I also need to remove a row from the result array when it is deselected. The issue with my current code is that every time I click on a row, the fileName values are ...

The (functionName) does not exist within the subclass as a valid function

I am currently developing an extension for a web-based text editor. However, I am facing some unexpected results due to the class hierarchy in my code. Despite attempting to relocate the "validate" function to the base class, I have not been successful in ...

The updating of the page in Angular 4.4 is not reflecting the changes made to the model

After recently adding a new angular component to an existing codebase, I'm struggling to update the view with changes in the model due to my unfamiliarity with angular 4. Despite looking at similar questions, none of the solutions seem to work for me. ...

The data structure '{ one: string; two: string; three: string; }' cannot be directly assigned to a 'ReactNode'

Here is the Array of Items I need to utilize const prices = [ { name: "Standard", price: "149EGP", features: [ { one: "Add 2500 Orders Monthly", two: "Add Unlimited Products And Categories", three: "Add 20 other ...

Tips on resolving handlebars 'module not found' error in typescript while compiling to umd

In my client-side JavaScript library written in TypeScript, I am attempting to incorporate Handlebars. However, when I try to import using import * as Handlebars from 'handlebars', I encounter an error message stating that TypeScript "cannot find ...

Having trouble getting my try catch block to run in React. What could be the issue?

I have a registration modal with 3 steps. Step 1: Fill out the information, Step 2: Get Activation Code, and Step 3: Success Message. When the user fills in the inputs and clicks the submit button, if there are no errors, they should move to the next ste ...

The argument of type 'NextRouter' cannot be assigned to the parameter of type 'Props' in this scenario

In my component, I am initializing a Formik form by calling a function and passing the next/router object. This is how it looks: export default function Reset() { const router = useRouter(); const formik = useFormik(RecoverForm(router)); return ( ...

Experiencing Difficulty Generating a Signed Request in JavaScript for AWS in Order to Access AWS Managed Prometheus

tag, I am currently in the process of developing a lambda function that will be responsible for querying and dynamically setting up AWS managed Prometheus alarms. Unfortunately, I have encountered an error when attempting to make a signed http request. Th ...

Encountering an issue while running the ng build --prod command in Angular

I ran into an issue while trying to execute the command ng build --prod in Angular. I've completed my small project and now need to generate the necessary files for uploading to my hosting provider. ERROR - ANGULAR CLI C:\Users\Johan Cor ...