Skip to content

Latest commit

 

History

History
643 lines (457 loc) · 19.2 KB

README.md

File metadata and controls

643 lines (457 loc) · 19.2 KB

Enumeration.TS

GitHub NPM Contributor Covenant

Enumeration.TS is a small but powerful package which brings support for type safe enums into TypeScript. Besides it provides you polymorphic support similar to regular classes to avoid switch cases and write better code.

We recommend to use our official website to navigate through available features. You can also use the latest documentation described below.

Give a star ⭐

If you like or are using this project please give it a star. Thanks!

Table of contents

What issues does it solve?

Imagine that we are going to introduce simple coloring concept in our application. So simple that we can use an Enum to represent a list of available colors.

In TypeScript there are 2 types of Enum:

  • Numeric based;
  • String based;

For our first implementation we decided that using numeric based Enum is sufficient, so we ended up like this.

export enum Color
{
    Red,
    Lime,
    Navy,
    Maroon,
    Silver
}

Some months later our management came to us and said that now we have to display available color names in the UI. Ok, this change looks simple so we decided to change our Enum to a string based and use item names in the UI.

export enum Color
{
    Red = 'Red',
    Lime = 'Lime',
    Navy = 'Navy',
    Maroon = 'Maroon',
    Silver = 'Silver'
}

This works perfectly fine for a while and one day we have got a request to display HEX and RGB color codes so our Enum concept no longer works. We might ask our backend developer to introduce a list of colors somewhere in the database but such change will start bringing frontend related logic to the backend. Besides this list is static, so we might end up writing database migrations each time color property change. We dont want to be dependent from backend developers so end up using switch cases like this through newly introduced color service.

export class ColorService
{
    public getHex(color: Color): string
    {
        switch (color)
        {
            case Color.Red:
                return '#FF0000';
            case Color.Lime:
                return '#00FF00';
            case Color.Navy:
                return '#000080';
            case Color.Maroon:
                return '#800000';
            case Color.Silver:
                return '#C0C0C0';
        }
    }

    public getRgb(color: Color): string
    {
        switch (color)
        {
            case Color.Red:
                return 'rgb(255, 0, 0)';
            case Color.Lime:
                return 'rgb(0, 255, 0)';
            case Color.Navy:
                return 'rgb(0, 0, 128)';
            case Color.Maroon:
                return 'rgb(128, 0, 0)';
            case Color.Silver:
                return 'rgb(192, 192, 192)';
        }
    }
}

No matter if it will be a backend change or a frontend one - we started introduce complexity to our application because of having behaviour for static items. Is there a way to simplify that?

Fortunately - yes. The pattern is called enumeration classes. We can replace Enum item with Enum-like class and keep behaviour inside it. Note that this pattern should be used only if you have some behaviour for Enum items. As you see we ended up writing a switch case so using this pattern might be a good option.

Let's use Enumeration.TS package and remove unnecessary ColorService. We have to define our color Enum like the following.

import { Enumeration } from '@dipscope/enumeration';

export class Color extends Enumeration<Color, string>
{
    public static readonly Red: Color = new Color('Red', '#FF0000', 'rgb(255, 0, 0)');
    public static readonly Lime: Color = new Color('Lime', '#00FF00', 'rgb(0, 255, 0)');
    public static readonly Navy: Color = new Color('Navy', '#000080', 'rgb(0, 0, 128)');
    public static readonly Maroon: Color = new Color('Maroon', '#800000', 'rgb(128, 0, 0)');
    public static readonly Silver: Color = new Color('Silver', '#C0C0C0', 'rgb(192, 192, 192)');

    public readonly hex: string;
    public readonly rgb: string;

    public constructor(key: string, hex: string, rgb: string)
    {
        super(key);

        this.hex = hex;
        this.rgb = rgb;

        return;
    }
}

We extend base Enumeration class and specify that we are going to have enumeration of colors with a string based key. As now each enumeration item represents a class we added hex and rgb properties. Now all static information related to colors stored where it should be. By having a key attached to some entity or comming directly from the backend we can write our code like following.

// Get a key from entity or backend. 
const key = ...; 

// Get type safe enumeration item by key.
const color = Color.get(key); 

// Get hex color code.
const hex = color.hex; 

// Get rgb color code.
const rgb = color.rgb;

This is a most basic sample of how implementation might looks like when using a library. It provides us several useful methods to work with such enumerations out of the box. Check static methods for more info.

It is a great alternative for similar packages like Enumify. It covers all built in Enum features like numeric comparisons and brings polymorphic support to enumeration world.

Want to know more? Let's dive into the details.

Installation

Enumeration.TS is available from NPM, both for browser (e.g. using webpack) and NodeJS:

npm i @dipscope/enumeration

This package has no dependencies so we can directly go to the definitions.

Definition

We support all features available with regular Enum and a bit more. Lets go through the steps required to make use of enumeration classes.

Numeric enumeration

If we have numeric based enum like the following.

export enum Permission
{
    None,
    View,
    Create,
    Edit,
    Delete
}

It can be defined as following.

import { Enumeration } from '@dipscope/enumeration';

export class Permission extends Enumeration<Permission, number> 
{
    public static readonly None: Permission = new Permission(1, 'None');
    public static readonly View: Permission = new Permission(2, 'View');
    public static readonly Create: Permission = new Permission(3, 'Create');
    public static readonly Edit: Permission = new Permission(4, 'Edit');
    public static readonly Delete: Permission = new Permission(5, 'Delete');

    public readonly name: string;

    public constructor(key: number, name: string)
    {
        super(key);

        this.name = name;

        return;
    }
}

In our case we extended it with string based name. If you are using compare operators for numeric Enum like >, >=, <, <= they are supported without any changes to the code as well as ==, !=.

// Get a user permission from somewhere.
const permission = Permission.Edit;

// Checks for permissions.
const gt = permission > Permission.View;
const lt = permission < Permission.Delete;

If you want to specify explicitly that such enumerations represents classes you are free to use instance methods.

// Get a user permission from somewhere.
const permission = Permission.Edit;

// Checks using instance methods.
const gt = permission.gt(Permission.View);
const lt = permission.lt(Permission.Delete);

Instance methods are just a syntax sugar for compare operators.

String enumeration

If we have string based enum like the following.

export enum Color
{
    Red = 'Red',
    Lime = 'Lime',
    Navy = 'Navy',
    Maroon = 'Maroon',
    Silver = 'Silver'
}

It can be defined as following.

import { Enumeration } from '@dipscope/enumeration';

export class Color extends Enumeration<Color, string>
{
    public static readonly Red: Color = new Color('Red', '#FF0000', 'rgb(255, 0, 0)');
    public static readonly Lime: Color = new Color('Lime', '#00FF00', 'rgb(0, 255, 0)');
    public static readonly Navy: Color = new Color('Navy', '#000080', 'rgb(0, 0, 128)');
    public static readonly Maroon: Color = new Color('Maroon', '#800000', 'rgb(128, 0, 0)');
    public static readonly Silver: Color = new Color('Silver', '#C0C0C0', 'rgb(192, 192, 192)');

    public readonly hex: string;
    public readonly rgb: string;

    public constructor(key: string, hex: string, rgb: string)
    {
        super(key);

        this.hex = hex;
        this.rgb = rgb;

        return;
    }
}

We extended base Enum with hex and rgb properties. We can also use compare operators similar to numeric enumeration if it makes sense in code. The behaviour will be the same as when we compare strings.

Polymorphic enumeration

If we have an Enum with behaviour and extending it with properties is not enough we can go with polymorphic enumeration. It can be used with both numeric and string based keys. Imagine that we are working with shapes. Shapes are coming to us from the backend as a numeric static list.

export enum Shape
{
    Triangle,
    Square,
    Pentagon,
    Hexagon,
    Heptagon
}

We decided to use enumeration class pattern as we have to draw this shapes somewhere in the UI and draw method contains some complex logic. We can declare each enumeration item as self sufficient class using following declaration.

import { Enumeration, defer } from '@dipscope/enumeration';

export abstract class Shape extends Enumeration<Shape, number> 
{
    public static readonly Triangle: Shape = defer(() => new Triangle(1));
    public static readonly Square: Shape = defer(() => new Square(2));
    public static readonly Pentagon: Shape = defer(() => new Pentagon(3));
    public static readonly Hexagon: Shape = defer(() => new Hexagon(4));
    public static readonly Heptagon: Shape = defer(() => new Heptagon(5));

    public abstract draw(context: CanvasRenderingContext2D): CanvasRenderingContext2D;
}

export class Triangle extends Shape
{
    public draw(context: CanvasRenderingContext2D): CanvasRenderingContext2D 
    {
        // Draw triangle here...

        return context;
    }
}

export class Square extends Shape
{
    public draw(context: CanvasRenderingContext2D): CanvasRenderingContext2D 
    {
        // Draw square here...

        return context;
    }
}

export class Pentagon extends Shape
{
    public draw(context: CanvasRenderingContext2D): CanvasRenderingContext2D 
    {
        // Draw pentagon here...

        return context;
    }
}

export class Hexagon extends Shape
{
    public draw(context: CanvasRenderingContext2D): CanvasRenderingContext2D 
    {
        // Draw hexagon here...

        return context;
    }
}

export class Heptagon extends Shape
{
    public draw(context: CanvasRenderingContext2D): CanvasRenderingContext2D 
    {
        // Draw heptagon here...

        return context;
    }
}

Now each enumeration item is declared like a class with behavior. We have to use defer function provided by the library to defer item initialization at a later time as at the moment of Shape class declaration our direct descendants are not defined. With such definition you can now call polymorphic methods in you code.

// Get drawing context.
const context = new CanvasRenderingContext2D();

// Get enum as number from backend.
const shapeId = shapeService.getFirstShapeId();

// Get our shape.
const shape = Shape.get(shapeId);

// Draw our shape. 
shape.draw(context);

The behaviours of base enumeration and polymorphic one are absolutely the same. The only difference is calling defer function for a deferred initialization.

Static methods

When we finish with our enumeration definitions we can start manipulating them. Enumeration class provides us several static methods to iterate over available items.

Let's take the following enumeration as an example.

import { Enumeration } from '@dipscope/enumeration';

export class Color extends Enumeration<Color, string>
{
    public static readonly Red: Color = new Color('Red', '#FF0000', 'rgb(255, 0, 0)');
    public static readonly Lime: Color = new Color('Lime', '#00FF00', 'rgb(0, 255, 0)');
    public static readonly Navy: Color = new Color('Navy', '#000080', 'rgb(0, 0, 128)');
    public static readonly Maroon: Color = new Color('Maroon', '#800000', 'rgb(128, 0, 0)');
    public static readonly Silver: Color = new Color('Silver', '#C0C0C0', 'rgb(192, 192, 192)');
}

As you see this is a simple enumeration of colors with a string based key.

Get

To get an instance of enumeration item call get method and provide a key associated with it.

const key = 'Red';
const redColor = Color.get(key);

This call will return you an item if it is available or undefined if item with provided key is not present.

Has

To check if enumeration has an item with a certain key call has method.

const key = 'Red';
const hasRedColor = Color.has(key);

This call will return true if item with provided key exists and false otherwise.

Keys

To get a list of all keys available call keys method.

const colorKeys = Color.keys();

This call will return an iterator for all registered keys.

Values

To get a list of all values available call values method.

const colorValues = Color.values();

This call will return an iterator for all registered values.

Entries

If you want to iterate over keys and values at the same time call entries method.

const colorEntries = Color.entries();

This call will return a key-value iterable for all registered items.

Map

If you want to get a map of enumeration items call map method.

const colorMap = Color.map();

This call will return a map of all registered items.

Instance methods

There are several instance methods which base enumeration class provides. They are related mostly to compare operations and in most cases will not be used directly.

Let's take the following enumeration as an example.

import { Enumeration } from '@dipscope/enumeration';

export class Permission extends Enumeration<Permission, number> 
{
    public static readonly None: Permission = new Permission(1, 'None');
    public static readonly View: Permission = new Permission(2, 'View');
    public static readonly Create: Permission = new Permission(3, 'Create');
    public static readonly Edit: Permission = new Permission(4, 'Edit');
    public static readonly Delete: Permission = new Permission(5, 'Delete');
}

As you see this is an enumeration of permissions with numeric based key.

Eq

To check if one enumeration item equals another call eq method.

const eq = permission.eq(Permission.Edit);

We can also simply write the following.

const eq = permission === Permission.Edit;

This call will return true if enumeration item equals another and false otherwise.

Neq

To check if one enumeration item not equals another call neq method.

const neq = permission.neq(Permission.Edit);

We can also simply write the following.

const neq = permission !== Permission.Edit;

This call will return true if enumeration item not equals another and false otherwise.

Gt

To check if one enumeration item greater than another call gt method.

const gt = permission.gt(Permission.View);

We can also simply write the following.

const gt = permission > Permission.View;

This call will return true if enumeration item is greater than another and false otherwise.

Gte

To check if one enumeration item greater than or equals another call gte method.

const gte = permission.gte(Permission.View);

We can also simply write the following.

const gte = permission >= Permission.View;

This call will return true if enumeration item is greater than or equals another and false otherwise.

Lt

To check if one enumeration item lower than another call lt method.

const lt = permission.lt(Permission.View);

We can also simply write the following.

const lt = permission < Permission.View;

This call will return true if enumeration item is lower than another and false otherwise.

Lte

To check if one enumeration item lower than or equals another call lte method.

const lte = permission.lte(Permission.View);

We can also simply write the following.

const lte = permission <= Permission.View;

This call will return true if enumeration item is lower than or equals another and false otherwise.

ValueOf

To convert enumeration item into a primitive value call valueOf method.

const value = permission.valueOf();

This call will convert a key to a primitive value. It is also called during compare operations.

ToString

To convert enumeration item into string call toString method.

const str = permission.toString();

This call will convert a key to a string representation.

Versioning

We use SemVer for versioning. For the versions available, see the versions section on NPM project page.

See information about breaking changes, release notes and migration steps between versions in CHANGELOG.md file.

Contributing

Please read CONTRIBUTING.md for details on our code of conduct, and the process for submitting pull requests to us.

Authors

  • Dmitry Pimonov - Initial work - dpimonov

See also the list of contributors who participated in this project.

Notes

Thanks for checking this package.

Feel free to create an issue if you find any mistakes in documentation or have any improvements in mind.

We wish you good luck and happy coding!

License

This project is licensed under the Apache 2.0 License - see the LICENSE.md file for details.