typescript-rtti: Bug: Properties of function type return any
I have a class like this:
class A {
a: (arg1: string, arg2: boolean) => number;
}
I would like to get the arguments and the return type of such a function-type property. I couldn’t find anything, is there any way to do this?
Thank you!
About this issue
- Original URL
- State: closed
- Created 2 years ago
- Comments: 21 (12 by maintainers)
Commits related to this issue
- support for function type reflection (#67) — committed to typescript-rtti/typescript-rtti by rezonant 2 years ago
- support for function type reflection (#67) — committed to typescript-rtti/typescript-rtti by rezonant 2 years ago
- failing test for type node version of this feature (#67) — committed to typescript-rtti/typescript-rtti by rezonant 2 years ago
@Wyctus One of these days i will give it a shot
I think https://github.com/typescript-rtti/typescript-rtti/commit/c65f06c556af774c654d2500525a0436939e302b should do it.
https://app.circleci.com/pipelines/github/typescript-rtti/typescript-rtti/323/workflows/e2470485-dbe4-4f20-8f02-e64a58541c8a
@Wyctus Support has been added in typescript-rtti@0.7.0.
That would be a lot of work! Have you considered submitting PRs here instead? I could definitely use some help in moving things forward as we find corner cases and missing functionality.
Since you’ve expressed interest in this feature, I thought you might like to review what it took to add this support, so I did this one as a PR for easy review: https://github.com/typescript-rtti/typescript-rtti/pull/68
To help you or anyone else understand how this library works a bit better, below is an annotated guide through the implementation process. It’s not that tricky, I promise!
–
To get started, I used the Typescript AST Viewer to analyze how Typescript expressed this particular type: https://ts-ast-viewer.com/#code/JYWwDg9gTgLgBAbzlApgMwDYoMbwL5xpQQhwDkMAnmCgM7ZTBgwC0sMwZA3AFA-YYAhrVpwAgoh5xphCBDgBeOAAoAlIoB8iZChgBXKADs4MKHpRwA-HACMAJgDMcAFzk0c7nDw9v-CIdoILAA6DAgAc2VUTBwYZTFVYPDdAAViGlhKZTJ3CDJEqhpVLiA
I then wrote a quick test in metadata.test.ts with our test snippet and set it to
it.onlyso that I could easily debug into how typescript-rtti was currently handling this type. Support for debugging the test suite is built in to the repository if you are using VS Code.I then saw we had a bit of a deficiency when handling property types: It would only work for declared types (ie those that have a type annotation on them). I could see this as
typeNodewas undefined, which makes sense as that will only be present when there is a literal type annotation in the code.That was fairly easy to fix:
The MetadataEncoder class has a
typeNode()helper which takes a ts.TypeNode and callstype()passing in both the type returned by the type checker for the given node, and the node itself (this is useful for many reasons deeper into the code).From here, I was no longer getting
anyback, but instead the expectedFunction. However, this could be better, as obviously the desire is to reflect upon the function’s return type and parameters. So at this point I headed intoformat.tswhere all of the metadata format types live, so that I could add a new kind of RtRef (RtRefs are the objects which have ieTΦ: '~'indicators). I choseFto represent a function type ref, and constructed a new interface to represent this new kind of RtTypeHere the intent is for
rto represent return type,pto represent the set of parameters, and I addedfto hold a flags string, though there is so far no use of this (just there for future uses).To add support for this new RtType, I needed to modify the
typeLiteral()function.typeLiteral()is the core of metadata emitting, it is responsible for creating the Typescript AST nodes that represent a specific type (ignoring deduplication / indexing / lookups). A key thing to note is thattypeLiteral()is only supposed to construct an AST fragment for the particular type its being called for. If any other types are referenced by that type, it is to useencoder.referToType(type: ts.Type)to do so. This allows the particular type encoder (which can be the legacy emitDecoratorMetadata encoder or the actual RTTI encoder) to handle indexing the types so that a given type is only emitted once for a given source file. The encoder is always passed into typeLiteral():As you can see, the previous behavior here was just to return an identifier token of
Functionso that the RtType would be theFunctionconstructor. If you examineRtType, you’ll see that it is valid for an RtType to be a constructor function among other things, that is how simple primitive (non-literal) types and classes are represented.The expansion here simply gets the call signatures for the particular type, warns out if its more than 1, and then proceeds to use the first signature to produce the new RtFunctionType. The
serialize()method is a helper to take a literal POJO and turn it into a TS AST fragment that represents that POJO. However, the referToType() returns an AST node already, so theliteralNodehelper tellsserialize()to skip encoding that value and simply pass it back as if it was a ts.Node (as it is).The warning is there to flag to any users that we’re using only the first call signature, as there could be a scenario where that’s possible, but some quick testing using TS AST Viewer showed that using
typeof foowhere foo is a function with multiple signatures and making an interface with two call signatures yielded a symbol which did not have the Function symbol flag, so I think that warning will never get hit.A gotcha here is that
getTypeOfSymbolAtLocation()really wants a valid node to determine the correct type of a Symbol, but if thetypeNodeis undefined, how do we do it for the paremeters which is an array of Symbols? The type encoder comes to the rescue here by exposing the RTTI “context” asencoder.ctx. From there, we can access the current “top level statement”, which will be the statement which has the SourceFile as its parent which is currently being processed. This provides a suitable anchor for the type checker to determine the necessary ts.Type to return.–
The last part was to add support for this new RtType within the reflection library (
reflect.ts). The library uses decorators and some metaprogramming to avoid boilerplate. First,'function'needed to be added to theReflectedTypRefKindunion, and then an entry was added in the TYPE_REF_KIND_EXPANSION constant. That constant is used to map from the low-level RtType indicators (~,F,|, etc) to the high level “kind” names (any,function,union, etc).I then added a new ReflectedTypeRef subclass
ReflectedFunctionRefannotated with@ReflectedTypeRef.Kind('function'). The decorator tells the library that when a plainReflectedTypeRefis created which haskind === 'function'it should replace it with an instance ofReflectedFunctionRef. The underlyingRtTypecan be found in therefprotected field. From here, I just added cached getters for the return type and parameters, and implemented the matches() and matchesValue() methods so that the type and value matching functionality works for these new type references.In order to handle checking that a runtime function value matches the given ReflectedFunctionRef, I augmented the type of
matches()to includeReflectedFunction, and then acquired the ReflectedFunction for the passed function, and passed it in:I think that’s pretty nice 😄