Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make TypeScript string enums that work with string literals and do type inference correctly

I want a type to describe a set of strings, and an object with keys for easy access to said strings.

Option 1 (doesn't provide proper type inferencing)

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');

Option 2 (correct type inference, but overly verbose)

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

Option 3 (not interchangeable with string literals)

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)

Criteria

I am looking for another option which allows me to have:

  1. An enum-style object for accessing string literals EnumLikeObject.Foo === 'foo'
  2. A type indicating that only enum members are allowed, whether they are:
    1. string literals - let thing: EnumLikeObject = 'foo'
    2. properties of the enum-style object - let thing: EnumLikeObject = EnumLikeObject.Foo
  3. the enum-style object and the type must have the same name
  4. In the declaration of the enum-style object and the type, no string literal may be repeated more than twice. If you've got a solution where they must only be repeated once, even better. (In this question, when I speak of verbosity, this criteria is primarily what I'm referring to.)

Objections & Discussion

  • issue #15930 link from Option 3 about string enums and string literals says "The rational here is if you use string enums, you should use them all the way to ensure safe refactoring, otherwise stick with literal types"
    • In our project, we use a parsing library for some xml data (we don't control the data format). This gives us a typed object that uses string literal types, which we map to our internally used objects which use these string enums. (Hence the requirement about string literals.) Sometimes, we work the other way from internal object to generating xml, and for ease of use in those cases we want an enum not just literal types.
    • I may look into changing the type definition for that parsing library to use our string enums instead of literals and then I can drop the string literal assignment requirement, but I'd rather avoid that because that library isn't our responsibility and it seems rather hacky to use our internal types from an external library.
  • issue #16389 from @tycho's comment says that "any literal that is not a constant is predicted to change in value, though not in type" which is why you need to tell the compiler if you know that a variable actually won't change in type beyond certain bounds. The compiler must infer the most general type to allow for changes in the value.
like image 633
alexanderbird Avatar asked Jun 21 '17 18:06

alexanderbird


People also ask

Can enum be string TypeScript?

Enums can also contain strings . This is more common than numeric enums, because of their readability and intent.

Can I use enum as type in TypeScript?

Enums are one of the few features TypeScript has which is not a type-level extension of JavaScript. Enums allow a developer to define a set of named constants. Using enums can make it easier to document intent, or create a set of distinct cases. TypeScript provides both numeric and string-based enums.

Are enums bad in TypeScript?

The they are useless at runtime argument This is a false argument for typescript in general, let alone Enums. and agree, if at runtime some code tries to change the values of one of your enums, that would not throw an error and your app could start behaving unexpectedly ( that is why Object.

How does TypeScript enum work?

In TypeScript, enums, or enumerated types, are data structures of constant length that hold a set of constant values. Each of these constant values is known as a member of the enum. Enums are useful when setting properties or values that can only be a certain number of possible values.


1 Answers

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.

like image 76
Madara's Ghost Avatar answered Oct 11 '22 05:10

Madara's Ghost