0

I am trying to consume an API using javascript.

An endpoint can give me an object with unique keys each time, for example:

{
  "-MKlw6VSTSf-FPBaTxfB": {
    "created_at": 1603934385.9833121,
    "jugadores": 0,
    "posiciones": 4
  },
  "-MKlxam1Zjtz14wZgMNp": {
    "created_at": 1603934776.2540152,
    "jugadores": 0,
    "posiciones": 4
  },
  "-MKm8JvbKJmMAumJbmoU": {
    "created_at": 1603937848.809657,
    "jugadores": 0,
    "posiciones": 4
  },
  "-ML3-HtshKPcKrME5Jk6": {
    "created_at": 1604237470.857504,
    "jugadores": 0,
    "posiciones": 4
  }
}

Or it can give me an error like this one:

{
  "error": true,
  "mensaje": "Hubo un error"
}

I have declared this types:


type APIError = {
  error: boolean
  mensaje: string
}

type ListadoJuegosPublicos = {
  [key:string]: {
    jugadores: number
    posiciones: number
    created_at: number
  }
}

And I have this function signature to retrieve the data:

async juegosPublicos ():Promise<APIError|ListadoJuegosPublicos>

The function works as expected. But when I try to iterate over the response, I get this error:

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'APIError | ListadoJuegosPublicos'.
  No index signature with a parameter of type 'string' was found on type 'APIError | ListadoJuegosPublicos'.ts(7053)

This is the code I am trying to use to iterate over objects

const juegos = await api.juegosPublicos()
if (juegos.error && typeof juegos.mensaje === 'string') {
  console.error(mensaje)
  return
}

Object.keys(juegos).forEach((juegoId:string) => {
  console.log(juegos[juegoId]) // <- this is the line than generates the error
})

How can I correctly iterate over this object in typescript

2 Answers 2

2

To start: I don't see why you're checking juegos.error for truthiness. If juegos is an APIError then this value is either true or false; if it is false then you shouldn't be checking for truthiness since you'd erroneously conclude that it is not an APIError, which it is. If juegos is a ListadoJuegosPublicos then this value is either undefined or an object; if it is an object, then you shouldn't be checking for truthiness since you'd erroneously conclude that it is an APIError, which it is not.

In what follows, I will only preserve the typeof juegos.mensaje === 'string' check, since this should be sufficient to determine if something is or is not an APIError.


Apparently, even though you understand that typeof juegos.mensaje !== 'string' implies that juegos is a ListadoJuegosPublicos, the compiler does not understand this, and does not perform control flow analysis-based narrowing on juegos. That is, the check on juegos.mensaje does not act as a type guard on juegos.

It looks like maybe ListadoJuegosPublicos having an index signature is the issue, and if so, perhaps microsoft/TypeScript#17960 is the relevant open GitHub issue, listed as a bug. If so, it looks like it's not going to be addressed anytime soon.

In any case, when the compiler does not recognize some code as a type guard, you have the option of extracting that code out to your own user-defined type guard function. For example:

function isAPIError(x: APIError | ListadoJuegosPublicos): x is APIError {
  return (typeof x.mensaje === 'string');
}

This is the same check as before, but explicitly marked as a type guard function that takes a value named x and returns a boolean value that determines whether or not the compiler treats x as an APIError. Now we call the type guard function in your original code:

  if (isAPIError(juegos)) {
    console.error(juegos.mensaje)
    return
  }

  Object.keys(juegos).forEach((juegoId: string) => {
    console.log(juegos[juegoId]) // okay now
  });

and it works. After the return statement, the compiler now recognizes that juegos must be a ListadoJuegosPublicos and allows the string index iteration.

Playground link to code

Sign up to request clarification or add additional context in comments.

Comments

0

Your code works fine. Actual issue is with below line

async juegosPublicos ():Promise<APIError|ListadoJuegosPublicos>

One thing you can do is replace type with any:

async juegosPublicos ():Promise<any>

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.