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

Creating object literal with generic key type fails to compile #22060

Closed
Rogach opened this issue Feb 20, 2018 · 17 comments
Closed

Creating object literal with generic key type fails to compile #22060

Rogach opened this issue Feb 20, 2018 · 17 comments
Labels
Duplicate An existing issue was already created

Comments

@Rogach
Copy link

Rogach commented Feb 20, 2018

TypeScript Version: 2.8.0-dev.2018022 or 2.7.2

Search Terms: generic keyof object literal

Code

function foo<S>(state: S, key: keyof S) {
    let newState: { [P in keyof S]: S[P] } = {[key]: state[key]};
}

Expected behavior:
Expected to compile, since I just created a simple literal object.

Actual behavior:

test.ts(2,9): error TS2322: Type '{ [x: string]: S[keyof S]; }' is not assignable to type '{ [P in keyof S]: S[P]; }'.

Seems that type is removed from key and replaced with string. I'm aware that all object keys are strings, but how do I then create an instance of Pick<S, P>?

Possibly Related Issues: #22053

@DanielRosenwasser
Copy link
Member

@sandersn perhaps #21070 should solve this?

@DanielRosenwasser
Copy link
Member

@mhegazy is this a duplicate of #21030?

@sandersn
Copy link
Member

@DanielRosenwasser It might, but the intent of that change is to create a union when a computed property's type is a union, so I don't think it will do anything here.

@gcnew
Copy link
Contributor

gcnew commented Feb 21, 2018

I think the current behaviour is correct. { [P in keyof S]: S[P] } is basically just S and {[key]: state[key]} is definitely not assignable to it.

@Rogach
Copy link
Author

Rogach commented Feb 21, 2018

@gcnew - { [P in keyof S]: S[P] } is actually not S, but Pick<S,P>. From the source:

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
}

In my understanding, S != Pick<S,P>, and {[key]: state[key]} should be assignable to Pick<S,P>. The issue is that key type is inferred to be string instead of generic type of key variable.

@gcnew
Copy link
Contributor

gcnew commented Feb 21, 2018

{ [P in keyof S]: S[P] } = Pick<S, keyof S>

What you need is Pick<S, key> which is encoded as { [P in key]: S[P] }. I.e. you have to use [P in key] instead of [P in keyof S].

@Rogach
Copy link
Author

Rogach commented Feb 21, 2018

@gcnew - Yes, you are right about Pick<S, keyof S>. But the bug in question is not about that, but about creating a new instance of Pick<S, keyof S>. For example, the following compiles:

let _ = require("lodash");
function foo<S>(state: S, key: keyof S) {
    let p: Pick<S, keyof S> = _.pick(state, key);
    let p1: { [P in keyof S]: S[P] } = p;
}

But the following does not:

function foo<S>(state: S, key: keyof S) {
    let p: Pick<S, keyof S> = {[key]: state[key]};
    let p1: { [P in keyof S]: S[P] } = p;
}
// test.ts(2,9): error TS2322: Type '{ [x: string]: S[keyof S]; }' is not assignable to type 'Pick<S, keyof S>'.

Again, the question (the bug?) is about creating a new object using generically-typed keys.

@Rogach
Copy link
Author

Rogach commented Feb 21, 2018

Just in case, to verify that _.pick(state, key) and {[key]: state[key]} do equal things:

$ node
> let _ = require("lodash")
undefined
> let obj = { "a": 42, "b": 43 }
undefined
> let key = "a"
undefined
> _.pick(obj, key)
{ a: 42 }
> {[key]: obj[key]}
{ a: 42 }

@gcnew
Copy link
Contributor

gcnew commented Feb 21, 2018

You've stumbled on an issue, but you are interpreting it from the wrong end. The real problem is that let p: Pick<S, keyof S> = _.pick(state, key) should not be allowed, not the other way around. Consider:

function foo<S>(state: S, key: keyof S): S {
    let p: Pick<S, keyof S> = _.pick(state, key);
    return p;
}

foo({ a: 1, b: 2 }, 'a'); // the return type is `{ a: number, b: number }`
                          // but the actual value is just `{ a: 1 }`

The concept that's missing is that key: keyof S is just a single value, not the set of all values. There was a suggestion in the past to fix that, but I can't find it now.

@gcnew
Copy link
Contributor

gcnew commented Feb 21, 2018

The takeaway is the current lodash typings are not entirely safe. They can maybe be improved if features such as #5453 (#17884, #17898, #17961, #18007) get implemented.

@Rogach
Copy link
Author

Rogach commented Feb 21, 2018

@gcnew - Your example seems to imply that Pick<S, keyof S> is a subtype of S. And indeed that for some bizzare reason compiles:

let _ = require("lodash");
function foo<S>(state: S, key: keyof S) {
    let p: Pick<S, keyof S> = _.pick(state, key);
    let s: S = p;
}

But I completely fail to understand why - clearly {a:number} is not a subtype of {a:number, b:number} (one property is missing). And Pick is explicitly described as From T pick a set of properties K in the official statement.

So I'd say that the problem is that Pick<S, keyof S> is a subtype of S, or the docs are wrong, but not that lodash typings are incorrect (they are probably based on the mistake in the docs).

@Rogach
Copy link
Author

Rogach commented Feb 21, 2018

I see your point - indeed keyof S is a set of all keys:

let obj: { a: number, b: number } = { a: 42, b: 43 };
let p: Pick<{a: number, b: number}, keyof {a: number, b: number}> = { a: 42 };

test.ts(8,5): error TS2322: Type '{ a: number; }' is not assignable to type 'Pick<{ a: number; b: number; }, "a" | "b">'.
  Property 'b' is missing in type '{ a: number; }'.

@Rogach
Copy link
Author

Rogach commented Feb 21, 2018

So it seems that quite a lot of typings are affected - aforementioned lodash, React (I originally stumbled upon the issue when working with React.Component.setState method, that expects an object with one or more keys from the full state object), and a slew of other libraries.

Is there no way to express one or more keys from type S idea?

@Rogach
Copy link
Author

Rogach commented Feb 21, 2018

No, I'm confused again. The following compiles:

let _ = require("lodash");

interface State {
    a: number;
    b: string;
}

class Component<S> {
    setState<K extends keyof S>(newState: Pick<S, K>) {
    }
}

let component = new Component<State>();
component.setState({ a: 1 });

So K extends keyof S seems to express the required one or more keys from type S. But now I get back to original question in this issue - given variable with type K, that extends keyof S, how do I construct Pick<S, K> without resorting to manual casting?

Attempting to use object creation syntax fails, object key type is inferred to be string instead of K:

function setState2<S, K extends keyof S>(key: K, value: S[K]) {
    let component = new Component<S>();
    let newState = {[key]: value};
    component.setState<K>(newState);
}

test.ts(19,24): error TS2345: Argument of type '{ [x: string]: S[K]; }' is not assignable to parameter of type 'Pick<S, K>'.

@Rogach
Copy link
Author

Rogach commented Feb 21, 2018

So, again, I want to stress that the only issue here is that in {[key]: value} expression type of the key variable is completely ignored and replaced by string.

@mhegazy
Copy link
Contributor

mhegazy commented Feb 21, 2018

This is really a design limitation, please see #21030 for more context.

@mhegazy mhegazy added the Duplicate An existing issue was already created label Feb 21, 2018
@typescript-bot
Copy link
Collaborator

Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.

@microsoft microsoft locked and limited conversation to collaborators Jul 25, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

6 participants