Typescript - Custom Object type that is always terminated by either a string or a number

Does TypeScript have a way to create a versatile object that can have multiple nested levels but always end with either a string or number property?

interface GenericObject {
  [key: string]: string | number | GenericObject | GenericObject[];
}

const object: GenericObject = {
  test: 'test',
  testNum: 2,
  test2: {
    test: 'test'
  },
  test3: [
    {
      test: 'test'
    }
  ]
}

While creating the object works fine, attempting to access its properties throws an error.

object.test2.test;
object.test3[0].test;

Property 'test' does not exist on type 'string | number | GenericObject | GenericObject[]'

Is there a way to make this structure work so it can be infinitely nested but always ending in either a string or a number?

Playground url - https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgOIRNYCDyAjAKwgTGQG8BYAKGWQG0BrCATwC5kBnMKUAcwF12XHiF7IAPshABXALZ5oEtBiy5CxUpPSYeaoiTr8A3NQC+1aggD2ILsivqS7bavz7SAXnLVakLuwByPzAAgBofZGCAOTl2ACZwmkiILjj2SiTfFLBA4ICI00SsrgBmdjoI2gzaGuT-ZCDs-MzkcyT+MwsqB3cAOmC4-uyTbscwIdK6AAZ+CbAjIA

Answer №1

For those seeking a combination of restriction and property inference, simply incorporate an additional function.

interface GenericObject {
  [key: string]: GenericObject | GenericObject[] | string | number
}

const validation = <Obj extends GenericObject>(obj: Obj) => obj

const result = validation({
  test: 'test',
  testNum: 2,
  test2: {
    test: 'test'
  },
  test3: [
    {
      test: [{ a: 's' }]
    }
  ]
})

result.test2.test // ok


const withError = validation({
  test: 'test',
  testNum: 2,
  test2: {
    test: 'test'
  },
  test3: [
    {
      test: [{ a: false }] // <--- expected error
    }
  ]
})

Playground

Answer №2

Although I am not an expert in TS, it seems like your definition is correct.

The issue lies in the fact that in TS, you can only access members that are present in all parts of a union type.

Therefore, the error you are encountering makes sense. There isn't enough overlap between the types number, string, and a certain GenericObject interface. TS cannot determine if the test2 variable is a primitive or an object with keys.

One possible solution to specify what test2 or test3 props are would be to remap the key using as:

(object.test2 as GenericObject).test;

Keep the object: GenericObject as it is. This is just my opinion. I'm curious to hear other suggestions on how to address your question about deeply nested objects.

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

Customize the initial color of the text field in Material UI

I am currently working with the mui TextField component and facing an issue. I am able to change the color when it is focused using the theme, but I cannot find a way to adjust its color (label and border) in its initial state when it's not focused. I ...

Trouble with updating data in Angular 8 table

In Angular 8, I have created a table using angular material and AWS Lambda as the backend. The table includes a multi-select dropdown where users can choose values and click on a "Generate" button to add a new row with a timestamp and selected values displ ...

The string returned from the Controller is not recognized as a valid JSON object

When attempting to retrieve a string from a JSON response, I encounter an error: SyntaxError: Unexpected token c in JSON at position In the controller, a GUID is returned as a string from the database: [HttpPost("TransactionOrderId/{id}")] public asyn ...

Can you explain the significance behind the syntax used in ngrx adapter selectors?

Stumbled upon this ngrx adapter example: export const { selectAll: selectAllItems } = adapter.getSelectors<State>(state => state.items); Confused about the assignment in this code snippet. Specifically, the notation involving a type: const ...

Can Ansible and Pulumi be integrated to work together effectively?

Is it possible to create multiple DigitalOcean droplets in a loop and then use Ansible to configure software and security measures on them, similar to how Terraform works? If so, what would the JavaScript/TypeScript code for this look like? I couldn' ...

Resolving the "Abstract type N must be an Object type at runtime" error in GraphQL Server Union Types

Given a unique GraphQL union return type: union GetUserProfileOrDatabaseInfo = UserProfile | DatabaseInfo meant to be returned by a specific resolver: type Query { getUserData: GetUserProfileOrDatabaseInfo! } I am encountering warnings and errors rel ...

Exploring the world of interfaces in nested mapping functions

Currently, I'm faced with the challenge of mapping an array inside another map with an array. These are the specific interfaces that I am working with: interface Props { columns: Array<{field: string, headerName: string, width: number}>; row ...

Lack of Typescript 2.1 and Angular 2 type definitions with browserify

` vscode 1.7 Typescript 2.1.1 Angular 2 latest package.json "dependencies": { "@angular/common": "^2.2.4", "@angular/compiler": "^2.2.4", "@angular/core": "^2.2.4", "@angular/platform-browser": "^2.2.4", "@angular/platform-browser-dyna ...

In Vue3, using the script setup feature allows for setting default property values within nested objects

I am developing a component that is designed to accept an object as a prop. In case the object is not provided, I aim to set a default value for it. <script setup lang="ts"> interface LocaleText { text: string; single?: boolean; coun ...

Showing Firebase messages in a NativeScript ListView in an asynchronous manner

I am currently struggling to display asynchronous messages in a ListView using data fetched from Firebase through the popular NativeScript Plugin. While I have successfully initialized, logged in, and received the messages, I'm facing challenges in ge ...

Limit Typescript decorator usage to functions that return either void or Promise<void>

I've been working on developing a decorator that is specifically designed for methods of type void or Promise<void>. class TestClass { // compiles successfully @Example() test() {} // should compile, but doesn't @Example() asyn ...

Obtaining the value of an ion-toggle in Ionic2 using the ionChange

Below is the toggle I am referring to: <ion-toggle (ionChange)="notify(value)"></ion-toggle> I am looking for a way to retrieve the value of the toggle when it is clicked in order to pass it as a parameter to the notify method. Any suggestion ...

In order to make Angular function properly, it is crucial that I include app.get("*", [...]); in my server.js file

Recently, I delved into server side JavaScript and embarked on my inaugural project using it. The project entails a command and control server for my own cloud server, operating with angular, Expressjs, and bootstrap. Presently, I am encountering challeng ...

Is there a more effective alternative to using the ternary condition operator for extended periods of time?

Do you know of a more efficient solution to handle a situation like this? <tr [style.background]="level == 'ALARM' ? 'violet' : level == 'ERROR' ? 'orange' : level == 'WARNING' ? 'yellow' ...

Various gulp origins and destinations

I am attempting to create the following directory structure -- src |__ app |__ x.ts |__ test |__ y.ts -- build |__ app |__ js |__ test |__ js My goal is to have my generated js files inside buil ...

Challenge: Visual Studio 2015 MVC6 and Angular 2 compilation issue - Promise name not found

Initially, I've made sure to review the following sources: Issue 7052 in Angular's GitHub Issue 4902 in Angular's GitHub Typescript: Cannot find 'Promise' using ECMAScript 6 How to utilize ES6 Promises with Typescript? Visual ...

Variable not accessible in a Typescript forEach loop

I am facing an issue with a foreach loop in my code. I have a new temp array created within the loop, followed by a nested foreach loop. However, when trying to access the temp array inside the nested loop, I encounter a "variable not available" error. le ...

I had high hopes that TypeScript's automatic type inference for constructor parameters would do the trick, but it seems to have let

You can experiment with the given code snippet at the online playground to confirm it. Consider this code: class Alpha { private beta; constructor(b: Beta) { this.beta = b; } doSomething() { ...

Securing Your Next.js Web App with Session Authentication

I have encountered a challenge while integrating NextAuth authentication into my React.js web application. To ensure seamless user authentication across the entire app, I am attempting to wrap a SessionProvider around the root component. However, VSCode ...

Eliminating every instance of the character `^` from a given string

I am encountering an issue with a particular string: "^My Name Is Robert.^". I am looking to remove the occurrences of ^ from this string. I attempted using the replace method as follows: replyText.replace(/^/g, ''); Unfortunately, thi ...