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

Mapped types enumerating keys in string behave poorly #22509

Closed
bterlson opened this issue Mar 13, 2018 · 10 comments
Closed

Mapped types enumerating keys in string behave poorly #22509

bterlson opened this issue Mar 13, 2018 · 10 comments
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript

Comments

@bterlson
Copy link
Member

bterlson commented Mar 13, 2018

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.

@RyanCavanaugh
Copy link
Member

Ping @ahejlsberg

@DanielRosenwasser DanielRosenwasser added Suggestion An idea for TypeScript In Discussion Not yet reached consensus labels Mar 13, 2018
@DanielRosenwasser DanielRosenwasser changed the title Map types enumerating keys in string behave poorly Mapped types enumerating keys in string behave poorly Mar 13, 2018
@jack-williams
Copy link
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;
}

@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented Mar 13, 2018

@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;

@jack-williams
Copy link
Collaborator

jack-williams commented Mar 13, 2018

@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?

@ahejlsberg
Copy link
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.

@bterlson
Copy link
Member Author

bterlson commented Mar 14, 2018

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

@RyanCavanaugh
Copy link
Member

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

@DanielRosenwasser
Copy link
Member

Keywords to find this later: generic index signature type parameters on index signature proxy @bterlson

@eggers
Copy link

eggers commented Aug 29, 2022

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'>

@tlrobinson
Copy link

tlrobinson commented Jan 29, 2023

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 @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
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

7 participants