Blueprint: Replace enums with string literals

Created on 7 Dec 2016  路  15Comments  路  Source: palantir/blueprint

Let's replace enums with string literals. Instead of something like

export enums RegionCardinality {
  CELLS,
  FULL_COLUMNS,
}

we can have something like

export type RegionCardinality = "cells"
  | "full-columns"

export const RegionCardinality = {
  CELLS: "cells" as RegionCardinality,
  FULL_COLUMNS: "full-columns" as RegionCardinality,
}

@adidahiya pointed out that this may break existing apps so I'm adding this to the 2.x milestone for now. See #307 for existing PR. We can reopen it once we're ready to make this change.

List of enums:

  • [x] Intent
  • [x] Position
  • [x] Table RegionCardinality
  • [x] Elevation (cards)
  • [x] AnimationState (Collapse)
  • [x] CollapseFrom (CollapsibleList)
  • [x] HotkeyScope
  • [x] PopoverInteractionKind
  • [x] DateRangeBoundary / RangeIndex
  • [x] Months
  • [x] TimePickerPrecision
  • [x] Direction (Table)
  • [x] RenderMode (Table)
  • [x] QuadrantType (Table)
  • [x] Orientation (Table)
breaking change refactor

Most helpful comment

TypeScript 2.4's string enums are now definitely the way to go.

All 15 comments

What is the reason for this change?

It makes things faster to use/hack on. It also still is fully type-checked if you're using TypeScript.

For example, instead of:

import { Button, Intent } from "@blueprintjs/core";
// later
<Button intent={Intent.DANGER}>Delete</Button>

you could simplify to:

import { Button } from "@blueprintjs/core";
// later
<Button intent="danger">Delete</Button>

Of course, doing things the first way will still work as well, so no existing code should break. (Okay, it's possible some code could break depending on how enums were used in type positions or if people were doing crazy things with the numerical values of the enum...)

These examples unfortunately aren't equal in terms of usability and speed. String operations are generally slower than enum (number) operations. You can't easily refactor a string literal, but you can for example hit F2 on an enum member and rename all occurrences, and more little things like this. Why not use these things for its intended use cases?

the idea behind moving to typed string literals is that they can be used either as Intent.DANGER or as "danger" if you want to skip the import. the Intent export will still be there, but it'll be a map of string-to-string instead of string-to-number.

the performance effect of string vs number comparison is negligible; it's not useful to think like that.

Here's a cool trick in ts2.1 (from https://basarat.gitbooks.io/typescript/docs/types/literal-types.html):

/** Utility function to create a K:V from a list of strings */
function strEnum<T extends string>(o: Array<T>): {[K in T]: K} {
  return o.reduce((res, key) => {
    res[key] = key;
    return res;
  }, Object.create(null));
}

/**
  * Sample create a string enum
  */

/** Create a K:V */
const Direction = strEnum([
  'North',
  'South',
  'East',
  'West'
])
/** Create a Type */
type Direction = keyof typeof Direction;

This also makes the API nicer for vanilla Javascript devs, where enumeration types like this aren't as idiomatic as they are in Typescript. https://github.com/dphilipson/typescript-string-enums is handy (I use it), and Typescript is adding string enumerations as a first-class thing in 2.4 (https://github.com/Microsoft/TypeScript/pull/15486).

Also, it makes it safer to use "|| default" idiom because you don't have to be careful of the 0-valued enumeration as (e.g.) Position.TOP_LEFT is: this.props.position || Position.BOTTOM.

TypeScript 2.4's string enums are now definitely the way to go.

+1 for string enums: https://www.typescriptlang.org/docs/handbook/enums.html

Make it a const enum to make it even lighter weight (no code emitted by typescript compiler; simply replaces all usages of the enums values with string literals during compile)

export const enum RegionCardinality {
  CELLS = "cells",
  FULL_COLUMNS = "full-columns"
}

(edit: On second thought, you may not want a const enum. A non-const enum will emit code that allows javascript projects to reference your enums by name. Const enums are more relevant for pure typescript projects)

They are essentially interchangeable with string literal types, but have the benefit of being organized, providing a place to document each value, and providing more explicit code completion in IDEs.

@UselessPickles ah didn't know about that const trick, but I think you're right that we do want to emit an enum type that can be used in code.

@giladgray Yeah, the const trick makes much more sense in a purely TypeScript project (not a library to be used in other projects), or only for enums within a library project that are private/internal to the library.

const enums will be more useful once this bug is fixed: https://github.com/Microsoft/TypeScript/issues/17372

Another thing worth being aware of is that string literals are NOT assignable to enum types, even if the string literal exactly matches one of the enum's values. By defining something as an enum, you are forcing all TypeScript users to use the enum instead of a string literal (not a bad thing, but worth being aware of).

See this issue (marked "working as intended"): https://github.com/Microsoft/TypeScript/issues/15930

However, enum values ARE assignable to string literal types of matching values.

Code examples:

/**
 * General concept of region cardinality described here.
 * Individual values cannot be documented/explained clearly here.
 */
export type RegionCardinality = "cells" | "full-columns";

/**
 * General concept of region cardinality described here.
 */
export enum RegionCardinalityEnum {
  /**
   * Documentation explaining CELLS cardinality
   */
  CELLS = "cells",
  /**
   * Documentation explaining FULL_COLUMNS cardinality
   */
  FULL_COLUMNS = "full-columns"
}

function acceptsStringLiteral(cardinality: RegionCardinality): void {}
function acceptsEnum(cardinality: RegionCardinalityEnum): void {}

acceptsStringLiteral("cells"); // valid
acceptsStringLiteral(RegionCardinalityEnum.CELLS); // valid

acceptsEnum("cells"); // COMPILER ERROR!!!
acceptsEnum(RegionCardinalityEnum.CELLS); // valid

I personally don't see much value in string literal types now that string enums are available, though. My vote is for string enums only.

@UselessPickles That's good, actually. If the enum changes, the strings wouldn't update.

Tangentially related: I've been working on a little project to implement a generic visitor pattern that works on any string literal enum type, and any string literal union type. It provides a compile-time guarantee that you handle every possible value, including null/undefined when relevant.

I just finished cleaning it up and documenting it last night. Check it out: https://github.com/UselessPickles/ts-string-visitor

Available as an NPM package: https://www.npmjs.com/package/ts-string-visitor

fixed by #1921

Was this page helpful?
0 / 5 - 0 ratings

Related issues

vinz243 picture vinz243  路  3Comments

tgreenwatts picture tgreenwatts  路  3Comments

brsanthu picture brsanthu  路  3Comments

sighrobot picture sighrobot  路  3Comments

adidahiya picture adidahiya  路  3Comments