hacklang

Inconsistent error while storing a darray into a Shape


I have a shape like this

const type TFileInfo = shape(
        'displayName' => string,
        'givenName' => string,
        'jobTitle' => string,
        'businessPhones' => vec<string>
    );
    private Person::TFileInfo $person;

Now my constructor of the class looks like so

public function __construct(string $apiresponse) { // instance method
    $json = \json_decode($response, /* associative = */ true);
    TypeAssert\matches<self::TFileInfo>($json);
    $this->person = $json; //OFFENDING LINE
    $this->person['businessPhones1'] = "";
}

Now strangely the above code does not throw any error .

If I remove the offending line , then the last line throws a compile time error Expected nothing because the field 'businessPhones1' is not defined in this shape type, and this shape type does not allow unknown fields

What am I missing here ? Is there a better way to assign an API response to a typed variable ?


Solution

  • TypeAssert\matches doesn't prove that its argument is the type you specified, in contrast to the behavior of some other built-ins like is_null which are special-cased in the typechecker. Instead, it coerces the argument and returns it, so you need to move your standalone call to the assignment, i.e. $this->person = TypeAssert\matches<self::TFileInfo>($json);.

    You might have expected a type error from the $this->person = $json assignment then, but in fact json_decode and some other unsafe built-in PHP functions are special-cased by the typechecker to be bottom types (convertible to anything) so they could be usable at all before type-assert. It remains this way today: see its type definition in the HHVM source, probably for compatibility.


    One other interesting point about this case is that $this->person = $json coerces $this->person to a bottom type as well downstream of the binding. To my understanding, this is a specific behavior of the Hack typechecker to do this for a single level of property nesting, yet it preserves the types for properties of properties (the second example has_error):

    <?hh // strict
    class Box<T> { public function __construct(public T $v) {} }
    function no_error<T>(Box<int> $arg): T {
        $arg->v = json_decode('');
        return $arg->v;
    }
    function has_error<T>(Box<Box<int>> $arg): T {
        $arg->v->v = json_decode('');
        return $arg->v->v;
    }