You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I hope this will surface in the language services like Find references and Rename?
functionpluck<T,KextendskeyofT>(xs: T[],prop: K): T[K][];classThing{name: string;}// Is this found when looking for references to Thing.name?// Does renaming Thing.name to firstName update the string?letx=pluck(things,"name");
To achieve this, you don't need to know what key is.
You need to know that the literal "x" is in fact the property name I.x.
Because you check during compilation that "x" is actually defined in I, 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 find i[key] when looking for I.x?
Given that key is const you could but that's an edge case, which won't work with more general code like let 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 key if you really wanted to: find the literals, then recursively find key usage if that literal is assigned to a const.
A scenario I would really want to light up when finding refs is my initial post:
// Your usual pluck def.functionpluck<T,KextendskeyofT>(xs: T[],prop: K): T[K][];classThing{name: string;}letx=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 on Thing.
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.
leta: keyofI;// Immediately expanded to a: "x" | "y"letb: typeofa|"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.
leta: keyofI;// a: "x" [keyof I] | "y" [keyof I]letb: typeofa|"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:
interfaceVec2{x: number;y: number}interfaceVec3{x: number;y: number;z: number}typeT=keyofVec2|keyofVec3;// 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) to Vec2(u,v) should you rename a x that matches T?
I think there is no good answer as renaming will break Vec3 and not renaming will break Vec2.
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 keyof are important, not having them greatly reduces the value of keyof.
It was presented like an alternative to nameof and in many common cases like pluck, it is. 👏
But if you look at #1579, several people still ask for nameof just because languages services are missing. Not finding pluck(xs, "x") when looking for usages of x is bad.
Just saying: this is a hard hit into the keyof experience, especially when considering its use in places where nameof could have been used. Several people in the nameof thread were content with keyof, 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?
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 canonical property to types, whose value is themselves or the untagged type.
Instead of comparing type references t1 === t2 (or using that as Map keys, etc.), you can now compare their canonical reference t1.canonical === t2.canonical (resp. Map keys).
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 || t1 instead.
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 createLiteralType and then all references of contains and containsType called on a literal type will need to change. also isTypeRelatedTo would 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 :-)
Activity
jods4 commentedon May 5, 2017
Is that even necessary? Do we really care about the type of
key?Say I want to Rename
I.xtot.The wanted output is:
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 find
i[key]when looking forI.x?Given that
keyisconstyou 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.A scenario I would really want to light up when finding refs is my initial post:
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.
jods4 commentedon May 6, 2017
I see!
The way the compiler works is more obvious to me in an example where the assignment is delayed.
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.Tricky cases are then stuff like:
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 break
Vec3and 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 to
nameofand in many common cases likepluck, it is. 👏But if you look at #1579, several people still ask for
nameofjust because languages services are missing. Not findingpluck(xs, "x")when looking for usages ofxis bad.mhegazy commentedon May 12, 2017
Does not seem that there is much we can do at the time being. this is rather a design limitation with the current system.
jods4 commentedon 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.
mhegazy commentedon May 12, 2017
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.
jods4 commentedon May 12, 2017
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?
mhegazy commentedon May 12, 2017
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 .
jods4 commentedon May 12, 2017
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 references
t1 === 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.
mhegazy commentedon May 12, 2017
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.jods4 commentedon May 12, 2017
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.
rob3c commentedon Aug 25, 2017
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 :-)