4

I'm trying to set up GeoFire in my Angular2 (RC5) app. I've installed geofire and firebase with npm and configured systemjs to import it. Here's my package.json:

{
  "name": "MyProject",
  "version": "0.1.0",
  "scripts": {
    "start": "tsc && concurrently \"npm run tsc:w\" \"npm run lite\" ",
    "lite": "lite-server",
    "postinstall": "typings install",
    "tsc": "tsc",
    "tsc:w": "tsc -w",
    "typings": "typings"
  },
  "license": "ISC",
  "dependencies": {
    "@angular/common": "^2.0.0-rc.5",
    "@angular/compiler": "^2.0.0-rc.5",
    "@angular/core": "^2.0.0-rc.5",
    "@angular/forms": "0.3.0",
    "@angular/http": "2.0.0-rc.5",
    "@angular/platform-browser": "^2.0.0-rc.5",
    "@angular/platform-browser-dynamic": "^2.0.0-rc.5",
    "@angular/router": "3.0.0-rc.1",
    "@angular/router-deprecated": "2.0.0-rc.2",
    "@angular/upgrade": "2.0.0-rc.5",
    "angular2-in-memory-web-api": "0.0.15",
    "bootstrap": "^3.3.6",
    "core-js": "^2.4.0",
    "firebase": "^3.3.0",
    "geofire": "^4.1.1",
    "reflect-metadata": "^0.1.3",
    "rxjs": "5.0.0-beta.6",
    "systemjs": "0.19.27",
    "zone.js": "^0.6.12"
  },
  "devDependencies": {
    "concurrently": "^2.0.0",
    "lite-server": "^2.2.0",
    "typescript": "^1.8.10",
    "typings": "^1.0.4"
  }
}

and my systemjs config file:

  var map = {
    'app':                        'app', // 'dist',
    '@angular':                   'node_modules/@angular',
    'angular2-in-memory-web-api': 'node_modules/angular2-in-memory-web-api',
    'rxjs':                       'node_modules/rxjs',
    'firebase':                   'node_modules/firebase',
    'geofire':                    'node_modules/geofire',
  };
  // packages tells the System loader how to load when no filename and/or no extension
  var packages = {
    'app':                        { main: 'main.js',  defaultExtension: 'js' },
    'rxjs':                       { defaultExtension: 'js' },
    'angular2-in-memory-web-api': { main: 'index.js', defaultExtension: 'js' },
    'firebase':                   { main: 'firebase.js', defaultExtension: 'js' },
    'geofire':                    { main: 'dist/geofire.js', defaultExtension: 'js' },
  };

Finally I try to import geofire in my app.component.ts file like so:

import * as GeoFire from "geofire";

But on running npm start to transpile and launch the server, get the following error:

➜  MyProject npm start

> MyProject@0.1.0 start /Users/max/Development/myproject
> tsc && concurrently "npm run tsc:w" "npm run lite"

app/app.component.ts(3,26): error TS2307: Cannot find module 'geofire'.

npm ERR! Darwin 15.5.0
npm ERR! argv "/usr/local/Cellar/node/5.10.1/bin/node" "/usr/local/bin/npm" "start"
npm ERR! node v5.10.1
npm ERR! npm  v3.8.3
npm ERR! code ELIFECYCLE
npm ERR! MyProject@0.1.0 start: `tsc && concurrently "npm run tsc:w" "npm run lite" `
npm ERR! Exit status 2
npm ERR!
npm ERR! Failed at the MyProject@0.1.0 start script 'tsc && concurrently "npm run tsc:w" "npm run lite" '.
npm ERR! Make sure you have the latest version of node.js and npm installed.
npm ERR! If you do, this is most likely a problem with the MyProject package,
npm ERR! not with npm itself.
npm ERR! Tell the author that this fails on your system:
npm ERR!     tsc && concurrently "npm run tsc:w" "npm run lite"
npm ERR! You can get information on how to open an issue for this project with:
npm ERR!     npm bugs MyProject
npm ERR! Or if that isn't available, you can get their info via:
npm ERR!     npm owner ls MyProject
npm ERR! There is likely additional logging output above.

npm ERR! Please include the following file with any support request:
npm ERR!     /Users/max/Development/MyProject/npm-debug.log

Here's my tsconfig:

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "removeComments": false,
    "noImplicitAny": false
  }
}

The rest of the project files are based on the angular2 quickstart found here:

https://angular.io/guide/quickstart

Does anybody have any idea how I can get tsc to find the module and successfully transpile?

Thanks. Max.

Evan Wieland
  • 1,445
  • 1
  • 20
  • 36
Max Mumford
  • 2,482
  • 5
  • 28
  • 40
  • 1
    Cross-posted: https://github.com/firebase/geofire-js/issues/116 – Frank van Puffelen Aug 20 '16 at 14:35
  • Are you running tsc from command line? That will not work with SystemJS, you have to use https://github.com/frankwallis/plugin-typescript/ and transpile at runtime (systemjs will do it for you) or using systemjs-builder. – artem Aug 20 '16 at 20:37
  • @artem the error message shows both when running tsc from the command line & when running npm start. I've added the full error message and full package.json contents to the original question. – Max Mumford Aug 21 '16 at 12:38
  • Are there typings for GeoFire? If there are, have you installed them? – cartant Aug 21 '16 at 12:48
  • Nope, no typings, but as far as I know you can use modules without typing definitions, no? – Max Mumford Aug 21 '16 at 18:53

2 Answers2

9

That quickstart tutorial is missing one important point: SystemJS and TypeScript are two separate tools, each with its own configuration.

Adding geofire to systemjs config has no effect on typescript module resolution. Upon seeing this import

import * as GeoFire from "geofire";

tsc tries to find geofire.ts in usual places where typings for javascript modules are normally found, and gives an error because it's not there.

There are three different ways to solve this problem.

In any case, geofire package configuration in systemjs.config.js must declare its format as global, like this:

'geofire': {
    main: 'dist/geofire.js', 
    defaultExtension: 'js', 
    meta: {'dist/geofire.js': {format: 'global'}} 
 },

The first solution is to create the simplest possible type declaration for geofire yourself, using ambient modules.

Create geofire.d.ts file in the app folder:

declare namespace geofire {
    function GeoFire(firebaseRef: any): void;
}

declare module 'geofire' {
    export = geofire;
}

This is sufficient for import statement to compile, and for this call to typecheck:

   var geoFire = new GeoFire(firebaseRef);

NOTE for typescript 2.0

For typescript 2.0 the code above does not work. You have to use typescript-specific import = syntax

import GeoFire = require('geofire');

and change definition in geofire.d.ts to:

declare module 'geofire' {
    function GeoFire(firebaseRef: any): void;
    export = GeoFire;
}

Yet another possible way to solve this is to use a plugin for SystemJS that uses TypeScript API to invoke typescript and hook into its module resolution, so that it takes into account SystemJS configuration. With that plugin, that import will work at runtime in the browser, but invoking tsc from the command line would still give the same error.

The third way is to import geofire using SystemJS API, not the import statement in app.component.ts:

import { Component } from '@angular/core';

declare var SystemJS : any;
var GeoFirePromise = SystemJS.import('geofire');

@Component({
  selector: 'my-app',
  template: '<h1>My First Angular 2 App</h1>'
})

export class AppComponent {

    constructor() {  // or wherever you need to use geofire

        GeoFirePromise.then(function(GeoFire: any) {
            console.log(typeof GeoFire);
        });
    }

}

Note that SystemJS.import returns a promise that will have to load geofire.js, so anywhere you want to use it you have to do

    GeoFirePromise.then(function(GeoFire: any) {

If this is inconvenient, you can just declare global GeoFire variable

declare var GeoFire: any;

and load geofire.js using script tag in your HTML, as described in this post on ionic forum. Then, you would not event need to add geofire to systemjs.config.js.

artem
  • 46,476
  • 8
  • 74
  • 78
  • Thanks for such a clear and comprehensive answer, @artem. You've helped clear up some misunderstandings I had about the two tools. I went the geofire.d.ts route as I thought it was the cleanest. Max – Max Mumford Aug 22 '16 at 09:38
  • Hi again @artem. The solution was working, until I created a firebase.d.ts script, now tsc shows this error: app/app.component.ts(37,19): error TS2351: Cannot use 'new' with an expression whose type lacks a call or construct signature -- when I try to do this: var geoFire = new GeoFire(firebaseRef);. Error persists even after removing the firebase.d.ts file. Any idea why? – Max Mumford Aug 22 '16 at 12:55
  • 1
    Sorry I don't know yet how to make it to typecheck properly. The workaround I found so far is to bypass typechecking via var X: any = GeoFire; var geofire = new X(...); – artem Aug 22 '16 at 13:40
  • Ok thanks for the workaround. In the meantime I have discovered that tsc transpiles successfully if I import with: import { GeoFire } from "geofire"; and with this geofire.d.ts: declare module 'geofire' { export class GeoFire{ ref; constructor(firebaseRef: any); } }. But at runtime I get this error: TypeError: geofire_1.GeoFire is not a constructor – Max Mumford Aug 22 '16 at 14:15
  • Yes, the only way I found so far to match the import with what's exported at runtime is to do it exactly as in the answer; you don't even need to declare anything in the module because it does not typecheck anyway. I'll update the answer again if I find anything better – artem Aug 22 '16 at 15:59
  • I think I found it, I updated the .d.ts in the answer now. No idea why, but one has to declare both module and namespace in .d.ts. – artem Aug 22 '16 at 17:34
  • Hmm I'm still getting "Cannot use 'new' with an expression whose type lacks a call or construct signature." with typescript 2.0.0. – Max Mumford Aug 22 '16 at 21:17
  • I found different syntax that works in 2.0.0 - you have to change both the import and type definition file. The upside is that syntax seems much more logical now. I updated the answer once again. – artem Aug 22 '16 at 22:36
3

Notice, I'm still not a TypeScript expert and this is my first time dabbling with type definitions and such, but after a long work day I seem to have got something that works (at least for my setup). I'm using Ionic 2 RC2, Angular2 2.1.1, Geofire 4.1.2. The build uses webpack. I created geofire.d.ts in my app directory with the following:

declare module 'geofire' {

    type EventType = 'ready' | 'key_entered' | 'key_exited' | 'key_moved';

    interface GeoQueryCriteria {
        center: number[];
        radius: number;
    }

    interface GeoQueryUpdateCriteria {
        center?: number[];
        radius?: number;
    }

    interface GeoCallbackRegistration {
        cancel();
    }

    interface GeoQuery {
        center(): number[];
        radius(): number;
        updateCriteria(criteria: GeoQueryUpdateCriteria);
        on(eventType: EventType, callback: (key:string, location: number[], distance: number) => void): GeoCallbackRegistration;
        cancel();
    }

    class GeoFire {
        constructor(ref: any);
        ref(): any;
        set(key: string, loc: number[]): Promise<void>;
        get(key: string): Promise<number[]>;
        remove(key: string): Promise<void>;
        query(criteria: GeoQueryCriteria): GeoQuery;
        static distance(location1: number[], location2: number[]);  
    }

    export = GeoFire;
}

Finally in my Service/Page I can import GeoFire like so:

import GeoFire from 'geofire';

Note the exact syntax of the import there are no curly braces {}. I tried so many ways to get it to work differently but I think the way the javascript was written, this is how it has to be done. I found ways that would get rid of compile errors, but fail at runtime ('no constructor named...'), or it would recognize the constructor in compilation but not as a type.

I only created it for the public API so if you try to extend GeoFire for some reason be aware there could be name conflicts with other methods not exposed in this definition file.

If this helps you out please let me know and I'll contribute it to DefinitelyTyped so no one else has to go through the pain I (and obviously others with 340+ views) did.

Andrew Wynham
  • 2,310
  • 21
  • 25