镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Language services for keyof types #11997

Description

@jods4

As asked by Mohamed Hegazy (@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");

Activity

  1. jods4 commented on May 5, 2017

    @jods4
    Author

    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.

  2. jods4 commented on May 6, 2017

    @jods4
    Author

    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.

  3. mhegazy commented on May 12, 2017

    @mhegazy
    Contributor

    Does not seem that there is much we can do at the time being. this is rather a design limitation with the current system.

  4. jods4 commented on May 12, 2017

    @jods4
    Author

    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.

  5. mhegazy commented on May 12, 2017

    @mhegazy
    Contributor

    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.

  6. jods4 commented on May 12, 2017

    @jods4
    Author

    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?

  7. mhegazy commented on May 12, 2017

    @mhegazy
    Contributor

    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 .

  8. jods4 commented on May 12, 2017

    @jods4
    Author

    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.

  9. mhegazy commented on May 12, 2017

    @mhegazy
    Contributor

    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.

  10. jods4 commented on May 12, 2017

    @jods4
    Author

    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.

  11. rob3c commented on Aug 25, 2017

    @rob3c

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

  12. locked and limited conversation to collaborators on Jun 19, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugA bug in TypeScriptDesign LimitationConstraints of the existing architecture prevent this from being fixed

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions