Skip to content
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

Language services for keyof types #11997

Closed
jods4 opened this issue Nov 2, 2016 · 13 comments
Closed

Language services for keyof types #11997

jods4 opened this issue Nov 2, 2016 · 13 comments
Labels
Bug A bug in TypeScript Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@jods4
Copy link

jods4 commented Nov 2, 2016

As asked by @mhegazy in #11929 this issue is for tracking language services for the new keyof type.


I hope this will surface in the language services like Find references and Rename?

function pluck<T, K extends keyof T>(xs: T[], prop: K): T[K][];

class Thing { name: string; }

// Is this found when looking for references to Thing.name?
// Does renaming Thing.name to firstName update the string?
let x = pluck(things, "name");
@ghost
Copy link

ghost commented May 5, 2017

Had a discussion about this with @sandersn and the conclusion is that this wouldn't be easy.

For a simplified example:

interface I { x: number; y: number; }
const key: keyof I = "x";

The problem is that the type of key, queried from the checker, isn't keyof I, it's "x" | "y". The checker resolves keyof types to a string union immediately. From there, we can't go backward to get keyof I back.

This is sort of similar to #12687 in that the problem involves information being destroyed during type resolution.

A potential solution would be to have the literal types in keyof I be distinct from "x" and "y", but assignable to/from them. This has some relation to string enums (#15486). But adding a new type of type shouldn't be done lightly.

Related: #1579, because nameof I.x would work much better with services.

@jods4
Copy link
Author

jods4 commented May 5, 2017

Is that even necessary? Do we really care about the type of key?

Say I want to Rename I.x to t.
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 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.
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 on Thing.
So at one point the information is there.

@ghost
Copy link

ghost commented May 6, 2017

The type of key matters because it determines the contextual type of the literal "x". We would want to distinguish an "x" that is keyof I from any other "x" that happens to appear the program. In your example the equivalent would be getting the type of prop.

@jods4
Copy link
Author

jods4 commented May 6, 2017

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) 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.

@mhegazy
Copy link
Contributor

mhegazy commented 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.

@mhegazy mhegazy closed this as completed May 12, 2017
@mhegazy mhegazy added the Design Limitation Constraints of the existing architecture prevent this from being fixed label May 12, 2017
@jods4
Copy link
Author

jods4 commented May 12, 2017

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.

@mhegazy
Copy link
Contributor

mhegazy commented 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
Copy link
Author

jods4 commented May 12, 2017

@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
Copy link
Contributor

mhegazy commented May 12, 2017

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 .

@jods4
Copy link
Author

jods4 commented 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 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.

@mhegazy
Copy link
Contributor

mhegazy commented May 12, 2017

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.

@jods4
Copy link
Author

jods4 commented 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
Copy link

rob3c commented 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 :-)

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Bug A bug in TypeScript Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
Development

No branches or pull requests

3 participants