Repository navigation
Language services for keyof types #11997
Description
Activity
Is that even necessary? Do we really care about the type of
key?Say I want to Rename
I.xtot.
The wanted output is:interface I { t: number; y: number } const key: keyof I = "t";
To achieve this, you don't need to know what
keyis.
You need to know that the literal"x"is in fact the property nameI.x.
Because you check during compilation that"x"is actually defined inI, it seems to me that you do have that information available at some point.Find refs is maybe less well defined?
Do you intend to findi[key]when looking forI.x?
Given thatkeyisconstyou could but that's an edge case, which won't work with more general code likelet key: keyof I = Math.random() > 0.5 ? "x" : "y".If you could find the literal
"x"that would already be very helpful. And by the same argument as above, if you can rename it you can find it.Aside: that gives us an alternate strategy to find
keyif you really wanted to: find the literals, then recursively findkeyusage if that literal is assigned to aconst.A scenario I would really want to light up when finding refs is my initial post:
// Your usual pluck def. function pluck<T, K extends keyof T>(xs: T[], prop: K): T[K][]; class Thing { name: string; } let x = pluck(things, "name"); // Find all Thing.name ^
I think this should be doable because during compilation you have to validate that the literal
"name"exists onThing.
So at one point the information is there.I see!
The way the compiler works is more obvious to me in an example where the assignment is delayed.let a: keyof I; // Immediately expanded to a: "x" | "y" let b: typeof a | "both"; // b: "x" | "y" | "both"; b = "x"; // ok because "x" is in the union type, no direct link with keyof I.
I have an implementation idea: what if you keep the type system as is but add an annotation on types literals at expansion time to indicate where they're from.
That annotation doesn't change the semantics of the type: it's still a literal string type. The annotation is an extra piece of information attached, for language services.
When you validate that the literal value"x"belongs to the literal type"x", you can propagate the annotation and implement language services.let a: keyof I; // a: "x" [keyof I] | "y" [keyof I] let b: typeof a | "both"; // b: "x" [keyof I] | "y" [keyof I] | "both"; b = "x"; // literal "x" matches literal type "x" annotated with keyof I
Tricky cases are then stuff like:
interface Vec2 { x: number; y: number } interface Vec3 { x: number; y: number; z: number } type T = keyof Vec2 | keyof Vec3; // T = "z" [keyof Vec3] | "y" [??] | "x" [??]
When merging the unions, it's unclear to me what you should do with the annotations: drop them? combine them?
More than a technical issue, it's a spec problem. When renaming
Vec2(x,y)toVec2(u,v)should you rename axthat matchesT?
I think there is no good answer as renaming will breakVec3and not renaming will breakVec2.My opinion is that you should drop the annotation (if they differ) when combining and not care. The common use case works and there's no perfect solution for edge cases. At least the compiler will catch errors during compilation.
Language services for
keyofare important, not having them greatly reduces the value ofkeyof.
It was presented like an alternative tonameofand in many common cases likepluck, it is. 👏
But if you look at #1579, several people still ask fornameofjust because languages services are missing. Not findingpluck(xs, "x")when looking for usages ofxis bad.Reacted by Frederico GalvãoDoes not seem that there is much we can do at the time being. this is rather a design limitation with the current system.
- addedDesign LimitationConstraints of the existing architecture prevent this from being fixedConstraints of the existing architecture prevent this from being fixed
on May 12, 2017 Just saying: this is a hard hit into the
keyofexperience, especially when considering its use in places wherenameofcould have been used. Several people in thenameofthread were content withkeyof, should these language services later come.At least, I feel like you should tag this "Revisit" or something and keep it open until you find a design that can support it. Closing because it's hard to do now doesn't feel good/right.
This is dependent on the implementation of the compiler. if we did change the representation of keyof to be a different kind of types (instead of a union), then possibly we would be able to revisit this.
Mohamed Hegazy (@mhegazy) I don't have nearly the same knowledge of TS internals as you, but a type different from a union looks like a lot of extra complexity (to work with existing union, intersection, narrowing, etc.)
What about the design I suggested in my comment just above?
Expand into a union of strings literal types like today, but use tagged string literals?
Shouldn't that be enough to later track keyof source at use site?but use tagged string literals?
We rely heavily on the identity of the literal types to be able to narrow unions efficiently. that means a literal type
"foo"can only have one representation. tagging means you have multiple versions of"foo", and identity checking would not work.We do some form of tagging, by associating aliases with unions for display purposes. we could look into doing something similar for keyof, but that is not guaranteed to work all the time. so not sure it would be a general solution .
I was suspecting that you'd intern the types so that there is only a single
"foo".But at least it's reasonably doable... By using a typical equivalence class design you can implement efficient identity checks.
Add a
canonicalproperty to types, whose value is themselves or the untagged type.
Instead of comparing type referencest1 === t2(or using that asMapkeys, etc.), you can now compare their canonical referencet1.canonical === t2.canonical(resp.Mapkeys).There's a trade-off between adding the property to all types (slightly more memory) and a slightly more complex type check using
t1.canonical || t1instead.The overall impact on speed should be neglectable.
if you want to experiment with this idea, i would be happy to help. i would start by
createLiteralTypeand then all references ofcontainsandcontainsTypecalled on a literal type will need to change. alsoisTypeRelatedTowould need an update as appropriate.I have far too many projects at the moment to take on another one, unfortunately.
If I ever get some bandwidth and this hasn't happened by then, I would gladly have a try.
I think this feature is very desirable.I agree this is disappointing, but it looks like it's a hard problem that I hope gets addressed someday! (fingers crossed)
The lack of reference finding and refactoring support is such a major limitation that I can't see why anybody uses string literal types for multiple direct assignments in anything but trivial code (unless, of course, they don't need to worry about maintaining it themselves lol). The intellisense certainly looks cool in demos, though! ;-)
However, I find the extra magic that advanced index/mapped/etc types add to string literal type usage makes them quite handly for indirectly flowing constraints through definitions in a way that supports references and refactoring in addition to intellisense (especially when combined with things like generic dynamic class generation where string literal types can be maintained). However, it's more more verbose than it would need to be if the language service could somehow figure it all out directly from context and do the right thing.
Anyway, it's amazing that we're even able to do any of this stuff at all on top of javascript right now, and I eagerly look foward to each new release :-)
- locked and limited conversation to collaborators
on Jun 19, 2018
As asked by Mohamed Hegazy (@mhegazy) in #11929 this issue is for tracking language services for the new
keyoftype.I hope this will surface in the language services like Find references and Rename?