-
Notifications
You must be signed in to change notification settings - Fork 671
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Pseudo-types for referencing class properties and methods? #3141
Comments
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 |
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. For structural shapes, a structural comparison could be done. 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
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 |
Do you plan to raise a PR? I would love to test and give you feedback about the new type-annotation |
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:
The text was updated successfully, but these errors were encountered: