Repository navigation
Redesign ResolvedType to be a generic interface that does not implement Type #111
Description
@cowtowncoder I'm in the middle of creating an abstract database interface that involves transparent storage of objects (initially Java record) with full type conversions. For a corner of the conversion I'm using Jackson ObjectMapper, although I'm not sure about what I will use long-term. In any case I had a chance to dig into my old PLOOP code (described in #37) and how it relates to ResolvedType (see e.g. our discussion in #69). There are several tensions lurking in those discussions, as well as the proposal I made in #76 that GenericType<T> should not really be a java.lang.reflect.Type.
For this current work I just now had a long and in-depth conversation with one of the top LLMs, refreshing my memory about these issues and understanding what it would look like to bring PLOOP up to date and carry it forward using ClassMate. The benefit here is that in our discussion we started with actual, practical, working code that we had just implemented in this database framework. We looked at how our new implementation could be generalized by bringing in PLOOP+ClassMate. We came upon some important insights, which I believe form a synthesis of the tensions I was feeling in those other tickets, especially in light of the new PR #103 which basically tries to reverse a ResolvedType back into a Type. Below is a summary (produced by the LLM) of those insights. I leave a few additional comments at the end.
ClassMate ResolvedType Design Insights
Background
This builds on discussions from #37 (PLOOP properties layer, 2017), #69 (ResolvedType → JavaType conversion), and the draft PR #103 (ResolvedTypeMapper). These issues circle around the same fundamental design tension in ClassMate's type abstraction.
The Core Problem
ResolvedType currently implements java.lang.reflect.Type, which appears to offer seamless interoperability. However, Type has subtype interfaces (ParameterizedType, GenericArrayType, TypeVariable, WildcardType) that code routinely inspects:
Type type = ...;
if (type instanceof ParameterizedType pt) {
Type[] args = pt.getActualTypeArguments();
// process generic arguments
}ResolvedType doesn't implement these subtypes. Code that receives a ResolvedType through a Type reference and checks instanceof ParameterizedType gets false—silently losing all generic information. This affects Jackson's TypeFactory, Hibernate Validator, and any API that inspects Type structure.
PR #103 addresses this by creating a ResolvedTypeMapper that synthesizes proper ParameterizedType and GenericArrayType implementations from ResolvedType data. This works, but it's an external workaround for what should be built-in behavior.
ResolvedType as a Type Replacement
The insight from working with these APIs is that ResolvedType should be understood as a replacement for Type in application code, not an implementation of it.
Within an application, you'd use ResolvedType everywhere—passing it through property descriptors, converters, serializers. You'd only touch Type at boundaries:
- Input: Converting from
Field.getGenericType(),RecordComponent.getGenericType(), etc. - Output: Converting to
Typewhen calling external APIs (Jackson, Hibernate, etc.)
This positions ResolvedType as a richer abstraction that shields application code from Type's design limitations while remaining interoperable.
Why ResolvedType Should Be Generic
Java's reflection types aren't generic—ParameterizedType.getRawType() returns Type, not Type<T>. This forces runtime casts and loses compile-time safety:
Type listType = field.getGenericType(); // List<String>
Class<?> raw = getRawType(listType);
Object result = raw.cast(someValue); // no compile-time safetyA generic ResolvedType<T> would preserve type safety through the chain:
ResolvedType<List<String>> listType = resolver.resolve(field);
Class<List<String>> raw = listType.erasedClass();
List<String> result = raw.cast(someValue); // type-safeThis matters for frameworks passing type information through multiple layers.
Why ResolvedType Should Be an Interface
ClassMate's value is resolving type variables through inheritance hierarchies. But not all contexts need that resolution:
record User(String id, List<String> tags) {}For records, RecordComponent.getGenericType() returns fully-specified types. ClassMate resolution adds overhead without benefit.
If ResolvedType were an interface, there could be multiple implementations:
- Simple wrapper: Stores the original
Typedirectly;toType()returns it unchanged - Resolution result: ClassMate-backed;
toType()synthesizes aTypefrom resolved data
Both satisfy the same interface. Consumers don't know or care which implementation they receive.
The toType() Bridge
Rather than implementing Type (which fails instanceof checks), ResolvedType should provide an explicit toType() method that returns a proper Type hierarchy:
- For simple wrappers: return the stored original
Type - For resolution results: synthesize
ParameterizedTypeImpl,GenericArrayTypeImpl, etc. (as PR Draft: Issue #69 | Add ResolvedTypeMapper to convert ResolvedType into Type #103 does)
The synthesized types implement the correct interfaces, so instanceof ParameterizedType works as expected. External APIs receive types they can inspect normally.
This is essentially what PR #103 provides externally. Building it into ResolvedType would make it a first-class capability.
Proposed Design Sketch
public interface ResolvedType<T> {
Class<T> erasedClass();
List<ResolvedType<?>> typeArguments();
Optional<ResolvedType<?>> typeArgument(int index);
boolean isParameterized();
boolean isArray();
Optional<ResolvedType<?>> arrayComponentType();
Type toType(); // original or synthesized
// Factory methods
static <T> ResolvedType<T> of(Class<T> clazz) { ... }
static ResolvedType<?> of(Type type) { ... } // simple wrapper, stores original
static ResolvedType<?> resolve(Type type, Class<?> context) { ... } // full resolution
}Key design points:
- Interface, not abstract class—allows implementation flexibility
- Generic
<T>parameter—preserves compile-time type safety - Does not implement
java.lang.reflect.Type—avoidsinstanceoffailures toType()method—explicit bridge for external API interoperability- Multiple factory methods—simple wrapping vs. full resolution
Summary
ResolvedType currently occupies an awkward middle ground: implementing Type suggests compatibility, but the subtype contracts aren't satisfied. PR #103 demonstrates that users need proper Type representations for external APIs, but the current design forces external workarounds.
The proposed redesign clarifies ResolvedType's role: a richer, type-safe replacement for Type within application code, with an explicit toType() bridge for interoperability. Making it an interface enables both lightweight wrappers (preserving original types) and full resolution results (synthesizing types), unified under a common abstraction.
This raises lots of questions which we can discuss in the comments. e.g. Is ResolvedType the best name for this new, redesigned thing? Are you interested in going in this direction for a new major ClassMate version?
For the short term in my framework I will create this general, improved ResolvedType interface (probably with a different name), with factories that produce it. The early iterations may have ClassMate and the current ResolvedType serving as backing implementations, depending on how much this helps go forward quickly. Longer term I would hope that this interface on our end could be replaced with a redesigned ClassMate ResolvedType, whatever its name; and/or integrated with PLOOP.
I would also be interested in discussing the possibility of working with you for a new ClassMate that might even have a separate property description facility mentioned in #37 and possibly even used eventually in a new version of Jackson. (With LLMs such large-scale thinking and refactoring suddenly becomes much more feasible, even producing better results than if humans were to painstakingly pore over the code—as long as innovative, design-oriented humans direct and guide the LLMs.)
Activity
cowtowncoder commentedon Dec 23, 2025
Quick notes...
First of all, most of changes would require major version bump, or fork (which "proper" Java library major version bump often needs to be anyway, see Jackson).
With that, there are sort of 2 tracks:
toJDKType()conversions?) to target for 1.xa. Practical (new coordinates? new repo? (i.e. fork)) and
b. Functionality/design
On 2 b, my initial gut reaction is
ResolvedTypeas a Type Replacement (not instance). Yes, indeed.ResolvedTypean interface... maybe, not feeling strongly here. Example wrtRecordtho makes zero sense to me; Records require resolution same as POJOs (forList<String>fields etc)ResolvedTypegeneric: no, I don't think so. LLM may be misguided here; trying to use generic for use like this is more likely an anti-pattern in actual use -- code ends up fighting against generic typing. Esp. if trying to parameterize alongjava.lang.reflect.TypeOn 1, we should probably consider fate of #69 / #103.
Idea of something like
toJDKType()forResolvedTypesounds like reasonable path wrt 1.x, but maybe I forget something.cowtowncoder commentedon Dec 23, 2025
Oh and on whether to go with Big Redesign... I don't know. Right now my bandwidth is somewhat limited, to be honest.
garretwilson commentedon Dec 23, 2025
Thanks for reading, @cowtowncoder . My objective was just to gather all my thoughts on this topic over the years, together with some deeper insights that I gained while using an LLM to help me remember the context of all this. Even if you do not decide to go in a large-scale refactoring, I thought you might be interested in this synthesis and proposal.
To clarify, this insight was based upon a revisiting of ClassMate in relation to an actual implementation I'm working on, together with a discussion using an LLM as a soundboard. Although te LLM produced the summary of my thoughts, this is not a proposal the LLM dreamed up on its own.
In trying to remember why ClassMate is useful in the context of the application I'm working on, I confess that I did rely on the LLM's assertion that ClassMate resolution is not needed for
record. When I presented it with your reaction, it qualified what it had stated:Is that what you meant? And is the LLM's assertion that "ClassMate is needed for record component types if and only if the record itself is generic" correct? If not, let me know. I haven't dug deep into your ClassMate resolution code, so if there are other instances other than a generic
recordin which resolution is needed, I want to know!Nevertheless if we agree that most records (I don't know; I'm just guessing) aren't generic (and there are doubtless other cases in which the
ResolvedType"factory" could tell that resolution were not needed), I still see value of the case in which a lightweightResolvedTypeinterface implementation would simply wrap around the originalTypewould be probably more efficient than carrying around all thatResolvedTypecarries around now, and without the need to reconstruct theTypein the newtoType()method.We can't blame this one on the LLM. 😄 This was something I had already added to PLOOP, and I mentioned that two years ago on #69:
This generic PLOOP
TypeInfo<T>I referred to is something I wrote back in 2017:One thing I'm noticing now though (it was a long time ago) is that my type
Tseems to be in terms of the erased type. But I think that was still valuable in the actual usage. I'll have to see once I get deeper back into the code.You can see that all these ideas (a generic type
Tindicating the original type, the ability to produce/reconstruct the originalType, etc.) were things we had been discussing here and there in these tickets. The LLM conversation and summary just provided me an opportunity to unify my thoughts.I'm going to go ahead and implement something like a
ResolvedType<T>(with a different name) in my database abstraction library and see how it goes. I'll design it with the goal of eventually bringing it back into PLOOP or a similar library. I'll let you know how it turns out. If you later want to collaborate on some Big Redesign of ClassMate, let me know.Happy Holidays!
cowtowncoder commentedon Dec 23, 2025
Absolutely!
On Records: LLM answer seems to assume ClassMate is only needed for resolving Type Variables, or for main type itself; or if Record itself is generic. None of this is needed:
Recordwith fieldList<String>is much easier to handle with ClassMate (or similar abstraction) thanjava.lang.reflect.Type. So I really do not see much distinction b/w POJOs and Records.Or maybe we just disagree in usefulness/value of
java.lang.reflect.Typetype hierarchy? To me it's a pain to use, best to do without (except for erasedClass<?>which is necessary) so no code ever should useTypefor anything -- all access should be usingResolvedType.However, that leads to question of why ClassMate has no
ResolvedRecordType-- and it's something that probably ought to be added, in v2 (since v1 is Java 8-based, still).But I am more puzzled by
TypeInfo<T>-- just class parameterization ofClass<T>itself I see limited (... or no) value for it. But particularly forResolvedType. This based on usage by Jackson which perhaps differs from your usage of PLOOP?But basically, I've rarely (or never?) had a case where binding of
Twas available or useful for this case.It is always something to cast away, not use. When resolving POJOs, types being resolved are mostly for properties (Fields, Setter parameter, Getter result value), unrelated to containing type.
So perhaps this is distinction between the "root type", outer-most type, where maybe type of thing being passed (
Class<T>,TypeReference<T>) could be useful. And then secondary types (of Members) less so.Then again, to see pros and cons, it'd be necessary to try it out, and apply consistently.
On possible ClassMate 2.x, it may seem silly, but for me big part really is knowing how to handle versioning.
My gut feeling is that Jackson-style "re-package" is actually way to go, given that most ClassMate usage is (I think) via other libraries, frameworks, not direct use by applications. Which basically complicates compatibility aspects (due to need for binary compatibility, constraints (or not) of having deps to both 1.x and 2.x).
But unlike Jackson, would probably not change
com.fasterxml.classmateintotools.classmatebut maybe rathercom.fasterxml.classmate2(more due to not needing to buy domain for Maven publishing etc).garretwilson commentedon Dec 24, 2025
I think we may be talking past either other here. In further conversation with the LLM, it's starting to seem to me that there are three aspects to
ResolvedType:TypeVariable<D>withParameterizedType(and of course support of otherTypeimplementations). The LLM really brought it home to me that this is the core "resolution" ofResolvedType. (If that's not correct, let me know.)Typehierarchy.Type—not because we enjoy working withType, but because it's needed for interoperability with other APIs. (This is PR Draft: Issue #69 | Add ResolvedTypeMapper to convert ResolvedType into Type #103 .)So yes we would like to work with some reimagined
ResolvedTypeeverywhere instead of thejava.lang.reflect.Typehierarchy. My point is merely that ifResolvedTypewere an interface, in cases in which all there are noTypeVariable<D>and the originalTypewas already fully resolved, the factory would simply provide a lightweight wrapper around the originalType. Yes an application would work withResolvedTypeinstead ofType, but internally this implementation wouldn't have to carry all the baggage thatResolvedTypedoes now. For example your currentResolvedType.getTypeParameters()would delegate directly to …💡(light bulb moment)
Oh, I see.
ResolvedType.getTypeParameters()returnsList<ResolvedType>. You're saying that you see a value inResolvedTypeexposing the entire hierarchy as a parallel tree ofResolvedType, not just a full resolution of theTypetree by replacing allTypeVariable<D>withParameterizedType. So even this lightweightTypewrapper (implementing aResolvedTypeinterface) would still returnList<Type>if it simply delegated to e.g.ParameterizedType.getActualTypeArguments().But wait, would a caller always want to walk the entire
ResolvedTypetree? If theTypeis already resolved, then when it gets turned into aResolvedTypeis an implementation detail. So for aTypetree that was already resolved (which is quite often I would assume!), the lightweight implementation ofResolvedType.getTypeParameters()could still returnList<ResolvedType>—it could simply in turn wrapParameterizedType.getActualTypeArguments()inResolvedType.ofAlreadyResolvedType(Type)for example. It could do this each time (like Optional.of()) or lazily cache them. Something likeStream.of(ParameterizedType.getActualTypeArguments()).map(ResolvedType::ofAlreadyResolvedType).toList()comes to mind.Actually I see that the LLM was even ahead of me on this point! Literally minutes before you responded above, it had mapped out what this interface would look like (calling it
TypeInfo<>), and here is what it showed for getting type arguments:So yes, we are all considering a new
ResolvedTypeto be a full replacement forTypewith a better interface, and for the whole tree (the LLM "understood" that part before I did).In conclusion I think I finally see why you were emphasizing that
ResolvedTypewould always be useful to fully replaceTypefor the full hierarchy; that was something I overlooked before. And that's a good point, but I don't think it negates the benefit of havingResolvedTypebe an interface, and in some ways actually supports it!Thanks for pointing out that subtlety. I'm literally right now in the middle of implementing this re-envisioned
ResolvedTypeas an interface, so I'll see how it turns out.My proposal was a no-holds-barred brainstorming look at what would be possible with a redesign, so if you ever go in that direction, I would expect it to be a brand new ClassMate major version in the very least.
cowtowncoder commentedon Dec 24, 2025
Ah. This does explain why it did feel like we were talking past each other!
And just to make sure: I am not really against
ResolvedTypebeinginterface-- esp. when there's clear need.Also 100% agree with JDK
Typeconvertibility being valuable for inter-operability.Slightly skeptical of feasibility of sort of hybrid/lazy resolution, mostly due to likely complexity of making things work on-demand, but that's also not something to immediately rule out/rule in.
garretwilson commentedon Dec 24, 2025
Literally seconds before your last response, my LLM put out the first version (after days of discussing and planning) of this re-envisioned
ResolvedType. I'm getting ready to take a break for the day, and I haven't fully reviewed everything (especially the part about wildcards), but it's looking beautiful and I want to show it to you to illustrate the elegance of what this ticket proposes, in particular the interface aspect.Here an extract from
TypeInfo<T>(which would be the newResolvedTypeI'm proposing):(Edit: Actually this approach is flawed, but the concept is sound. I've edited out some of the information in this comment below, keeping some for the record but striking it out because it wasn't quite correct. I'll provide an update in my follow-up comment.)
If we're just getting
TypeInfo<>for a raw class, it returnsnew SimpleTypeInfo<>((Class<Object>) clazz).…
Where it gets interesting is in a type likeList<String>. It returnsnew ParameterizedTypeInfo<>(parameterized), the constructor of which just wraps lightweight wrappers around each generic type: … In a non-genericized record (which is going to be 100% of the data we support right now, and I imagine the most frequent thing you'll see in real life—as far asrecordgoes anyway), the recursiveTypeInfo::ofsimply winds up returningSimpleTypeInfoinstances for the generic types! So we get a fully tree ofTypeInforeplacement forType, that works for probably the majority of data in the wild, without even touching ClassMate resolution!!But what happens if we have arecord Wrapper<T>(List<T> items) {}and we try this with theList<T> itemswhere we do need ClassMate resolution? (This is I think the objection you raised above the incorrect blanket statement that arecordnever needs its field types resolved.) The firstcreateTypeInfo()level returns aParameterizedTypeInfoas before, but when wrapping the parameterized type arguments the second level hits theTypeVariableand insidecreateTypeInfo()throwsUnsupportedOperationException, as you see above. And that's all we need for now.The long-term solution is that ClassMate would adopt this overall approach, and inside
createTypeInfo()it would use itsTypeResolverlogic to resolve the tree just as we are doing, resolving and wrapping the types inTypeInfo.But for the medium-term, when I need generic records or full generic object support, I'll simply implement a
ResolvedTypeInfothat implementsTypeInfoby wrapping the current ClassMateResolvedType, providing lightweight wrappers around its hierarchy ofResolvedTypeinstances. It won't be quite as efficient or lightweight as the long-term solution; that's why it's a medium term solution. But for the short term I don't even need genericizedrecordsupport in the first place, so I don't even have to touch ClassMate at all! The beauty of all this is that this generalized interface allows me to plug in type resolution via ClassMate or even something else later and everything will just magically continue working behind theTypeInfo<>abstraction layer, without the caller knowing or caring.garretwilson commentedon Dec 27, 2025
The previous approach I outlined for
TypeInfo<>with an integration point for ClassMate was simplistic and naive. I let the LLM do handwaving, but fortunately later I pressed it as to what that ClassMate integrate would entail, and it led to several days of deep research and design. Eventually I just went ahead and integrated ClassMate so I could be sure it could be done. My head is ready to explode and I came away with an even deeper appreciation for what @cowtowncoder has done with ClassMate. I now understand ClassMate probably three times better than I ever have—even more than I had back in 2017 when first working on PLOOP.Let's back up and talk about what ClassMate really is. (This is my current understanding; correct me where I'm wrong.)
ClassMate "resolves" types. Here "resolved" essentially means _substituting every
TypeVariable<>with something else. The "someting else" could be from:TypeVariable<>Thus ClassMate is only useful when resolving
Typeis being resolved in relation to some concrete class that has type bindings.(That was where the simplistic approach above went wrong; there is no way to "patch in" ClassMate support for a single
Typeresolution; that's where things likeMemberResolvercome in.)So what value is a ClassMate
ResolvedTypethen? From my current work, it provides three benefits:ResolvedTypewill never contain aTypeVarable<>.Typehierarchy. (This is another reasonResolvedTypeshould not implementType.)With all this in mind, my
TypeInfoframework (which represents a reimaginedResolvedTypeas I describe above) is complete. Its factory methods assume the underlying type is already "resolved". (Actually I think I'm going to refine this to automatically useTypeVariable<>andWildcardTypebounds, likeTypeResolverdoes.)There is a
SimpleTypeInfowhich simply wraps the raw class, as you see above. We can return the originalType(theClass) directly. This is one of the benefits ofTypeInfo(the reimaginedResolvedType)being an interface. Notice in particular that we detect if the type is a raw type, in which case we cheat and wrap around ClassMate'sResolvedTypebecause the actual generic types may have a hierarchy. (This is where we discovered issue #53, still waiting to be released.) Again I think I will change the theTypeInfo.of()factories to simply useTypeVariableandWildcardTypebounds.But it is the "resolution in the context of a subject type" part which is interesting, and the part I was missing before. I have abstracted that out into a
TypeInfoResolver, and it is used like this:And here is where it gets fun and efficient. I have one
IDENTITYTypeInfoResolverthat simply usesTypeInfo.of()if no resolution is needed. I have anotherTypeBindingsTypeInfoResolver, which looks up type bindings from a single ClassMateTypeBindings. But remember that ClassMateMemberResolvermay have to navigate an entire hierarchy of type bindings if the subject type has inherited types. But in some cases we know that is not the case. In fact in some cases we know there is no resolution needed at all!So here is the
recordfield converter code, in the new introspector framework that will eventually go back and be part of the updated PLOOP or a similar library:In other words, if we're dealing with records, and the record is not generic, there is no resolution needed at all because there can be nothing to resolve, so we use
TypeInfoResolver.IDENTITYas the resolver, which just delegates toTypeInfo.of(). If the record is generic, we still don't have to useMemberResolver, because there can be no hierarchy of type bindings to navigate—there is only theTypBindingsof therecord! So we use a very simpleTypeBindingsTypeInfoResolverthat uses ClassMate either to resolve the type (usingTypeResolver) to get its type bindings, or simply uses the existingResolvedTypeif theTypInfofor the subject type already wraps aResolvedType.Thus in the case of a general non-generic
record(which is probably 95% of what we will be storing in the database), theTypeInfoinstances we use are simple wrapper classes that never touch ClassMate. Even for generic records, we delegate to ClasssMate in the background but then just do a single-levelTypeBindingslookup—no need for the wholeMemberResolverfacility because we knew ahead of time it would make no difference.The takeaway here is that this illustrates the benefit of having
TypeInfo(the reimaginedResolvedType) be an interface, as it can simply return lightweight wrapper classes for things that don't need resolving in the first place.cowtowncoder commentedon Dec 28, 2025
One quick comment/confirmation on this:
100% spot on. I learned contextual nature (Methods and Fields may depend on type variables of their declaring class and indirectly parent types from thereof -- and specifically type variables of base types' member only resolvable once sub-type's bindings are available) during Jackson development, but could only clean up the resolution in ClassMate
Oh, and another note: Fix for #53 is now released as ClassMate 1.7.2.
garretwilson commentedon Dec 28, 2025
Thanks for the confirmation! As you know it's difficult to get a holistic sense of "what it all means" unless 1) there is a an immediate application to see how it works in practice, and 2) lots of poring over the nitty gritty. In my case, I'm working on a framework (the database abstraction) that uses this immediately, and the LLMs help answer questions by poring over many source files within seconds—something that would have taken me much longer before.
I have now implemented
TypeInfo<T>in my framework (with the intention of extracting it back out to the PLOOP library I started back in 2017), and it is now working as a proof of concept of this reconceptualization ofResolvedTypeI laid out in this ticket. However there were three nagging questions:TypeInfo.of(Type)substituteTypeVariable<>s, or should it throw an exception if the type is not already "resolved"? (ClassMateTypeResolver.resolve()does the former.) The worry is that a user might inadvertently wrap aTypewithTypeInfo, yet the type variables may not be fully "resolved" against some subject class. (Interestingly it turns out that in practice this hardly matters; in a framework, either a raw class is passed (usually derived from the write path of a generic class, e.g.new ArrayList<>()will result in the consumer receivingArrayList.class) or a token type such asGenericType(usually on the read path, e.g. the user is saying they want aFoo<Bar>type). It seems rare we actually encounter just a bareTypefloating around.)NumberBox.classexpose its declared types such asNumberfor declaredNumberBox<T extends Number>? In issue Allow for subtype resolution with unknown generics #53, ClassMate made the decision of "yes". But in further analysis, I wondered whether this even means the same thing. It turns on what the purpose ofTypeInfo/ResolvedTypeis (see below).toType()method return the originalType(e.g. containingTypeVariable<>s), or return a fully "resolved"Typeequivalent? This is what we discussed in issue Convert GenericType or ResolvedType to JavaType #69, and at the time I'm not sure I even fully appreciated the distinction. PR Draft: Issue #69 | Add ResolvedTypeMapper to convert ResolvedType into Type #103 produces a fully-resolvedTypeequivalent of theResolvedType, but that is partly out of necessity, becauseResolvedTypediscards the originalType. In a reconceivedTypeInfo/ResolvedType, the originally type could be saved and returned, e.g. for further resolution against a subject type. But is that useful? Why do we need atoType()to begin with?With these quandaries in mind, over the last 24 hours I've refined my conceptualization of
TypeInfoin a way that I think addresses these concerns. Let's think ofTypeInfo/ResolvedTypeas simply "usable type info". This doesn't imply it is fully resolved (i.e. in relation to a generic subject type) or where the type information came from. It just means you can use it without running into type variables. This is in line with how we use it—we pass other methods aTypeInfofor a property type, for example, and those methods use the type information to convert some value in a database back what what the user wants for example. It's up to a user to create theTypeInfothey want. In this view,TypeInfo.of()would simply provide the best type information available, whether it involves using upper bounds ofTypeVariable<>, or doing something similar withWildcardTypes.In this view, sending back the declared type variables for a raw
NumberBox.class(NumberifNumberBoxis declared asNumberBox<T extends Number>) is the correct thing to do, becauseTypeInfois meant to be usable type info, and the method receiving it will want to know as much as possible about what the type arguments might be (or could be).If
toType()is the bridge back to the JDK world, e.g. to supply to other APIs that don't understantTypeInfo, that means theTypeproduced should also represent theTypeversion of theTypeInfo—not the originalTypeunless the originalTypealready had allTypeVariable<>s substituted. But doesn't this meanTypeInfo.of()might loose value type information, such as what the type variables were, for further resolution against some subject class? It's starting to seem that there should be a separate method such assourceType()that indicates theTypefrom which theTypeInfowas created. (For aTypeInfobacked by ClassMateResolvedTypewe may not know the original type so in the short term we would have to use thetoType()form, but long term `TypeInfo may not need ClassMate, or a future version of ClassMate may provide us that information.)Perhaps the conceptual hurdle was from originally thinking that
TypeInfo/ResolvedTypeimplied "resolved" in the sense of "fully resolved against some other subject type". In this refined thinking,ResolvedTypemeansTypeVariableSubstitutedTypewithout implying resolution against a subject type (although you want to do that of course to get the full picture). IfTypeInfoseen as a more pragmatic "here is type information you can actually use", and it's up to the caller to make sure theTypeInfois resolved in the best possible way, this refinement may provide a consistent, useful, and semantically appropriate API.