Reading nested dictionaries in PHP / Hack

632 views Asked by At

I'm trying to have a simple nested dictionary, and then read a nested value

$response = dict[
  'some_other_key' => 'asdf',
  'sub_response' => dict['success' => false],
];

if ($response['sub_response']['success']){
     // do stuff
}

I am so confused by this error:

Typing[4324] Invalid index type for this array [1]
-> Expected int [2]
-> This can only be indexed with integers [3]
-> But got string [2]

      40 | 
      41 |     $response = dict[
[3]   42 |       'some_other_key' => 'asdf',
      43 |       'sub_response' => dict['success' => false],
      44 |     ];
      45 | 
[1,2] 46 |     if ($response['sub_response']['success']) {
      47 |       return $response;
      48 |     }

1 error found.

It seems like it's reading the wrong key and complaining that it's a string? What am I doing wrong?

1

There are 1 answers

1
Josh Watzman On

In Hack, a dictionary has the same type for every key and every value; for non-uniform use-cases a shape is probably more appropriate. In other words, dictionaries are good for things like mapping a bunch of user IDs to their corresponding user object (dict<int, User>) -- it's a uniform set of mappings, but you don't know ahead of time how many or what exactly the keys will be. A shape is better for cases like this, where you know ahead of time what keys you have (some_other_key, sub_response) and so the typechecker can track a type for each individual key.

The documentation mentions this, though it's pretty buried and unclear IMO:

If you want different keys to have different value types, or if you want a fixed set of keys, consider using a shape instead.

So what's happening here is that the typechecker is trying to infer a type for $response. They keys are string, sure, but it's indeed getting confused about the values. Sometimes you use it as a dict<string, string> and sometimes as a dict<string, dict<string, bool>> -- which is not allowed.

Even though this code is indeed in error, the message is extremely confusing (to the point it might be worth filing a bug). I think you are right that the typechecker is thinking that $response['sub_response'] must be a string and so ['success'] is invalid? But that's weird that it wouldn't infer a type of dict<string, mixed> for $response -- which is a valid type for $response, though still not what you wanted, but it would probably give a better error message.

In any event, what you seem to want here is a shape, where the type of each individual key is tracked separately. This does what you want, I think:

$response = shape(
  'some_other_key' => 'asdf',
  'sub_response' => dict['success' => false],
);

if ($response['sub_response']['success']){
  // do stuff
}

(You may also want sub_response to be a shape, depending on how you ultimately use it.)