Node.jsをTypeScriptで書く

typescriptnode.js

公式のTypeScript-Node-Starterから始めてもいいが、依存が少し余分なので一から作ることにした。

コードはここ

$ yarn add --dev typescript tslint tslint-microsoft-contrib jest ts-jest @types/jest

package.json

scriptsとテストフレームワークJestの設定を追加。

{
  "devDependencies": {
    ...
    "typescript": "^2.4.2"
  },
  "scripts": {
    "start": "npm run build && node dist/app.js",
    "build": "npm run lint && tsc",
    "test": "jest --forceExit",
    "lint": "tslint -c tslint.json -p tsconfig.json --type-check"
  },
  "jest": {
    "transform": {
      "^.+\\.ts$": "./node_modules/ts-jest/preprocessor.js"
    },
    "testRegex": "/test/.*\\.test\\.(ts|js)$",
    "moduleFileExtensions": [
      "ts",
      "js"
    ],
    "testEnvironment": "node"
  }
}

tsconfig.json

公式のそのまま。

{
    "compilerOptions": {
        "module": "commonjs",
        "target": "es6",
        "noImplicitAny": true,
        "moduleResolution": "node",
        "sourceMap": true,
        "outDir": "dist",
        "baseUrl": ".",
        "paths": {
            "*": [
                "node_modules/*",
                "src/types/*"
            ]
        }
    },
    "include": [
        "src/**/*"
    ]
}

tslint.json

MSでも使われているらしいルールを使うことにする。 結構厳しくて console.log() なんかもエラーになるので必要に応じてruleを追加する。

{
    "extends": "tslint-microsoft-contrib",
    "rules": {
        "no-console": [""],
        "no-relative-imports": false,
        "no-http-string": false,
        "no-backbone-get-set-outside-model": false
    }
}

使うパッケージをインストール

本体と型。

以前は型ファイルを持ってくるのにtsdとかtypingsが使われていたが 今はDefinelyTypedの内容が npmの@types/~に上がるようになった。

$ yarn add express
$ yarn add --dev @types/express

コードを書く

VSCodeだったらtslintプラグインがあるので入れる。tsとtslintをglobal installする必要がある。

import * as express from 'express';

/**
 * GET /echo
 * Return a string same as "say" query param.
 */
export function echoApi(req: express.Request, res: express.Response): void {

    const query: { say: string } = <{ say: string }> req.query;
    if (query.say === undefined) {
        res.send(echo(query.say));
    } else {
        res.status(400).send('"say" query param is required');
    }
}

/**
 * return a string same as input
 * @param say input (= output)
 */
export function echo(say: string): string {
    return say;
}

テストを書く

superagentを使って HTTPサーバーのテストを行うsupertestを使う。

$ yarn add --dev supertest @types/supertest
import * as supertest from 'supertest';
import { app } from '../src/app';
import { echo } from '../src/echo';

let request: supertest.SuperTest<supertest.Test>;
beforeAll(() => {
  request = supertest(app);
});

/**
 * integration test
 */
describe('GET /echo', () => {
  it('should return a string same as "say" query param', (): {} => {
    const say: string = 'Aa 1あ';

    return request
    .get('/echo')
    .query({ say: say })
    .expect(200, say);
  });

  it('is bad request that "say" query param is not given', (): {} => {
    return request
    .get('/echo')
    .expect(400);
  });
});

/**
 * unit test
 */
describe('echo', () => {
  it('should return a string same as input', () => {
    const say: string = 'Aa 1あ';
    expect(echo(say)).toBe(say);
  });
});

requestしたのをreturnするのを忘れるとテストが無条件で通ってしまうので注意。

$ npm test
...
 PASS  test/echo.test.ts
  GET /echo
    ✓ should return a string same as "say" query param (34ms)
    ✓ is bad request that "say" query param is not given (4ms)
  echo
    ✓ should return a string same as input (1ms)

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        1.601s, estimated 2s
Ran all test suites.