Skip to content

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

Closed

Description

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.

Activity

  1. SLaks commented on Jul 10, 2017

    @SLaks

    This should also work for numeric enums.

  2. simonbuchan commented on Jul 31, 2017

    @simonbuchan

    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'.
    
  3. sandersn commented on Aug 2, 2017

    @sandersn
    Member

    Simon Buchan (@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'.

  4. simonbuchan commented on Aug 2, 2017

    @simonbuchan

    Nathan Shively-Sanders (@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.

  5. sandersn commented on Aug 3, 2017

    @sandersn
    Member

    Simon Buchan (@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.

  6. simonbuchan commented on Aug 8, 2017

    @simonbuchan

    Nathan Shively-Sanders (@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.

  7. mhegazy commented on Aug 22, 2017

    @mhegazy
    Contributor

    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>
        }
    }
  8. sandersn commented on Aug 24, 2017

    @sandersn
    Member

    #15473 should be to fix this.

  9. mhegazy commented on Sep 8, 2017

    @mhegazy
    Contributor

    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}>;
      }
  10. simonbuchan commented on Sep 8, 2017

    @simonbuchan

    Mohamed Hegazy (@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!

  11. kimwykoff commented on Apr 16, 2018

    @kimwykoff

    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?

  12. SLaks commented on Apr 16, 2018

    @SLaks

    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

  13. kimwykoff commented on Apr 16, 2018

    @kimwykoff

    SLaks (@SLaks) Thanks. Sorry for the basic questions.

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

Metadata

Metadata

Labels

CommittedThe team has roadmapped this issueFixedA PR has been merged for this issueSuggestionAn idea for TypeScript

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions