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'.
Related Issues:
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.
Most helpful comment
Another case with standard enums: