Thursday, January 12, 2017

TypeScript + enum + node that works

If you have an internal module in TypeScript and use it on node, and wanted to add an enum on it:

task.ts:
module com.anicehumble
{
   export enum Status 
   {
       PENDING = 0,
       IN_PROGRESS = 1,
       COMPLETED = 2
   }
   
   export interface Task
   {
       uuid: com.anicehumble.uuid;
       ownedBy: string;
       title: string;
       status: Status;
   }
}


Then the reading the value of enum would result to runtime error: 'com is not defined':
console.log(com.anicehumble.Status.IN_PROGRESS);


If we try to move the enum Status and make it node-compatible by putting it on separate file without the module namespace and exporting the enum, the task.ts will be interpreted as external module instead, and as such, all dotted names will be interpreted as external modules too and would result to compile-time error if they are not imported, e.g., TypeScript will complain that it has no exported member uuid on com.anicehumble even there is one defined on an internal module, as all names under com.anicehumble will now be interpreted as external modules instead.

When a module become an external one like the code below, all members (e.g., uuid) under the module com.anicehumble should be moved to task.ts and cannot be defined anymore in an internal module on a separate file; or if it will not be inlined in task.ts, uuid should be implemented as external module, and it should be imported like the importing of Enums from the-enums.ts below:


the-enums.ts:
export enum Status
{
    PENDING     = 0,
    IN_PROGRESS = 1,
    COMPLETED   = 2
}


task.ts:
import * as Enums from './the-enums';

module com.anicehumble
{   
   export interface Task
   {
       uuid: com.anicehumble.uuid; // TypeScript will complain com.anicehumble 'has no exported member uuid' even it is defined in an internal module on a separate file.
       ownedBy: string;
       title: string;
       status: Enums.Status;
   }
}


To maintain the internal-ness of a TypeScript module, we will just simulate the enum, and then for the enum-using node, it will just import the external module version of the enum.


So let's define the task's status enum and make it an external module version of an enum instead. You might be wondering why we need to use const instead of enum. Later, the advantage of using const over enum will be apparent.


tasks-status-enum.ts
export const PENDING  = 0;
export const IN_PROGRESS = 1;
export const COMPLETED = 2;


task.ts:
module com.anicehumble
{
   export type Status = 0 | 1 | 2;
   
   export interface Task
   {
       uuid: com.anicehumble.uuid;
       ownedBy: string;
       title: string;
       status: Status;
   }
}


The internal module version of the enum is the export type Status = 0 | 1 | 2. We will not be directly using the numbers in node, as it will look like magic numbers, we just add that to constraint the values that can be assigned to Task's status. The code below uses magic number:

const t : com.anicehumble.Task = <any>{};
t.status = 2; // http://stackoverflow.com/questions/47882/what-is-a-magic-number-and-why-is-it-bad
    

For enum-using node, this is how enum will be used:

app.ts:
import * as TaskStatus from './task-status-enum';

const t : com.anicehumble.Task = <any>{};

t.status = TaskStatus.IN_PROGRESS;

console.log(TaskStatus.IN_PROGRESS); // no more undefined error

And this will not work, as the status value is constrained to 0, 1, 2 values only:

t.status = 42; // compile error: 42 is not assignable to type Status

This will not compile as type Status and number are not type-compatible:

const n: number = 42;

t.status = n; // compile error: number is not assignable to type Status

and so is this, even if the value of 2 of n is in the allowable values of Status:

const n: number = 2;

t.status = n; // compile error: number is not assignable to type Status

And now for the interesting part, we can make things more discoverable if we will make the simulated enum have a type of com.anicehumble.Status:


tasks-status-enum.ts
export const PENDING : com.anicehumble.Status    = 0;
export const IN_PROGRESS: com.anicehumble.Status = 1;
export const COMPLETED: com.anicehumble.Status   = 2;

Adding the type to the constant, that external module version of simulated enum is now related to the internal module version of the enum (i.e., export type Status = 0 | 1 | 2)

With real enum, the type cannot be added, the following is invalid:

export enum Status
{
    PENDING : com.anicehumble.Status    = 0,
    IN_PROGRESS: com.anicehumble.Status = 1,
    COMPLETED: com.anicehumble.Status   = 2
}


Update

Had I known TypeScript's const enum from the start, this post will not be written. Use const enum, it's better than what was suggested here.



Happy Coding!

No comments:

Post a Comment