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 string enum as key in {[key]: Type} throws not an index signature #22892

Closed
appsforartists opened this issue Mar 26, 2018 · 8 comments
Closed
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@appsforartists
Copy link

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

@mhegazy
Copy link
Contributor

mhegazy commented Mar 26, 2018

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) { ... }

@mhegazy mhegazy added the Working as Intended The behavior described is the intended behavior; this is not a bug label Mar 26, 2018
@appsforartists
Copy link
Author

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

@mhegazy
Copy link
Contributor

mhegazy commented Mar 27, 2018

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.

@mhegazy mhegazy added Suggestion An idea for TypeScript In Discussion Not yet reached consensus and removed Working as Intended The behavior described is the intended behavior; this is not a bug labels Mar 27, 2018
@appsforartists
Copy link
Author

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.

@dardino
Copy link

dardino commented Mar 27, 2018

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

@mhegazy
Copy link
Contributor

mhegazy commented Mar 27, 2018

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

@RyanCavanaugh RyanCavanaugh added Working as Intended The behavior described is the intended behavior; this is not a bug and removed In Discussion Not yet reached consensus Suggestion An idea for TypeScript labels Aug 6, 2018
@RyanCavanaugh
Copy link
Member

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.

@royling
Copy link

royling commented Feb 3, 2021

This can now be easily done with the new template literal types introduced in 4.1:

enum X {
  A = 'a',
  B = 'b',
}

type ValueOfX = `${X}`

type Y = {
  [P in ValueOfX]?: number;
}

let cc: Y = {
  a: 0,
  c: 1, // error: c does not exist in type Y
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

5 participants