Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Typesafe string enums in TypeScript pre-2.4.

License

NotificationsYou must be signed in to change notification settings

dphilipson/typescript-string-enums

Repository files navigation

Typesafe string enums in TypeScript.

Build Status

Table of Contents

Installation

npm install --save typescript-string-enums

This library requires TypeScript 2.2 or later. If you require TypeScript 2.1 compatibility, version0.2.0 of this library is the last one with support.

Usage

Creating and using enums

Define an enum as follows:

// Status.tsimport{Enum}from"typescript-string-enums";exportconstStatus=Enum("RUNNING","STOPPED");exporttypeStatus=Enum<typeofStatus>;

Use it elsewhere:

import{Status}from"./Status";console.log(Status.RUNNING);// -> "RUNNING";// Works fine.constgoodStatus:Status=Status.RUNNING;// TypeScript error: Type '"hello"' is not assignable to type '"RUNNING" | "STOPPED"'constbadStatus:Status="hello";// Enum can be used for discriminated unions:typeState=RunningState|StoppedState;interfaceRunningState{    status:typeofStatus.RUNNING;    pid:number;}interfaceStoppedState{    status:typeofStatus.STOPPED;    shutdownTime:Date;}functionsaySomethingAboutState(state:State){// The following typechecks.if(state.status===Status.RUNNING){console.log("The pid is "+state.pid);}elseif(state.status===Status.STOPPED){console.log("The shutdown time is "+state.shutdownTime);}}

Instead of a list of values, an object may be passed instead if it is desired that the string valuesbe different from the constant names. This also has the advantage of allowing JSDoc comments to bespecified on individual values. For example:

exportconstStatus=Enum({/**     * Hovering over Status.RUNNING in an IDE will show this comment.     */RUNNING:"running",STOPPED:"stopped",});exporttypeStatus=Enum<typeofStatus>;console.log(Status.RUNNING);// -> "running"

Additional functions

Enum.isType(enum, value)

Enum.isType checks if a value is of a given enum type and can be used as a type guard. For example:

constColor=Enum("BLACK","WHITE");typeColor=Enum<typeofColor>;letselectedColor:Color;constcolorString=getUserInputString();// Unsanitized string.// TypeScript error: Type 'string' is not assignable to type '"BLACK" | "WHITE"'.selectedColor=colorString;if(Enum.isType(Color,colorString)){// No error this time because within type guard.selectedColor=colorString;}else{thrownewError(`${colorString} is not a valid color`);}

Enum.keys(enum)

ResemblesObject.keys(), but provides strict typing in its return type.

Enum.values(enum)

ResemblesObject.values(), but provides strict typing in its return type.

Example of bothEnum.keys() andEnum.values():

constFileType=Enum({PDF:"application/pdf",Text:"text/plain",JPEG:"image/jpeg",});typeFileType=Enum<typeofFileType>;constkeys=Enum.keys(FileType);// Inferred type: ("PDF" | "Text" | "JPEG")[]// Return value: ["PDF", "Text", "JPEG"] (not necessarily in that order)constvalues=Enum.values(FileType);// Inferred type: ("application/pdf" | "text/plain" | "image/jpeg")[]// Return value: ["application/pdf", "text/plain", "image/jpeg"] (not necessarily in that order)

Enum.ofKeys(object)

Creates a new enum with the same keys as the provided enum or object and whose values are equal toits keys. This is most useful if for some reason it is necessary to do string comparisons againstthe keys of an enum rather than the values. For example:

constErrorColor=Enum({OK:"green",ERROR:"red"});typeErrorColor=Enum<typeofErrorColor>;constErrorLevel=Enum.ofKeys(ErrorColor);consterrorLevel=getErrorLevel();if(errorLevel===ErrorLevel.ERROR){    ...}

Motivation

Enums are useful for cleanly specifying a type that can take one of a few specific values.TypeScript users typically implement enums in one of two ways: built-inTypeScript enums or string literals, buteach of these has drawbacks.

Why not built-in enums?

Built-in enums have one big drawback. Their runtime value is a number, which is annoying duringdevelopment and makes them unsuitable for use with external APIs.

enumStatus{RUNNING,STOPPED}conststate={status:Status.RUNNING,pid:12345};console.log(state);// -> { status: 0, pid: 12345 }. What status was that again?// I hope you're not expecting other services to send you objects that look like this.

Why not string literals?

String literals make refactoring difficult. Suppose I have two enums:

typeStatus="RUNNING"|"STOPPED";typeTriathlonStage="SWIMMING"|"CYCLING"|"RUNNING";

Then if at a later stage I want to changeStatus to be"STARTED" | "STOPPED", there's no easyway to do it. I can't globally find/replace"RUNNING" to"STARTED" because it will also changethe unrelated string constants representingTriathlonStage. Instead, I have to examine everyoccurrance of the string"RUNNING" to see if it needs to change. Besides, these kinds of globalnon-semantic substitutions should make you nervous.

Another disadvantage of string literals comes when using IDE autocomplete features. It's convenientto be able to typeStatus. and have autocomplete suggestStatus.RUNNING andStatus.STOPPED,but with string literals no such suggestion is possible.

I might try to solve both problems by introducing constants for the string literals, but this hasissues as well:

// Typo on "STOPPED" not caught by anything below without additional boilerplate.typeStatus="RUNNING"|"STPOPED";// Naive attempts to define constants for these don't work, as shown below.constStatusNaive={RUNNING:"RUNNING",STOPPED:"STOPPED",};// Type error even though it shouldn't be, because StatusNaive.RUNNING has type// string which is not assignable to Status.conststatus:Status=StatusNaive.RUNNING;// Correctly defining constants is annoyingly repetitive. I shouldn't need to// write each value three times.constStatus={RUNNING:"RUNNING"as"RUNNING",STOPPED:"STOPPED"as"STOPPED",};

This library is effectively a programmatic version of these repetitive definitions. It attempts toprovide the best of both worlds: string enums with the convenience of built-in enums.

How It Works

This section is not necessary to use this library, but for those curious about how it isimplemented, read on. The explanation uses the concepts of index types and mapped types, asdescribed in TypeScript'sAdvanced Types page.

The relevant type declarations are as follows:

functionEnum<Vextendsstring>(...values: V[]):{[KinV]:K};function Enum<Textends{[_:string]:V},Vextendsstring>(definition: T): T;...type Enum<T> = T[keyof T];

We are creating a overloaded function namedEnum and a type namedEnum, so both can be importedwith a single symbol.

Consider the first overload, which handles the case of variadic arguments representing the enumvalues. In TypeScript, a string constant is a type (for example, inconst foo = "Hello", thevariablefoo is assigned type"Hello"). This means that the array

["RUNNING","STOPPED"]

can be inferred to have type("RUNNING" | "STOPPED")[], and so when it is passed into a functionwith the above type signature, the type parameterV is thus inferred to be"RUNNING" | "STOPPED". Then the return type{ [K in V]: K } is a mapped type which describes anobject whose keys are the types that make upV and for each such key has a value of the same typeas that key. Hence, the type ofEnum("RUNNING", "STOPPED") is

// This is a type, not an object literal.{    RUNNING:"RUNNING";    STOPPED:"STOPPED";}

Next, consider the second overload, which handles the case which takes an object of keys and values,and for the sake of example consider

constStatus=Enum({RUNNING:"running",STOPPED:"stopped",});

The second type parameterV is inferred as"running" | "stopped", which forces TypeScript toinfer the first type parameterT as an object whose values are the specific string values thatmake upV. Hence, even though{ RUNNING: "running", "STOPPED": "stopped" } would have type{ RUNNING: string; STOPPED: string; }, passing it throughEnum causes its type to be inferredinstead as the desired

// Type, not object literal.{    RUNNING:"running";    STOPPED:"stopped";}

Next, consider the definition

typeEnum<T>=T[keyofT];

This is an index type which describes, for a given keyed typeT, the type obtained by indexingintoT with an arbitrary one of its keys (the syntaxT[keyof T] is meant to evoke theexpressiont[key] for somekey int). When passing in an arbitrary key to the object from theprevious step, we get a value which might be any one of the object's values, and so its type is thusthe union of the types of the object's values. Hence,Enum<typeof Enum("RUNNING", "STOPPED")>evaluates to"RUNNING" | "STOPPED", which is what we want.

Acknowledgements

This libary is heavily inspired by posts inthis thread. In particular, credit goes tousers@igrayson,@nahuel,and@kourge.

Copyright © 2017 David Philipson

About

Typesafe string enums in TypeScript pre-2.4.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors2

  •  
  •  

[8]ページ先頭

©2009-2025 Movatter.jp