- Notifications
You must be signed in to change notification settings - Fork3
Typesafe string enums in TypeScript pre-2.4.
License
dphilipson/typescript-string-enums
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
Typesafe string enums in TypeScript.
npm install --save typescript-string-enumsThis 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.
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({/** * Everything is fine. * * Hovering over Status.RUNNING in an IDE will show this comment. */RUNNING:"running",/** * All is lost. */STOPPED:"stopped",});exporttypeStatus=Enum<typeofStatus>;console.log(Status.RUNNING);// -> "running"
Several helper functions are provided. First areEnum.keys() andEnum.values(), which resembleObject.keys() andObject.values() but provide strict typing in their return type:
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)
Also available isEnum.isType(), which checks if a value is of a given enum type and can be usedas a type guard.
constColor=Enum({BLACK:"black",WHITE:"white",});typeColor=Enum<typeofColor>;letselectedColor:Color;constcolorString=getUserInputString();// Unsanitized string.if(Enum.isType(Color,colorString)){// Allowed because within type guard.selectedColor=colorString;}else{thrownewError(`${colorString} is not a valid color`);}
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.
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.
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.
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 appears with current IDEs.
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.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.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.
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 same 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.
By contrast, the type definition for the case which takes an object of keys and values is
functionEnum<Textends{[_:string]:V},Vextendsstring>(definition: T): T
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
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Packages0
Uh oh!
There was an error while loading.Please reload this page.
Contributors2
Uh oh!
There was an error while loading.Please reload this page.