Skip to content

Mapped types enumerating keys in string behave poorly #22509

Closed

Description

TypeScript Version: 2.8.0-dev20180308
Search Terms: mapped types
Code

type Thing = {
    [K in string]: K
};

var thing: Thing = new Proxy({}, {
    get(_, k) {
        return k;
    }
});

let res = thing.x; // res should be of type 'x', but is string

Playground Link

Here I expect string to behave as if it were the union of all possible string literal types. The present semantics are identical to a string indexer. IMO if we don't want map types K in string to do anything interesting, then the syntax should be an error and we should require people to write the indexer.

For what it's worth, ultimately I would love to type Allen's Apparatus for Method Extraction, roughly typed below:

type ExtractorThing = {
    [K in string]: <T>(base: T) => K extends keyof T ? T[K] : never;
}

var extract: ExtractorThing = new Proxy({}, {
    get(target, prop) {
        "use strict";
        return (base: any) => function(this: any, ...args: any[]) {
          return base[prop].apply(this || base, args); 
        }
    }
});

let obj = {
    x: 0,
    succ() {
        return ++this.x;
    },
    dec() {
        return --this.x;
    }
}

let succ = extract.succ(obj);
let dec = extract.dec(obj);

succ(); // 1
succ(); // 2
dec(); // 1
dec(); // 0

I would probably work towards putting a constraint on T such that you get a red squiggle if you pass an object without the proper method to extract, but hopefully this serves as a useful example.

Activity

  1. RyanCavanaugh commented on Mar 13, 2018

    @RyanCavanaugh
    Member
  2. changed the title [-]Map types enumerating keys in `string` behave poorly[/-] [+]Mapped types enumerating keys in `string` behave poorly[/+] on Mar 13, 2018
  3. jack-williams commented on Mar 13, 2018

    @jack-williams
    Collaborator

    That would be very cool! I guess it would be kind of like specifying a parameterised getter? (If that were a thing).

    interface Thing {
      get <K extends string>(): <T>(base: T) => K extends keyof T ? T[K] : never;
    }
  4. DanielRosenwasser commented on Mar 13, 2018

    @DanielRosenwasser
    Member

    Jack Williams (@jack-williams) Sort of. It might mean that string index signatures would need to boil down to something similar to the following call signatures

    <K extends string>(x: K): *Type*;
    <K extends string>(x: K, value: *Type*): void;

    But then what about numeric index signatures? You'd need a way to encode number-like strings:

    <K extends number | (numeric string)>(x: K): *Type*;
    <K extends number | (numeric string)>(x: K, value: *Type*): void;
  5. jack-williams commented on Mar 13, 2018

    @jack-williams
    Collaborator

    Daniel Rosenwasser (@DanielRosenwasser) Yes I guess you would want these to be equivalent:

    EDIT: The syntax I've picked is completely misleading as it just defines a method called get. The syntax should probably be something different like get [K: string] (): T.

    interface Idx<T> {
        [x: string]: T;
    }
    
    interface GetterSetter<T> {
      get <K extends string>(): T
      set <K extends string>(v: T): void;
    }

    I'm afraid my understanding of numeric indexers probably has many holes. Am I correct in thinking that the stickiness with number-like strings mostly arises when you have named properties? E.g.

    interface NumGetSet<T> {
      get <K extends number>(): T
      set <K extends number>(v: T): void;
      "10": number; // How do we detect that this is inconsistent? 
    }

    Although, I don't know if you want want to replace numeric indexers necessarily. Perhaps it could just be limited to types that extend string?

  6. ahejlsberg commented on Mar 14, 2018

    @ahejlsberg
    Member

    This really is a request to add compiler support for ECMAScript proxy objects, i.e. compiler knowledge of the relationship between property access operations and the get and set proxy methods. It is simply not possible to model that with our current type system features (because nothing allows you to capture the literal type corresponding to the property name in a property access). This new feature would likely also include support for mapping call operations onto the apply and construct methods.

    I do think this is entirely orthogonal to mapped types, and I think the current behavior of mapping over type string is correct and consistent.

  7. bterlson commented on Mar 14, 2018

    @bterlson
    MemberAuthor

    Anders Hejlsberg (@ahejlsberg) I think proxies are a bit of a red herring here. For example, something like the following is fairly common I think?

    {
      [K in string]?: K extends '_data' ? DataType : DefaultType
    }

    It's like a type with an index signature but special properties don't have to be assignable to the index type.

    I agree this is somewhat useful for the get trap side of proxies. I believe exotic get behavior is more common than exotic call behavior so this may even be a reasonable starting point for "proxy support".

    I think I contest consistent, but I admit this is based on a surface-level understanding. I'll try to illustrate. Given the type { [K in 'a' | 'b']: K }, it has three important behaviors:

    1. A property is created for each member of in's RHS.
    2. The type of that property corresponds to the property name
    3. All properties are are required

    However, { [K in string]: K }, a very similar construct, seems to only exhibit the first behavior but not the second or third.

  8. added
    DeclinedThe issue was declined as something which matches the TypeScript vision
    and removed on Jun 24, 2021
  9. RyanCavanaugh commented on Jun 24, 2021

    @RyanCavanaugh
    Member

    This doesn't seem to be a common request and I haven't seen other problems that would be solved by addressing this

  10. DanielRosenwasser commented on Oct 7, 2021

    @DanielRosenwasser
    Member

    Keywords to find this later: generic index signature type parameters on index signature proxy Brian Terlson (@bterlson)

  11. eggers commented on Aug 29, 2022

    @eggers

    I have a use case now where this would be a helpful feature:

    type Item<T extends string> = {
      id: T;
    };
    
    export type ItemMap = {
      [id in string]: Item<id>;
    };
    const map: ItemMap = {};
    
    const item = map['someid']; // type is Item<string> when it would be nice to be Item<'someid'>
  12. tlrobinson commented on Jan 29, 2023

    @tlrobinson

    I would like a solution to this as well. I'm specifically interested in typing a Proxy for use in a tRPC-like library that requires accurate types to share types in the server and client, but Jacob Eggers (@eggers) has another good use case, and I don't think something special for Proxy is necessary.

    Maybe this is a little contrived but you could assert an id field matches the key in the map:

    type IdMap<Keys extends string> = { [K in Keys]?: { id: K }}
    const map: IdMap<string> = {}
    
    map.foo = { id: "foo" }
    map.foo = { id: "bar" } // does NOT error as I believe it should

    If you're able to enumerate the possible keys (I'm not in my real use case) this does work as expected:

    const map: IdMap<"foo"|"bar"> = {}
    
    map.foo = { id: "foo" }
    map.foo = { id: "bar" } // DOES correctly error
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

    DeclinedThe issue was declined as something which matches the TypeScript visionSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions