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

Using const string enum as object key produces an indexed type #16687

Closed
Jessidhia opened this issue Jun 22, 2017 · 13 comments
Closed

Using const string enum as object key produces an indexed type #16687

Jessidhia opened this issue Jun 22, 2017 · 13 comments
Assignees
Labels
Committed The team has roadmapped this issue Fixed A PR has been merged for this issue Suggestion An idea for TypeScript

Comments

@Jessidhia
Copy link

Jessidhia commented Jun 22, 2017

TypeScript Version: nightly (2.5.0-dev.20170621)

Code

const enum Test {
  A = 'a',
  B = 'b'
}

type TestMap = {[key in Test]: string}

// Type '{ [x: string]: string; }' is not assignable to type 'TestMap'.
// Property 'a' is missing in type '{ [x: string]: string; }'.'
const x: TestMap = {
  [Test.A]: 'string',
  [Test.B]: 'another string'
}

Expected behavior:

Keys from a string enum to be usable as static key names, similar to how constant strings are usable in computed key position. This probably should work even with non-const enums.

const x: TestMap = { // currently works
  ['a']: 'string',
  ['b']: 'another string'
}

Actual behavior:

Not usable without a (potentially incorrect?) type assertion.

@SLaks
Copy link

SLaks commented Jul 10, 2017

This should also work for numeric enums.

@simonbuchan
Copy link

Is this the same error?

function make<Name extends string, Value>(name: Name, value: Value): Record<Name, Value> {
    return {
        [name]: value,
    };
}

Gives:

[ts] Type '{ [x: string]: Value; }' is not assignable to type 'Record<Name, Value>'.
[ts] A computed property name must be of type 'string', 'number', 'symbol', or 'any'.

@sandersn
Copy link
Member

sandersn commented Aug 2, 2017

@simonbuchan Not really. @Kovensky wants the keys of strings enums to be statically known for use in computed properties.

You want one unknown member of an unknown set of keys to be create a type with computed name in it. Something like { [Name]: Value }, with [Name] to be assignable to other uses of T[Name] or something like that. (I'm haven't worked out in detail how it would have to work.)

Unfortunately, Typescript doesn't have a type like this, though, so instead the type inferred for the object literal has a string indexer, which is not assignable to a mapped type, even though both have an unknown set of allowed properties. You'll have to read the PRs for mapped types, but I think the reason is that a string indexer will always have an unknown set of properties, but the mapped type, when instantiated, will have a particular set of properties. For example:

make('hi', 'there'): Record<'hi', string>

And { [x: string]: Value } should not be assignable to that since it might not have the property 'hi'.

@simonbuchan
Copy link

@sandersn I think I get it reflecting on this a bit: Record<Key, Value> is { [key in Key]: Value }, not { [key: Key]: Value }, because the latter doesn't make sense when Key is not string (or any or never) or a string literal type (and this issue is that string enums should be considered the latter?). Is it correct that the only other type Key could be for Key extends string is a union of the previous? Would it make sense if TS had something like literal string as a type?

function make<Name extends literal string, Value>(name: Name, value: Value): { [name: Name]: Value };
make("foo", "bar"); // { foo: string } - though I would like { foo: "bar" }, that's another issue
["foo", "bar"].map(name => make(name, name)); // either error on make "'string' is not assignable to 'literal string' or Array<{ [name: string]: string }> as before?

function zipMake<Names extends string, Values>(names: Names[], values, Values[]) {
  return names.map((name, i) => make(name, values[i])); // "'Names' is not assignable to 'literal string'"
}
function zipMake<Names extends literal string, Values>(names: Names[], values, Values[]); // Either error or can only be called with 0 or 1-length literal array of `names` (or matching constrained type)?

This arose when I was playing around with a toy version of recompose, the use case is essentially that withState("counter", "setCounter", 0) should return a function that takes wraps a react component and adds the counter and setCounter props`: when simplified, something like:

type WithCounterEnhancer = <ExternalProps>(
  wrappedComponent: React.ComponentType<ExternalProps & { counter: number, setCounter(value: number }>,
): React.ComponentType<ExternalProps>;

I could get it to typecheck pretty well externally with ExternalProps & Record<Name, Value> & Record<UpdaterName, (value: Value) => void>, but I see now it would break in weird ways with type unions.

@sandersn
Copy link
Member

sandersn commented Aug 3, 2017

@simonbuchan Do you mind creating a separate issue for this with your example code and discussion? This feels like it is generating discussion distinct from that based on the original example. I'll see if I can answer your questions there.

@simonbuchan
Copy link

@sandersn Seems like it's #2049 in this specific case. I've been thinking about the general case and working on a proper proposal - turns out languages are hard :). Further discussion would be in #2049 or a proposal.

@mhegazy
Copy link
Contributor

mhegazy commented Aug 22, 2017

another interesting use case described in #17784

class Component<S> {
    state: S;
    setState(partialState: Partial<S>) { }
    onFiledChanged = (filedName: keyof S) => (value) => {
        this.setState({ [filedName]: value });
    }
    render() {
        return <form>
                <input onChange={ this.onFiledChanged('foo') } />
                <input onChange={ this.onFiledChanged('bar') } />
            </form>
    }
}

@sandersn
Copy link
Member

#15473 should be to fix this.

@mhegazy mhegazy added Committed The team has roadmapped this issue and removed In Discussion Not yet reached consensus labels Aug 30, 2017
@mhegazy mhegazy added this to the TypeScript 2.6 milestone Aug 30, 2017
@sandersn sandersn added this to In Progress in Rolling Work Tracking Aug 30, 2017
@sandersn sandersn removed this from In Progress in Rolling Work Tracking Sep 7, 2017
@mhegazy mhegazy added the Fixed A PR has been merged for this issue label Sep 8, 2017
@mhegazy
Copy link
Contributor

mhegazy commented Sep 8, 2017

Note that this does not cover the generic case, i.e.

  function f<K extends string>(k: K) {
      return { [k]: 0 } ;  // type is { [x:string]: number}
  }

for these cases we recommend casting to unionize type that generates a union of all properties of the object, i.e.:

  type Unionize<T> = {[P in keyof T]: {[Q in P]: T[P]}}[keyof T];

  function f<K extends string>(k: K) {
      return { [k]: 0 } as Unionize<{[P in K]: number}>;
  }

@simonbuchan
Copy link

simonbuchan commented Sep 8, 2017

@mhegazy, with that code, won't a k : "foo" | "bar" generate { foo: number, bar: number }? It should instead be { foo: number } | { bar: number }, or equivalent.

Edit: I'm not really clear on what the [Q in P] is doing, I guess: is it possible for P to be a union type when it's already the index of keyof T? Current VS Code claims { foo, bar } like I said, changing [Q in P] to just [P] just turns it into {} (which seems like a bug), but it looks like that might be what the fix allows.

Edit Edit: looks like TS 2.5.2 correctly reports an error on [P].

Edit 3: OK, tried on nightlies, and it looks like with #18317 Unionize<T> works as expected (nice!) Looks like I will need a pretty careful comment though!

@kimwykoff
Copy link

kimwykoff commented Apr 16, 2018

I would like this feature to work too, but I have a couple of issues

  1. I would like to construct an object with any number of values from the enum, but I'm forced to provide an object for every one:
export const enum StepNames {
    one = 'one',
    two = 'two',
    final = 'final'
}

export type Step = {[key in StepNames]: {header: string}};

When I try to use it like:

const oneStep: Step = { 'one': { header: 'First' } };

I get this compile error:

    TS2322: Type '{ 'one': { header: string; }; }' is not assignable to type 'Step'.
  Property 'two' is missing in type '{ 'one': { header: string; }; }'.
  1. I would like to parameterize my Step type, then I can pass in any set of step names, but I get a strange error message. Here's an example extending from the OP's.
const enum Test {
    A = 'a',
    B = 'b'
}

type TestMap<T> = {[key in keyof T]: string}

const x: TestMap<Test> = {
    ['a']: 'string',
    ['b']: 'another string'
}
// Error: TS2322: Type '{ ['a']: string; ['b']: string; }' is not assignable to type 'Test'.

Is there any way to get around this problem?

@SLaks
Copy link

SLaks commented Apr 16, 2018

@kimwykoff

  1. You need to mark those properties as optional. Demo

  2. Since you're using values (not the enum property names), you need to specify T, not keyof T. Demo

@kimwykoff
Copy link

@SLaks Thanks. Sorry for the basic questions.

@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
Committed The team has roadmapped this issue Fixed A PR has been merged for this issue Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

6 participants