'How to handle the type of incoming JSON I do not know the content of?

I have the following code ...

let services = []
fetch('http://localhost:11492/api')
.then(r => r.json())
.then(r => {
  for (const service in r.services) {
    r.services.service = service
    services.push(r.services[service])
  }
})

... that is flagged by ESLint with ...

ESLint: Unsafe member access .services on an `any` value. (@typescript-eslint/no-unsafe-member-access)

... in several places, starting with r in the ...

for (const service in r.services)

... line.

The problem is that I do not know what the exact content of the JSON content will be. I only know that it will have a structure such as

{
  "services": {
    "something": {
      // anything can happen here
    },
    "somethingElse": {
    // anything can happen here, different from the above
    },

(...)
}

but the content of the entries is not defined.

My question: how can I express the JSON structure above as a corresponding type?

I thought that I could cast this (first) error as

for (const service in r.services as Record<string, any>)

but it does not help because the issue is with the type of r itself.



Solution 1:[1]

Parsing JSON is hard for typescript because no compile time code can guarantee the data structure of that JSON. So you should strongly type what you can, and leave the rest as unknown.

In this case, I believe that would look like:

interface MyJson {
  services: Record<string, unknown>
}

let services: unknown[] = []

fetch('http://localhost:11492/api')
.then(r => r.json()) // r.json() returns Promise<any>
.then((r: MyJson) => { // assume any is your known JSON structure
  for (const service in r.services) {
    r.services.service = service
    services.push(r.services[service])
  }
})

Note how services is typed as unknown[], because you state that the data structure beyond is literally not knowable at compile time. So this type is the only type that makes sense.

unknown is not a type that you can do much with with any type safety, because Typescript can't enforce anything about its usage. Maybe that's fine because you're just passing through to something else, like writing to a file.

But my hunch is that there is a structure to that data, it might just be complex. It may involve unions of multiple values, or string template types, but there probably is a way to write that type if you study the documentation. And then you would edit the MyJson type to match that type.

You could also write predicates to test unknown values for specific things and safely cast those to known types.

There's a lot of ways to handle this, though. And the right solution depends on what the data structure is, how trusted is the format of the data to be consistent, how important is it that the usage be type safe in your program, and many more factors.

Solution 2:[2]

Then set the type of r:

.then((r: { services: Record<string, any> }) => {
  for (const service in r.services) {
    r.services.service = service
    services.push(r.services[service])
  }
})

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Alex Wayne
Solution 2 skara9