Typescript: Using string enum as key in {[key]: Type} throws not an index signature

Created on 27 Mar 2018  路  7Comments  路  Source: microsoft/TypeScript

TypeScript Version: 2.9.0-dev.20180325

Search Terms: string enum index signature cast

Code

enum Fruits {
    MANGO = 'MANGO',
    BANANA = 'BANANA',
}

type StringDict = { [key: string]: string };

function map(dict: StringDict, transform: (key: string, value: string) => void) {
    const result = {};

    for (const key of Object.keys(dict)) {
        result[key] = transform(key, dict[key]);
    }

    return result;
}

map(Fruits, (key, value) => {
    value.toLowerCase();
});


map(Fruits as StringDict, (key, value) => {
    value.toLowerCase();
});

Expected behavior:
Both map calls succeed, because a string enum is essentially a {[key: string]: string}. I ought to be able to use it anywhere that needs something indexed by string (as long as the values are appropriate).

Actual behavior:
index.ts(14,5): error TS2345: Argument of type 'typeof Fruits' is not assignable to parameter of type 'StringDict'.
Index signature is missing in type 'typeof Fruits'.
index.ts(19,5): error TS2352: Type 'typeof Fruits' cannot be converted to type 'StringDict'.
Index signature is missing in type 'typeof Fruits'.

Playground Link

Related Issues:

20011, #18029, #16760

Working as Intended

Most helpful comment

Another case with standard enums:

enum MyEnum {
    ValA = 0,
    ValB = 1,
}

type IEnumTpProp<R> = {[key in keyof typeof MyEnum]: R };

var X: IEnumTpProp<string> = {
    ValA: "text1",
    ValB: "text2",
    0: "error" // expected: error;  current: error; //OK
};

X[MyEnum.ValA] = "no error??"; // current: no error; expected: error // KO
X[MyEnum[MyEnum.ValA]] = "no error"; // current: no error; expected: no error // OK

All 7 comments

StringDict has a stronger contract than Fruits, since Fruits["someRandomString"] is not string; and thus Fruit is not a StringDict.

the oposite is true as well, since the type of Fruite.Mango is a specific literal type "MANGO" that is not just any string.

For these two issues the type assertion Fruits as StringDict fails.

consider defining the function as object or as generic function in K where K is the keys of the object passed, both would allow you to call it on Fruite. e.g.:

function map(dict: object, transform: (key: string, value: string) => void) { ... }

or

function map<K extends string>(dict: Record<K, string>, transform: (key: string, value: string) => void) { ... }

That's presuming I control the definition of map.

I feel like I ought to be able to pass a string enum into a function that requires a [string]: string and it should work. I understand your point about type theory, but it fails practically here.

Is the solution when I don't control map to do map(myEnum as {} as {[key: string]: string}? Not only is that ugly, but it doesn't protect me in the case that someone changes the shape of the enum (e.g. adding a non-string value).

it is unsafe, since the index signature on the enum is really string | undefined. but i suppose we can bend the rules here the same way we do for object literals... worth discussing.

Thanks.

The index signature on {a: 1, b: 2} is also really string|undefined, so accepting a string enum's potential for an undefined key feels consistent with that.

Another case with standard enums:

enum MyEnum {
    ValA = 0,
    ValB = 1,
}

type IEnumTpProp<R> = {[key in keyof typeof MyEnum]: R };

var X: IEnumTpProp<string> = {
    ValA: "text1",
    ValB: "text2",
    0: "error" // expected: error;  current: error; //OK
};

X[MyEnum.ValA] = "no error??"; // current: no error; expected: error // KO
X[MyEnum[MyEnum.ValA]] = "no error"; // current: no error; expected: no error // OK

@dardino i am afraid this is a different issue. all objects can be indexed with strings/numbers and result in an any if not defined. it should be reported as an error under --noImplicitAny.

This would be super problematic because we always allow expressions of the form expr.propname if expr has a string index signature, so there would be effectively zero "typo protection" on propname. We don't want to be in the situation where removing or renaming a string enum key doesn't cause new errors to appear, or where misspelling a string enum key doesn't cause an error.

Was this page helpful?
0 / 5 - 0 ratings