Categories
TypeScript Best Practices

TypeScript Best Practices — Type Assertions and Type Annotations

TypeScript is an easy to learn extension of JavaScript. It’s easy to write programs that run and does something. However, it’s hard to account for all the uses cases and write robust TypeScript code.

In this article, we’ll look at the best practices to following when writing code with TypeScript, including type assertions and having explicit type annotations.

Enforce Consistent Usage of Type Assertions

Type assertion styles should be consistent across our project.

Type assertions are also referred to as data type casting in TypeScript.

However, they’re technically different.

We can either use the as keyword or <> to add type assertions to our values.

For instance, we can either stick with:

let x = "foo" as string;

or:

let x = <string>"foo";

const is also allowed with the rule. It’s available since TypeScript 3.4 and it allows us to make something read-only.

For instance, we can write:

let x = "foo" as const;

or:

let x = <const>"foo";

Consistent Type Definitions with interface or type

In TypeScript, we can define interfaces for annotating types.

Also, we can create type alias with the type keyword to do the same thing.

It’s a good idea to be consistent with what we use when we define types.

For instance, we can either stick with interfaces:

interface Foo {
  a: string;
  b: number;
}

or type aliases:

type Foo = {
  a: string;
  b: number;
};

Require Explicit Return Types on Functions and Class Methods

It’s a good idea to add return type annotations for functions and class methods.

This way, we know what each function or method returns.

For instance, instead of writing:

function foo() {
  return 'bar';
}

We can write:

function foo: string() {
  return 'bar';
}

Likewise, with class methods, instead of writing:

class Foo{
  method() {
    return 'bar';
  }
}

We write:

class Foo{
  method(): string {
    return 'bar';
  }
}

If our method returns nothing, we use the void return type:

function foo(): void {
  return;
}

or:

class Foo{
  method(): void {
    return;
  }
}

Require Explicit Accessibility Modifiers on Class Properties and Methods

If we leave out the accessibility modifiers in classes, then it may be hard for people to understand whether a class property or method is accessible or not.

By default, if we leave them out, the class member is public.

So we may also want to restrict access to some members in most cases.

Therefore, we should include them.

So instead of writing:

class Foo {
  foo() {
    console.log("foo");
  }

  bar() {
    //...
  }
}

We may want to restrict access to some members by writing:

class Foo {
  foo() {
    console.log("foo");
  }

  private bar() {
    //...
  }
}

This way, the TypeScript compiler will give us an error if we try to compile it.

Access modifiers include public , private , readonly and protected .

A public member is available to everything.

A private member is only available within the class.

A readonly member is public but it’s read-only.

A protected member is only available within the class or any child class.

Require Explicit Return and Argument Types on Exported Functions and Classes’ Public Class Methods

To make working with functions and public methods of classes easier, we should add explicit argument and return types so that we can get autocomplete and errors when we work with them.

For instance, instead of writing:

export function test() {
  return;
}

We write:

export function test(): void {
  return;
}

And instead of writing:

class Foo {
  method() {
    return;
  }
}

We write:

export class Foo {
  method(): void {
    return;
  }
}

Likewise, with argument types, we write:

export class Foo {
  method(foo: string): string {
    return foo;
  }
}

and:

export function test(foo: string): string{
  return foo;
}

Now we don’t have to guess or look up the types of exported classes and functions.

Conclusion

We should annotate return and argument types so don’t have to look up the types of functions and class methods that are exported.

Consistent usage of type assertions notation may also be a good idea.

Also, it may be a good idea to have consistent usage of interface or type to create new types.

Categories
TypeScript Best Practices

TypeScript Best Practices — Classes, Types, and Overloads

TypeScript is an easy to learn extension of JavaScript. It’s easy to write programs that run and does something. However, it’s hard to account for all the uses cases and write robust TypeScript code.

In this article, we’ll look at the best practices to following when writing code with TypeScript, including member overloads, class instance variables, and restricting types.

Member Overloads Should be Consecutive

If we have member overloads, then they should be consecutive so that we can spot them easily.

For instance, instead of writing:

declare namespace Foo {

export function foo(s: string): void;

export function foo(n: number): void;

export function bar(): void;

export function foo(sn: string | number): void;

}

We write:

declare namespace Foo {
  export function foo(s: string): void;
  export function foo(n: number): void;
  export function foo(sn: string | number): void;
  export function bar(): void;
}

This also applies to interfaces, classes, type aliases, and exports.

We should write:

interface Foo {
  foo(s: string): void;
  foo(n: number): void;
  foo(sn: string | number): void;
  bar(): void;
}

or:

class Foo {
  foo(s: string): void;
  foo(n: number): void;
  foo(sn: string | number): void {}
  bar(): void {}
}

or:

export function foo(s: string): void;
export function foo(n: number): void;
export function foo(sn: string | number): void;
export function bar(): void;

Use [] or Array<T> for Arrays

To restrict the types of arrays, we can use T[] or Array<T> to restrict the types that an array can hold.

For instance, instead of writing:

const arr = [1, 2, 3];

We write:

const arr: Array<number> = [1, 2, 3];

or:

const arr: number[] = [1, 2, 3];

So that arr can only hold numbers.

If we want our array to be read-only, we can write:

const arr: ReadonlyArray<number> = [1, 2, 3];

No Awaiting Something that’s Not Then-able

If something doesn’t have a then method, then we shouldn’t put await before it.

Usually, await should be used for promises rather than any object that has a then method.

For instance, instead of writing:

const foo = async () => await "value";

We should write:

const foo = async () => {
  await Promise.resolve(1);
};

No Comments With Prefix @ts Should be Used

@ts is used to suppress TypeScript compiler errors.

Therefore, we probably shouldn’t use them since they may lead to errors later.

Instead of writing:

// @ts-nocheck

We don’t write any comment that starts with @ts .

Prevent Specific Types from Being Used

We may want to prevent some types to be used.

For instance, we may want to prevent String from being used instead of string .

We can do that in our tsconfig.json , by writing:

{
  "@typescript-eslint/ban-types": ["error", {
    "types": {
      "String": {
        "message": "Use string instead",
        "fixWith": "string"
      },"{}": {
        "message": "Use object instead",
        "fixWith": "object"
      }
    }
  }]
}

The config above prevents String from being used and if it’s used, the compiler will show the message ‘Use string instead’ and won’t compile.

We did the same thing with the {} type.

Class Literals Should be Exposed in a Consistent Style

We should have a consistent style when exposing class members to the outside.

There are a few styles that we can adopt.

We can use the ‘fields’ style as follows:

class Foo {
  public readonly foo = 1;
  public readonly bar = [1, 2, 3];
  private readonly ['baz'] = 'hello';

  public get qux() {
    return `qux ${foo + 1}`;
  }
}

If we have the ‘fields’ style, then we don’t have getters for our instance variables if they have read-only values.

Alternatively, we can use the ‘getters’ style by writing:

class Foo {
  public readonly foo = 1;
  public readonly bar = [1, 2, 3];
  public static get baz() {
    return 1;
  }

  private get qux() {
    return 'qux';
  }
}

We have getters for any code that isn’t defined as read-only.

We can take our pick, but it’s a good idea to be consistent.

Conclusion

We can restrict types of array entries by specifying the types that we want the array to be.

Also, we can restrict types for the whole project by changing the configuration of our project.

We can also restrict that overloads of the same function be placed together.

Finally, we can stick to one style of declaring class instance variables.

Categories
Angular TypeScript

How to Build a Barcode Scanner App That Gets Prices From eBay

eBay has a free API for accessing their listing data, and HTML has a camera API for getting images from a camera via a web page. This means that we can create apps that scan barcodes from your web app, get the code, and send it to the eBay API for querying.

In this story, we will build a PHP app for querying the eBay API, and then we will build a web app for getting the barcode and sending it to our API. The back end is simple. It is just a script for getting data from the eBay API via the ISBN code. Create a folder for the back-end app and put in the following:

<?php
use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentHttpFoundationResponse;
use GuzzleHttpClient;

$ebay = $app['controllers_factory'];
$ebayAppId = $_ENV['EBAY_APP_ID'];
$client = new Client([
    'base_uri' => '[http://svcs.ebay.com'](http://svcs.ebay.com%27)
]);

$ebay->get('/find-by-code/{code}/{page}', function ($code, $page) use ($app, $client, $ebayAppId) {
    if (strlen($code) == 10 || strlen($code) == 13){
        $type = 'ISBN';
    }
    else if (strlen($code) == 12){
        $type = 'UPC';
    }
    else{
        return $app->json(['error' => 'invalid code'], 400);
    }

    if (!is_int(intval($page)) || $page <= 0){
        return $app->json(['error' => 'invalid page'], 400);
    }

$response = $client->request('GET', "/services/search/FindingService/v1?OPERATION-NAME=findItemsByProduct&SERVICE-VERSION=1.0.0&SECURITY-APPNAME=$ebayAppId&RESPONSE-DATA-FORMAT=JSON&REST-PAYLOAD&paginationInput.entriesPerPage=10&productId.[@type](http://twitter.com/type "Twitter profile for @type")=$type&productId=$code&paginationInput.pageNumber=$page");
    return $app->json(json_decode($response->getBody(), true));
});

return $ebay;

We call that ebay.php. The function ($code, $page) use ($app, $client, $ebayAppId) part allows the route to access outside variables since the callback is in a different scope that the outside variables.

Then in index.php, we put

<?php
require_once 'vendor/autoload.php';

$app = new SilexApplication();
$dotenv = new DotenvDotenv('.');
$dotenv->load();
$app->register(new JDesrosiersSilexProviderCorsServiceProvider(), [
    "cors.allowOrigin" => "*",
]);

$app['debug']= true;

$app->get('/hello/{name}', function($name) use($app) {
    return 'Hello '.$app->escape($name);
});

$app->mount('/ebay', include 'ebay.php');
$app["cors-enabled"]($app);
$app->run();

so that we can access our routes.

In composer.json, we put

{
    "require": {
        "silex/silex": "^2.0",
        "vlucas/phpdotenv": "^2.4",
        "jdesrosiers/silex-cors-provider": "~1.0",
        "guzzlehttp/guzzle": "~6.0"
    }
}

so we can run composer install to install our dependencies if Composer is installed.

Now that we have the back end done. We can do the front end. The app is going to be built with Angular. We scaffold the app with the Angular CLI. We run ng new frontend to scaffold the app.

The only things different from our typical apps is that HTTPS is required to access the camera, so we have to generate our own HTTPS certificate for our development server. We should have theserver.crt and server.key file in the same folder as the front-end app files.

The serve section of our angular.json should have:

"serve": {
  "builder": "[@angular](http://twitter.com/angular "Twitter profile for @angular")-devkit/build-angular:dev-server",
  "options": {
    "browserTarget": "frontend:build",
    "sslKey": "server.key",
    "sslCert": "server.cert"
  },
  "configurations": {
    "production": {
      "browserTarget": "frontend:build:production"
    },
    "mobile": {
      "browserTarget": "frontend:build:mobile"
    }
  }
},

where

"sslKey": "server.key",
"sslCert": "server.cert"

should be referencing the path of your certificates.

Then to run the Angular CLI development server, we run:

ng serve --ssl

When you go to https://localhost:4200, you will see an insecure connection error in most browsers. Click proceed to continue.

If your camera is on your Android device, we can debug remotely. In Chrome, press F12 to open the console, click on the top-right menu with the three vertical dots. Then click Remote Devices, connect your Android device to your computer, and enable remote debugging according to the instructions.

Instead of running ng serve --ssl, you run ng serve --ssl --host 0.0.0.0

Once all that is done, you should see the following:

once the app is built.

We install a library for accessing the device’s camera and our flux store by running:

npm i @zxing/ngx-scanner @ngrx/store @angular/material @angular/cdk

In app.module.ts, we put:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { ZXingScannerModule } from '@zxing/ngx-scanner';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatInputModule } from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button';
import { MatTabsModule } from '@angular/material/tabs';
import { MatCardModule } from '@angular/material/card';
import { HttpClientModule } from '@angular/common/http';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatTableModule } from '@angular/material/table';
import { MatSelectModule } from '@angular/material/select';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HomePageComponent } from './home-page/home-page.component';
import { StoreModule } from '[@ngrx/store](http://twitter.com/ngrx/store "Twitter profile for @ngrx/store")';
import { barCodeReducer } from './bar-code-reducer';
import { FormsModule } from '@angular/forms';
import { EbayTabComponent } from './ebay-tab/ebay-tab.component';

@NgModule({
  declarations: [
    AppComponent,
    HomePageComponent,
    EbayTabComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    ZXingScannerModule,
    BrowserAnimationsModule,
    MatButtonModule,
    MatToolbarModule,
    MatInputModule,
    MatTabsModule,
    StoreModule.forRoot({ barCode: barCodeReducer }),
    FormsModule,
    MatCardModule,
    HttpClientModule,
    MatPaginatorModule,
    MatTableModule,
    MatSelectModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

This incorporates the Angular Material components and our flux store.

Now we have to make a centralized store for our data. We create a file called bar-code-reducer.ts and add the following:

export const SET_BARCODE = 'SET_BARCODE';

export function barCodeReducer(state: string = '', action) {
    switch (action.type) {
        case SET_BARCODE:
            return action.payload;
        default:
            return state;
    }
}

Now we can add our front-end components. We run:

ng g component ebayTab
ng g component homePage

This adds the page to display our barcode scanner and a section to display our eBay data.

Next, we create a service to create our HTTP request by running:

ng g service productSearch

After that, we should have produce-search.service.ts. We put the following in there:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from 'src/environments/environment';

@Injectable({
  providedIn: 'root'
})
export class ProductSearchService {

  constructor(
    private http: HttpClient
  ) { }

  searchProduct(barcode: string, currentPage: number) {
    return this.http.get(`${environment.apiUrl}/ebay/find-by-code/${barcode}/${currentPage}`)
  }
}

In ebay-tab.component.ts, we put:

import { Component, OnInit } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { Observable } from 'rxjs';
import { ProductSearchService } from '../product-search.service';
import { ConstantPool } from '@angular/compiler';

@Component({
  selector: 'app-ebay-tab',
  templateUrl: './ebay-tab.component.html',
  styleUrls: ['./ebay-tab.component.css']
})
export class EbayTabComponent implements OnInit {

barcode$: Observable<string>;
  barcodeValue: string;
  products: any[] = [];
  totalPages: number = 0;
  totalEntries: number = 0;
  entriesPerPage: number = 0;
  currentPage: number = 1;
  displayedColumns: string[] = [
    'itemId',
    'title',
    'location',
    'country',
    'shippingServiceCost',
    'currentPrice',
    'convertedCurrentPrice',
    'bestOfferEnabled',
    'buyItNowAvailable',
    'listingType'
  ];

constructor(
    private store: Store<any>,
    private productSearchService: ProductSearchService
  ) {
    this.barcode$ = store.pipe(select('barCode'))
    this.barcode$.subscribe(barcode => {
      this.barcodeValue = barcode;
      this.products = [];
      this.searchProduct(this.barcodeValue, this.currentPage);
    }, err => {

})
  }

  ngOnInit() {
  }

  searchProduct(barcodeValue, currentPage) {
    this.productSearchService.searchProduct(barcodeValue, currentPage)
      .subscribe((res: any) => {
        try {
          this.products = res.findItemsByProductResponse[0].searchResult[0].item as any[];
          this.products = this.products.map(p => {
            let shippingServiceCost = p.shippingInfo[0].shippingServiceCost;
            let sellingStatus = p.sellingStatus;
            return {
              itemId: p.itemId,
              title: p.title,
              country: p.country,
              location: p.location,
              shippingServiceCost: Array.isArray(shippingServiceCost) ? `${shippingServiceCost[0]['__value__']} ${shippingServiceCost[0]['@currencyId`]} : '',
              currentPrice: Array.isArray(sellingStatus) ? `${sellingStatus[0].currentPrice[0]['__value__']} ${sellingStatus[0].currentPrice[0]['@currencyId']}` : '',
              convertedCurrentPrice: Array.isArray(sellingStatus) ? `${sellingStatus[0].convertedCurrentPrice[0]['__value__']} ${sellingStatus[0].convertedCurrentPrice[0]['@currencyId']}` : '',
              bestOfferEnabled: p.listingInfo[0].bestOfferEnabled[0],
              buyItNowAvailable: p.listingInfo[0].buyItNowAvailable[0],
              listingType: p.listingInfo[0].listingType[0]
            }
          })
          this.totalPages = res.findItemsByProductResponse[0].paginationOutput[0].totalPages[0];
          this.totalEntries = res.findItemsByProductResponse[0].paginationOutput[0].totalEntries[0];
          this.entriesPerPage = res.findItemsByProductResponse[0].paginationOutput[0].entriesPerPage[0];
        }
        catch (ex) {
          this.products = [];
        }
      }, err => {
        this.products = [];
      })
  }

getProducts(event) {
    this.currentPage = event.pageIndex + 1;
    this.searchProduct(this.barcodeValue, this.currentPage);
  }
}

And in ebay-tab.component.html, we have:

<div *ngIf='products.length > 0'>
  <table mat-table [dataSource]="products" class="mat-elevation-z8">
    <ng-container matColumnDef="itemId">
      <th mat-header-cell *matHeaderCellDef> Item ID </th>
      <td mat-cell *matCellDef="let element"> {{element.itemId}} </td>
    </ng-container>

    <ng-container matColumnDef="title">
      <th mat-header-cell *matHeaderCellDef> Title </th>
      <td mat-cell *matCellDef="let element"> {{element.title}} </td>
    </ng-container>

    <ng-container matColumnDef="location">
      <th mat-header-cell *matHeaderCellDef> Location </th>
      <td mat-cell *matCellDef="let element"> {{element.location}} </td>
    </ng-container>

    <ng-container matColumnDef="country">
      <th mat-header-cell *matHeaderCellDef> Country </th>
      <td mat-cell *matCellDef="let element"> {{element.country}} </td>
    </ng-container>

    <ng-container matColumnDef="shippingServiceCost">
      <th mat-header-cell *matHeaderCellDef> Shipping Cost </th>
      <td mat-cell *matCellDef="let element"> {{element.shippingServiceCost}} </td>
    </ng-container>

    <ng-container matColumnDef="currentPrice">
      <th mat-header-cell *matHeaderCellDef> Current Price </th>
      <td mat-cell *matCellDef="let element"> {{element.currentPrice}} </td>
    </ng-container>

    <ng-container matColumnDef="convertedCurrentPrice">
      <th mat-header-cell *matHeaderCellDef> Converted Current Price </th>
      <td mat-cell *matCellDef="let element"> {{element.convertedCurrentPrice}} </td>
    </ng-container>

    <ng-container matColumnDef="bestOfferEnabled">
      <th mat-header-cell *matHeaderCellDef> Best Offer Enabled </th>
      <td mat-cell *matCellDef="let element"> {{element.bestOfferEnabled}} </td>
    </ng-container>

    <ng-container matColumnDef="buyItNowAvailable">
      <th mat-header-cell *matHeaderCellDef> Buy It Now </th>
      <td mat-cell *matCellDef="let element"> {{element.buyItNowAvailable}} </td>
    </ng-container>

    <ng-container matColumnDef="listingType">
      <th mat-header-cell *matHeaderCellDef> Listing Type </th>
      <td mat-cell *matCellDef="let element"> {{element.listingType}} </td>
    </ng-container>

    <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
    <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
  </table>

<mat-paginator [length]="totalEntries" [pageSize]="entriesPerPage" (page)='getProducts($event)'>
  </mat-paginator>
</div>
<div *ngIf='products.length == 0' class="center">
  <h1>No Results</h1>
</div>

In home-page.component.ts, we put:

import { Component, OnInit, ViewChild } from '@angular/cor';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { SET_BARCODE } from '../bar-code-reducer';
import { NgForm } from '@angular/forms")';
import { BarcodeFormat } from '@zxing/library';
import { ZXingScannerComponent } from '@zxing/ngx-scanner';

@Component({
  selector: 'app-home-page',
  templateUrl: './home-page.component.html',
  styleUrls: ['./home-page.component.css']
})
export class HomePageComponent implements OnInit {

  barcodeValue: number;
  webCamAvailable: boolean = true;
  barcode$: Observable<string>;
  searching: boolean = false;
  allowedFormats = [
    BarcodeFormat.QR_CODE,
    BarcodeFormat.EAN_13,
    BarcodeFormat.CODE_128,
    BarcodeFormat.DATA_MATRIX,
    BarcodeFormat.UPC_A,
    BarcodeFormat.UPC_E,
    BarcodeFormat.UPC_EAN_EXTENSION,
    BarcodeFormat.CODABAR,
    BarcodeFormat.CODE_39,
    BarcodeFormat.CODE_93
  ];
  hasCameras = false;
  hasPermission: boolean;
  qrResultString: string;

  availableDevices: MediaDeviceInfo[];
  selectedDevice: MediaDeviceInfo;
  [@ViewChild](http://twitter.com/ViewChild "Twitter profile for @ViewChild")('scanner')
  scanner: ZXingScannerComponent;

  constructor(
    private store: Store<any>
  ) {

  }

  ngOnInit() {
    this.scanner.camerasFound.subscribe((devices: MediaDeviceInfo[]) => {
      this.hasCameras = true;
      this.availableDevices = devices;

});

    this.scanner.permissionResponse.subscribe((answer: boolean) => {
      this.hasPermission = answer;
    });
  }

  onValueChanges(result) {
    this.barcodeValue = result.codeResult.code;
    this.searching = true;
    this.store.dispatch({ type: SET_BARCODE, payload: this.barcodeValue });
  }

  searchProduct(barCodeForm: NgForm) {
    this.searching = false;
    if (barCodeForm.invalid) {
      return;
    }
    this.searching = true;
    this.store.dispatch({ type: SET_BARCODE, payload: this.barcodeValue });
  }

  scanSuccessHandler(event) {
    console.log(event);
    this.barcodeValue = event;
    this.store.dispatch({ type: SET_BARCODE, payload: this.barcodeValue });
  }

  onDeviceSelectChange(selectedValue: string) {
    this.selectedDevice = this.scanner.getDeviceById(selectedValue);
  }

  scanErrorHandler(event) {
    console.log(event);
  }

  scanFailureHandler(event) {
    console.log(event);
  }

  scanCompleteHandler(event) {
    console.log(event);
    this.barcodeValue = event.text;
    this.store.dispatch({ type: SET_BARCODE, payload: this.barcodeValue });
  }
}

In addition to scanning, you can also enter the barcode manually. If a camera is present, you should see a preview box. After scanning, we get the barcode value and propagate that to the ebay-tab component that we created via the flux store.

Categories
TypeScript

Introduction to JavaScript Inheritance

JavaScript is an object-oriented language. However, it’s different from many other OO languages in that it uses prototype-based inheritance instead of class-based inheritance.

Prototype-based inheritance means that objects inherit items from its prototype. A prototype is just another object, which can be inherited by other objects.

This is different from class-based inheritance in that classes are templates for creating new objects. Classes can inherit from other classes to reuse code from the class it’s inheriting from.

Old Syntax of Inheritance

Constructor Functions

Before ES6, we only have constructor functions to serve as templates to create new objects which are instances of the constructor.

For example, we can define a constructor function as follows:

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Then we can create a new instance of Person by writing:

let person = new Person('Joe', 10);

To inherit items from other constructor functions in a constructor function, we have to call the parent constructor function that we want to inherit from with the call method, and then set our constructor’s prototype’s constructor property to the parent constructor function that we want to inherit from.

For example, if we want a Employee constructor function to inherit the properties of the Person constructor, we can write:

function Person(name, age) {
  this.name = name;
  this.age = age;
}

function Employee(name, age, title) {
  this.title = title;
  Person.call(this, name, age);
  this.__proto__.constructor = Person;
}

let employee = new Employee('Joe', 20, 'waiter');
console.log(employee);

The call method takes the value of this we want to set, and the rest are arguments we pass into the function that the call method is called on.

If we look at the __proto__ property of the employee object, which has the prototype for it, we should get that __proto__.constructor of it should be the Person constructor like we set it to.

The properties and the values of the employee object should be what we passed into the Employee constructor when we called it.

Object.create()

The Object.create() method is another way to inherit from a prototype when we create an object.

The argument that it takes is the prototype object that we want the object returned from it to inherit from.

For example, we can use Object.create to create an object with a prototype as follows:

const person = {
  name: 'Joe',
  age: 20
}

let employee = Object.create(person);
employee.title = 'waiter';

console.log(employee);

If we look at the employee object, we’ll see that the __proto__ property will have the age and name properties set with values.

Setting the proto Property Directly

Setting the __proto__ property directly has been officially supported since ES6 and it’s an undocumented way to set the prototype of an object in various browsers before it like Firefox.

We can set an object to the __proto__ property directly, by writing something like:

const person = {
  name: 'Joe',
  age: 20
}

let employee = {
  title: 'waiter'
};

employee.__proto__ = person;
console.log(employee);

We should get the exact structure of the properties and values as we did when we created an object with the Object.create() method.

One thing we have to be careful about is that we don’t want to accidentally set it if we don’t want to change an object’s prototype. This may happen if we use JavaScript objects as maps. With ES6, we can use the Map object for this purpose.

Object.defineProperty

We can also use the defineProperty method to set the prototype of an object. For example, we can write:

const person = {
  name: 'Joe',
  age: 20
}

let employee = {
  title: 'waiter'
};

Object.defineProperty(employee, '__proto__', {
  value: person
});
console.log(employee.__proto__);

When we log the value of employee.__proto__ , we get back the person object.

Note that the prototype is in the value property of the 3rd argument of the defineProperty method call.

Photo by Chiara Daneluzzi on Unsplash

New Class Syntax

With the release of ES6, the new class syntax is introduced. On the surface, it looks like we have class-based inheritance, but underneath the surface, it’s exactly the same as before.

The class syntax is the same as constructor functions. For example,

function Person(name, age) {
  this.name = name;
  this.age = age;
}

is the same as:

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

We can instantiate both by writing:

const person = new Person('Joe', 10);

And we get the same object when we inspect its properties.

The class syntax also creates a clear and convenient way to do inheritance that looks like a traditional class-based inheritance.

We can create a super-class and a child class can inherit from it with the extends keyword. For example, we can write:

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

class Employee extends Person {
  constructor(name, age, title) {
    super(name, age);
    this.title = title;
  }
}

const employee = new Employee('Joe', 20, 'waiter');

In the code above, we have the extends keyword to indicate which class Employee inherits from. We can only inherit from one class.

The super method is called to call the parent constructor and set its’ properties. In this case, calling super will call the constructor method in the Person class.

this refers to the class that it’s inside in each class.

This is exactly the same as what we did before:

function Person(name, age) {
  this.name = name;
  this.age = age;
}

function Employee(name, age, title) {
  this.title = title;
  Person.call(this, name, age);
  this.__proto__.constructor = Person;
}

let employee = new Employee('Joe', 20, 'waiter');
console.log(employee);

The only thing is that when we inspect the employee object, we get that the __proto__.constructor property shows class instead of function .

The class syntax makes inheritance much more clear than before. It’s much needed syntactic sugar for the prototypical inheritance model that’s in JavaScript since the beginning.

Also, with the class syntax, we don’t have to call the call method on the parent constructor object and set this.__proto__.constructor anymore.

It’s better than using the Object.create() or setting the __proto__ property directly. Setting the __proto__ property has its problems like accidentally setting the wrong prototype.

Categories
TypeScript

Introduction to TypeScript Enums — Const and Ambient Enums

If we want to define constants in JavaScript, we can use the const keyword. With TypeScript, we have another way to define a set of constants called enums. Enums let us define a list of named constants. It’s handy for defining an entity that can take on a few possible values. In this article, we’ll continue from Part 1 and look at union enums and enum member types, how enums are evaluated at run-time, const enums, and ambient enums.

Union Enums and Enum Member Types

A subset of enum members can act as data types of variables and class members in TypeScript. We can use literal enum members, which are enum members with no specific values assigned to them for annotating data types of our variables and class members. If an enum member has no string literal, numeric literal or a numeric literal with a minus sign before it, then we can use them as data types for other members. For example, we can use them as we do in the following code:

enum Fruit {
  Orange,
  Apple,
  Grape
}

interface OrangeInterface {
  kind: Fruit.Orange;
  color: string;
}

interface AppleInterface {
  kind: Fruit.Apple;
  color: string;
}

class Orange implements OrangeInterface {
  kind: Fruit.Orange = Fruit.Orange;
  color: string = 'orange';
}

class Apple implements AppleInterface{
  kind: Fruit.Apple = Fruit.Apple;
  color: string = 'red';
}

let orange: Orange = new Orange();
let Apple: Orange = new Apple();

In the code above, we used our Fruit enum to annotate the type of the kind field in our OrangeInterface and AppleInterface . We set it so that we can only assign Fruit.Orange to the kind field of the OrangeInterface and the class Orange which implements the OrangeInterface . Likewise, we set the kind field of AppleInterface to the type Fruit.Apple so that we can only assign the kind field to the value of the instances of the Apple class. This way, we can use the kind field as a constant field even though we can’t use the const keyword before a class field.

If we log the values of orange and apple above, we get that orange is:

{kind: 0, color: "orange"}

and apple has the value:

{kind: 1, color: "red"}

When we use enums in if statements, the TypeScript compiler will check that if the enum members are used in a valid way. For example, it’ll prevent us from writing expressions that use enums that always evaluate to true or false . For example, if we write:

enum Fruit {
  Orange,
  Apple,
  Grape
}

function f(x: Fruit) {
  if (
   x !== Fruit.Orange ||
   x !== Fruit.Apple ||
   x !== Fruit.Grape
  ) {

  }
}

Then we get the error message “This condition will always return ‘true’ since the types ‘Fruit.Orange’ and ‘Fruit.Apple’ have no overlap.(2367)“ since at least one of them is always true , so the expression:

x !== Fruit.Orange ||
x !== Fruit.Apple ||
x !== Fruit.Grape

will always evaluate to true . This is because if x can only be of type Fruit , and if x isn’t Fruit.Orange , then it’s either Fruit.Apple or Fruit.Grape , so either of them must be true .

This also means that the enum type itself is the union of each member, since each member can be used as a type. If a data type has the enum as the type, then it must always have one of the members in it as the actual type.

How Enums are Evaluated at Runtime

Enums are converted to real objects when they’re compiled by the TypeScript compiler, so they’re always treated like objects at runtime. This means that if we have an enum, then we can use its member names as property names of an enum object when we need to pass it in as a parameter with it. For example, if we have the following code:

enum Fruit {
  Orange,
  Apple,
  Grape
}

function f(fruit: { Orange: number }) {
  return fruit.Orange;
}
console.log(f(Fruit));

Then we get 0 from the console.log output from the code in the last line since we logged the value of fruit.Orange , which is 0 since we didn’t initialize it to any value. Likewise, we can use the same syntax for the destructuring assignment of an enum like we do in the following code:

enum Fruit {
  Orange,
  Apple,
  Grape
}

let { Orange }: { Orange: number } = Fruit;
console.log(Orange);

In the code above, we treat the Orange member inside the Fruit enum as another property of an object, so we can use it to assign it to a new variable with destructuring assignment like we did above. So if we log Orange like we did on the last line of the code snippet above, then we get 0 again. Also, we can use destructuring assignment to assign it to a variable with a name that’s not the same as the property name like we do in the following code:

let { Orange: orange }: { Orange: number } = Fruit;
console.log(orange);

Then we should get 0 again from the console.log statement on the last line of the code above.

Photo by Gary Bendig on Unsplash

Enums at Compile Time

The only exception to the rule that enums are treated as objects if when we use the keyof keyword with enums. The keyof keyword doesn’t work like typical objects. For example if we have:

let fruit: keyof Fruit;

Then the TypeScript compiler expects that we assign strings with number methods to it. For example, if we try to assign something like a 'Orange' to the expression above, we get the following error:

Type '"Orange"' is not assignable to type '"toString" | "toFixed" | "toExponential" | "toPrecision" | "valueOf" | "toLocaleString"'.(2322)

This isn’t what expect from the typical usage of the keyof keyword since for normal objects, it’s supposed to let us assign the property names of the keys of an object that comes after the keyof keyword. To make TypeScript let us assign 'Orange' , 'Apple' or 'Grape' to it, we can use the typof keyword after the keyof keyword like we do in the following code:

enum Fruit {
  Orange,
  Apple,
  Grape
}

let fruit: keyof typeof Fruit = 'Orange';

The code above would be accepted by the TypeScript compiler and runs because this is what makes TypeScript treats our enum members’ names as key names of an object.

Reverse Mappings

Numeric enums in TypeScript can be mapped from enum values to enum names. We can get an enum member’s name by its value by getting it by the values that are assigned to it. For example, if we have the following enum:

enum Fruit {
  Orange,
  Apple,
  Grape
}

Then we get can the string 'Orange' by getting it by its index like we do with the following code:

console.log(Fruit[0]);

The code above should log 'Orange' since the value of the member Orange is 0 by since we didn’t assign any specific value to it. We can also access it by using the member constant inside the brackets like the following code:

console.log(Fruit[Fruit.Orange]);

Since Fruit.Orange has the value 0, they’re equivalent.

Const Enums

We can add the const keyword before the enum definition to prevent it from being included in the compiled code that’s generated by the TypeScript compiler. This is possible since enums are just JavaScript objects after it’s compiled. For this reason, the values of the enum members can’t be dynamically generated, but they can be computed from other constant values. For example, we can write the following code:

const enum Fruit {
  Orange,
  Apple,
  Grape = Apple + 1
}

let fruits = [
  Fruit.Orange,
  Fruit.Apple,
  Fruit.Grape
]

Then when our code is compiled into ES5, we get:

"use strict";
let fruits = [
    0 /* Orange */,
    1 /* Apple */,
    2 /* Grape */
];

Ambient Enums

To reference an enum that exists somewhere else in the code, we can use the declare keyword before the enum definition to denote that. Ambient enums can’t have values assigned to any members and they won’t be included in compiled code since they’re supposed to reference enums that are defined somewhere else. For example, we can write:

declare enum Fruit {
  Orange,
  Apple,
  Grape
}

If we try to reference an ambient enum that’s not defined anywhere, we’ll get a run-time error since no lookup object is included in the compiled code.

Enum members can act as data types for variables, class members, and any other things that can be typed with TypeScript. An enum itself can also be a data type for these things. Therefore, anything typed with the enum type is a union type of all the member enum types. Enums are included or not depending on what keywords we use before the enum. If they’re defined with const or declare , then they won’t be included in the compiled code. Enums are just objects when converted to JavaScript and the members are converted to properties when compiled to JavaScript. This means that we can use member names as property names of objects in TypeScript.