UselessPickles / ts-enum-util

Strictly typed utilities for working with TypeScript enums
MIT License
231 stars 6 forks source link

New feature: Get enums sorted by value #7

Closed maorefaeli closed 5 years ago

maorefaeli commented 5 years ago

Motivation: In numerous scenarios, the order of the values is more important than lexicographical keys. for example:

import React, {PureComponent} from "react";
import { $enum } from "ts-enum-util";

enum Log {
    debug,
    info,
    warning,
    error
}

class LogSelector extends PureComponent{
    public render() {
        return <select>{$enum(Log).map((value, key) => <option key={value}>{key}</option>)}</select>;
    }
}

The select will have the options ordered debug, error, info, warning which is harder to understand than debug, info, warning, eror.

Maybe add something like $enum(Log, true) to order by value? or passing comparable? Thanks

UselessPickles commented 5 years ago

You can currently get the results you want by using getEntries() to get an array of all the [key, value] tuples, then sorting the entries however you want before mapping them to JSX Elements:

$enum(Log)
    .getEntries()
    .sort((lhs, rhs) => lhs[1] - rhs[1])
    .map(([key, value]) => (<option key={value}>{key}</option>);

I will not be adding any extra params to the $enum() function to control how the EnumWrapper instance is initialized, because that will lead to confusion and bugs if you call $enum() on the same enum multiple times throughout your code. Only the first call will initialize the instance, and all subsequent calls return a cached instance. You would have to find all calls to $enum() for a given enum and update them all to use the same params to guarantee the EnumWrapper instance is initialized as you expect, which is a bad design.

I will, however, consider changing the hard-coded sort order to be based on value rather than key. The sort order is not intended to be meaningful. It is only intended to guarantee consistency across JS implementations, since there is no guaranteed order of iteration over keys of the original enum object. But I do agree that it is more likely that sorting by value will be more meaningful than sorting by key. I only sorted by key because it was the simplest and most efficient implementation to guarantee consistent order.

I will also consider adding a global setting to disable sorting. This would be useful if you know that object properties/keys are always iterated in the order in which they were defined within the environments that you will run your code (very common, but not guaranteed by ECMAScript specs). This would retain the exact order of the enum's definition, which is probably the most meaningful order.

maorefaeli commented 5 years ago

Thanks for your reply, I get what you say about the extra parameter to $enum(). I don't know if a global setting will be the best solution because different enums have different meanings, and therefore different sorting purposes. Maybe a sorting parameter to the map, keys, values, etc. ?

UselessPickles commented 5 years ago

different enums have different meanings, and therefore different sorting purposes.

Exactly why I can't really include a solution that works for everyone :)

I can only choose the default sort order that makes the most sense in the largest number of use cases, and provide tools to let you do whatever you want with the result afterward.

My options for default sort order are:

  1. Sorted by key.
  2. Sorted by value.
  3. Un-sorted (whatever order keys are iterated on the original runtime enum object, which will be the order in which the properties were defined/listed in many JS implementations, but there's no guarantee).

Option 3 cannot be guaranteed to be consistent across all JS implementations, so I don't want it to be the default. In environments where the order of definition of properties is preserved, I think this is most commonly the most meaningful order (developers often list enum definitions in some logical order), which is the only reason I'm considering making it an optional global setting.

Between options 1 and 2, I think 2 may be more likely to be meaningful (at least for numeric enums). I'm really not sure how to go about determining whether this is true, because it depends on having knowledge about how most people use enums. I can only assume based on how I use enums, and how I've seen enums used in a limited number of projects.

Regardless of the default sort order, tools are already available for you to sort however you want, via getEntries().sort().

Maybe a sorting parameter to the map, keys, values, etc. ?

I'd rather not complicate all those methods. Some of them are quite optimized right now because of assumptions of immutability, etc. It also wouldn't be very intuitive for all of those methods to accept a comparator.

I think the most reasonable improvement over existing functionality is to implement a sort() method directly on EnumWrapper that creates a new lightweight copy/view of the original EnumWrapper, but in terms of the specified sort order. This would be purely for increased convenience. The challenge is making some decisions on how to implement this, what kinds of use cases to optimize for, etc.

maorefaeli commented 5 years ago

Consider everything you said,

I'm considering making it an optional global setting

sounds like the best solution. One could set that flag for its most common use case and use getEntries().sort() for the others.

implement a sort() method directly on EnumWrapper

would be nice, but like you said, only on increased convenience over the existing solution :)

UselessPickles commented 5 years ago

After some more research, I've decided that the next major version of ts-enum-util aim to retain the exact order of the original enum definition by relying on ES6's specification of Object.getOwnPropertyNames and the de facto standard of key iteration in all modern browsers.

The subtle difference between some browser implementations of key iteration order is a matter of whether creation order is retained for ALL kinds of keys, or does it follow the ES6 Object.getOwnPropertyNames specification for iterating numeric keys in numeric order first, followed by string keys in order of creation, then symbol keys in order of creation.

Since I only care about the string keys (I explicitly ignore numeric reverse lookup keys that TypeScript generates for numeric enums), then I think it's safe for me to rely on Object.getOwnPropertyNames to give me the keys in the order they were originally defined in the enum. Even if a polyfill is used in some environment that doesn't have native support for Object.getOwnPropertyNames, it will be implemented in terms of either Object.keys or for/in iteration, which is almost guaranteed to follow de facto standards of at least retaining relative creation order of the string keys I care about.

UselessPickles commented 5 years ago

I might even implement a simplified polyfill for Object.getOwnPropertyNames so that this doesn't depend on a an external polyfill.

UselessPickles commented 5 years ago

If you want the new "original defined order" behavior, check out the latest alpha release ("alpha" tag on npm). Other than the change in sort order, the only other breaking change is that TypeScript 2.9 is now the minimum supported version.

The biggest change in the next major release is that I have taken the functionality of ts-string-visitor, expanded it to handle number literals (and therefore numeric enums), and merged it into ts-enum-util as $enum.visitValue() and $enum.mapValue(). The code is all done and tested. Updating the README is the hard part that I still have to do before releasing it.

UselessPickles commented 5 years ago

Version 4.0.0 is now released, which nearly guarantees that the original defined order of the enum will be retained. This new default sort order should be more useful in most cases.