Reputation: 4198
I want a type to describe a set of strings, and an object with keys for easy access to said strings.
When const TwoWords
is initialized with all of the keys' values type-asserted to TwoWords, then DoSomething(TwoWords.Foo)
compiles, but the typeguard on the switch statement doesn't work as expected - the type of word
in the default case is not never
.
type TwoWords = 'foo' | 'bar';
const TwoWords = {
Foo: 'foo' as TwoWords,
Bar: 'bar' as TwoWords
};
function DoSomething(word: TwoWords) {
switch (word) {
case TwoWords.Foo:
break;
case TwoWords.Bar:
break;
default:
let typeInferenceCheck: never = word; // Type '"foo"' is not assignable to type 'never'
}
}
DoSomething(TwoWords.Foo);
DoSomething('bar');
However, if I use a string literal type assertion for each of TwoWords
keys' values, the type of word
in the default case is never
as I would expect.
type TwoWords = 'foo' | 'bar';
const TwoWords = {
Foo: 'foo' as 'foo',
Bar: 'bar' as 'bar'
};
function DoSomething(word: TwoWords) {
switch (word) {
case TwoWords.Foo:
break;
case TwoWords.Bar:
break;
default:
let typeInferenceCheck: never = word; // OK
}
}
DoSomething(TwoWords.Foo);
DoSomething('bar');
In cases where 'foo'
and 'bar'
are much longer strings (say, a whole sentence), I don't want to duplicate them - it's too verbose. Is there another way to have a string keyed enum that behaves as expected from a type-inference perspective in a switch statement (or if/else chain)?
As per Madara Uchiha's answer, you can get proper type inference (as in Option 2) without the verbosity using TypeScript 2.4 string enums, but these aren't interchangeable with string literals.
DoSomething('bar'); // Type '"bar"' is not assignable to parameter of type 'TwoWords'
(See GitHub issue #15930 about string literal assignment to TypeScript 2.4 string enum)
I am looking for another option which allows me to have:
EnumLikeObject.Foo === 'foo'
let thing: EnumLikeObject = 'foo'
let thing: EnumLikeObject = EnumLikeObject.Foo
Objections & Discussion
Upvotes: 7
Views: 2675
Reputation: 175038
If you care to wait and endure for a bit,
TypeScript 2.4 brings true string enums to the playing field:
enum TwoWords {
Foo = 'foo',
Bar = 'bar'
}
function DoSomething(word: TwoWords) {
switch (word) {
case TwoWords.Foo:
break;
case TwoWords.Bar:
break;
default:
let typeCheck: never = word; // OK
}
}
That gets you the best of both worlds.
Upvotes: 6
Reputation: 15906
I think I got it. At least:
DoSomething
accepts either "foo"
or "bar"
.And to satisfy your criteria
TwoWords.Foo === 'foo'
And so without further ado:
const TwoWords = (function () {
const Foo = 'foo';
const Bar = 'bar';
const ret = {
Foo: Foo as typeof Foo,
Bar: Bar as typeof Bar,
};
return ret;
})()
type TwoWords = typeof TwoWords[keyof typeof TwoWords];
And then I had a light bulb moment
namespace TwoWords2 {
export const Foo = "foo";
export const Bar = "bar";
}
type TwoWords2 = typeof TwoWords2[keyof typeof TwoWords2]
// didn't test this, not sure if it actually updates the
// original object or just returns a frozen copy
Object.freeze(TwoWords2);
It's not a downside, in my opinion, because it still throws an error in the type checker and in VS Code, but TwoWords2.Bar = "five"
actually works because the namespace gets compiled to a simple object. But that's the way typescript works. Obviously the first code also has that problem, but it wouldn't throw a type error, so the second is superior, IMO.
Upvotes: 2