Open Ocramius opened 4 years ago
This looks like something like Typescript's structural typing, so perhaps something could be borrowed from their approach.
I've added some bells & whistles above: I couldn't find anything about Typescript's reflection support though. Structural typing seems very similar to GO's interfaces?
Apologies, I used wrong terminology. What I meant was index types.
// ------------------- [Definitions] ---------------
class ReflectionProperty<T> {
prop: T;
constructor(prop: T) {
this.prop = prop
}
getValue(): T {
return this.prop
}
}
class ReflectionObject<T extends object> {
obj: T;
constructor(obj: T) {
this.obj = obj
}
getProperty<N extends keyof T>(prop: N): ReflectionProperty<T[N]> {
return new ReflectionProperty(this.obj[prop]);
}
}
function takesNumber(val: number): void { }
// -------------------- [Usage] -------------
class C {
prop: string = 'zxc';
func(): number {
return 123;
}
}
class D {
e: number = 123;
func(): string {
return "zxc";
}
}
// here typescript figures out getValue returns string, not int
takesNumber(new ReflectionObject(new C).getProperty('prop').getValue())
// this is fine
takesNumber(new ReflectionObject(new D).getProperty('e').getValue())
// works with functions too
const cFunc = new ReflectionObject(new C).getProperty('func').getValue()
takesNumber(cFunc())
// knows that dFunc returns string
const dFunc = new ReflectionObject(new D).getProperty('func').getValue()
takesNumber(dFunc())
I'm also interested in this. Another example is Symfony's EventSubscriberInterface
which maps events to instance methods on the same class:
https://github.com/symfony/event-dispatcher/blob/5.x/EventSubscriberInterface.php#L27-L48
Structural typing would also be a nice addition for DTO classes with public readonly properties:
class Human {
public function __construct(
public readonly string $name
){}
}
class Animal {
public function __construct(
public readonly string $name
){}
}
/** @psalm-assert shape<shapeof Human> shape<shapeof Animal> */
This syntax might look like a mix between hack's shape keyword and typescript's structural typing + keywords.
Since there is already an array shape, the shape won't be an array as it is in hack. The difference between a shape and an existing array shape would be the way how it is compared.
For structural shapes, a structural comparison could be done. For arrays the comparison would be stricter.
I've translated the examples above to what it could look like:
Given:
class WindowsServer {
public string $name = 'X';
public int $age = 10;
}
class LinuxServer {
public string $name = 'X';
public int $age = 10;
public string $kernel = '5.14';
public function execute(Foo $foo): Bar{}
}
interface Executor {
public function execute(Foo $foo): Bar{};
}
These additional structures could be allowed:
Direct usage with props:
/**
* @psalm-type Server = shape{name: string, age: int}
* @param Server $windowsServer
* @param Server $linuxServer
*/
Support for callables / methods:
/**
* @psalm-type Server = shape{execute: (callable(Foo): Bar)}
* @param Server $linuxServer
* @param Server $someOtherExecutorThatImplementsTheExecutorInterface
*/
Type operations (like typescript's Typeof or Keyof) that result in enumerations of object information:
/**
* @psalm-type Methods = methodsof LinuxServer - enum of all public method names
* @psalm-type Types = propsof LinuxServer - enum of all (public?) properties
* @psalm-type Shapeof = shapeof LinuxServer - enum of both methods and props
*/
Casting of objects to structural shapes or array shapes:
/**
* @psalm-type Server = shape<propsof WindowsServer> - (Public?) properties only
* @psalm-type Executor = shape<methodsof LinuxServer> - Public methods only
* @psalm-type arrayInfo = array<propsof LinuxServer> - Special functions also useable for arrays
* @psalm-type LinuxShape = shape<shapeof LinuxServer> - Full shape : props + methods
*/
The hydration example could look like this:
/**
* @template T of object
* @param array<propsof T> $input
* @param T $object
* @return T
*/
function hydrate(array $input, object $object): object {}
Note : this will require the array to contain ALL the properties of the object. If you want to limit the options, there will be a need for typescript-like utility types like
Partial<SomeShape>
Pick<SomeShape, 'prop1'|'prop2'>
Omit<SomeShape, 'prop1'|'prop2'>
The assertion example could look like this:
/**
* @template TObject of object
* @template TParameter of string
*
* @psalm-assert shape{TParameter: callable} TObject
*
* @param TParameter $method
* @param TObject $object
*/
function assertMethodExists(string $method, object $object) { /** ... */ }
The event subscriber could look like this:
/**
* @psalm-type MethodNames = methodsof static
*
* @psalm-type Simple = array<string, MethodNames>
* @psalm-type Prioritized = array<string, array{ MethodNames, int }>
* @psalm-type Many = array<array-key, Simple|Prioritized>
*
* @return array<string, Simple|Prioritized|Many>
*/
public static function getSubscribedEvents();
Not sure how doable the above list is. But it is an interesting analysis nevertheless!
I was able to make class-property type using properties available from class like storage, dunno if it's a good way, but I do not see why it should not be possible to implement. I just don't know if it's possible to make like @veewee proposed props of T
or methodsof T
syntax
Do you plan to raise a PR? I would love to test and give you feedback about the new type-annotation class-property
.
Yeah, I will try to make a PR when I finish a few other things with it. I am yet to figure out the internals of the psalm, I want to add some filters like visibility, type :)
edit: That autocomplete was just me trying out adding autocomplete to language server 😅
Just throwing out vague idea: there is no implementation nor clear concept, so please feel free to close, but I hope it will be useful to brainstorm this amongst type-junkies.
I was considering adding types for
ReflectionMethod
andReflectionProperty
, but it seems to be tricky to do that, due to the lack of a system to reference properties and classes at type-level.Specifically, parameter
$name
ofReflectionClass<class-string>#getProperty($name)
could be aclass-property<class-string, non-empty-string, PropertyType of mixe>
, which could be used for type resolution.Similarly, parameter
$name
ofReflectionClass<class-string>#getMethod($name)
could be aReflectionMethod<class-method<class-string, non-empty-string, list<TParameter of mixed>, MethodType of mixed>>
, which could be used for type resolution, andclass-method
could be acallable
when combined with aclass-string<T>
or anobject of T
that is compatible with thatclass-method<T, ...>
.This sort of API is interesting for things like hydrators:
Important in the above is that I still don't have an idea of how we could type the
mixed
Also for assertions: