Psalm: Add support for values-of/keys-of enum meta types

Created on 20 May 2018  路  10Comments  路  Source: vimeo/psalm

class A {
  const B = [
    1 => "January",
    2 => "February",
    3 => "March",
    4 => "April",
    5 => "May",
    6 => "June",
    7 => "July",
    8 => "August",
    9 => "September",
    10 => "October",
    11 => "November",
    12 => "December",
  ];
}

/** @psalm-param keys-of(A::B) $i */
function getStringMonth(int $i) : void {}

/** @psalm-param values-of(A::B) $s */
function getIntMonth(string $s) : void {}

getStringMonth(5);
getIntMonth("January");

getStringMonth(13); // fails
getIntMonth("Mortuary"); // also fails
enhancement

Most helpful comment

Maybe this syntax (roughly translated from TypeScript) might work:

/**
 * @template T as array
 * @template K as key-of<T>
 * @param T $o
 * @param K $name
 * @return T[K]
 */
function getOffset(array $o, $name) {
  return $o[$name];
}

All 10 comments

will this work for template tags & __get() / __set() / __isset() / __unset() etc. ?

Nice. Can I suggest keyof/valueof instead of keysof/valuesof? 5 is _a key_ of the array, singular. E.g. TypeScript uses _keyof_.

might also be handy to have optionalof, i.e. to transform array{x:float, y:float, z:float} to array{x?:float, y?:float, z?:float}

Can I suggest keyof/valueof instead of keysof/valuesof

Yeah, but it needs a hyphen (so key-of, value-of) to avoid being mistaken for an object type

@SignpostMarv

will this work for template tags & __get() / __set() / __isset() / __unset() etc. ?

Yeah, this will work

/**
 * @template DATA as array<string, scalar|array|object|null>
 */
abstract class DataBag {
    /**
     * @var DATA
     */
    protected $data;

    /**
     * @param DATA $data
     */
    public function __construct(array $data) {
        $this->data = $data;
    }

    /**
     * @param key-of<DATA> $property
     */
    public function __get(string $property) {
        return $this->data[$property] ?? null;
    }

    /**
     * @param key-of<DATA> $property
     */
    public function __set(string $property, $value) {
        $this->data[$property] = $value;
    }
}

BUT I don't know how you'd be able to specify the output of __get in such a way as to parameterise it on possible input values.

maybe @return value-of<DATA, $property>, though that looks _weird_

maybe @return value-of<DATA, $property>, though that looks _weird_

@return DATA[$property] ?

Will this enforce __set($property, $value) to require specific $value, i.e.

/**
* @template-extends ParamBag<array{page:int, section:string}>
*/
class PagedContentRequest extends ParamBag {
}

$foo = new PagedContentRequest(['page' => 1, 'section' => 'a']);

$foo->page = '2'; // not an int
$foo->section = 2; // not a string

I'm also wondering how feasible/sensible it'd be to do this:

/**
* @template DATA as array<string, scalar|array|object>
*
* @template-extends ParamBag<DATA>
*/
abstract class PossiblyBadIdea extends ParamBag {
    /**
    * @param key-of<DATA> $property
    */
    public function __isset(string $property) : bool {
        return true; // because value-of can never be null
    }
}

Maybe this syntax (roughly translated from TypeScript) might work:

/**
 * @template T as array
 * @template K as key-of<T>
 * @param T $o
 * @param K $name
 * @return T[K]
 */
function getOffset(array $o, $name) {
  return $o[$name];
}

There are still a few bugs, but progress: https://psalm.dev/r/e7ea73d8f6

Am I correct in thinking that where T is array{a: int, b: string}, T|array<empty, empty> would resolve to array{a?: int, b?: string} ?

Not sure atm...

Was this page helpful?
0 / 5 - 0 ratings

Related issues

vudaltsov picture vudaltsov  路  3Comments

Ocramius picture Ocramius  路  3Comments

roukmoute picture roukmoute  路  3Comments

Pierstoval picture Pierstoval  路  3Comments

albe picture albe  路  3Comments