Node.jsはビルドしない主義
僕はスクリプト言語の少し書き換えてさくっと挙動を確かめられるスピード感が好きです。
なので node index.js
を打ったらすぐに挙動を確認できる状態を保ちたく、基本的にバックエンドにあるNode.jsのコードはビルドしない主義です。
(フロントのコードはこの限りではありません)
だけど型は有用
僕は普段VSCodeでコーディングをしています。
フロントエンドはAngularなどでTypeScriptにふれる機会が多く、型の有用性を感じています。
いきなりバックエンドのときと主張が違うじゃないかと思うかもしれませんが、フロントエンドは現状どうしてもビルドが必須です。ならばTypeScriptを使ったほうがいいだろうと考えています。
ビルドしないまま型を使えないだろうか
そういったわがまま要求が自分の中にあったので、どうやればそれを実現できるかを探っていました。
条件としては下記を実現しようとしていました。
- Node.js のコードはビルドしない (nodeコマンドでそのまま実行できる)
- 静的解析でテストができる
- VSCodeで型の補完がきく
そんな折、一年程前にTypeScriptのリリースノートを見ていると @ts-check
という機能が追加された事を知りました。
読みすすめてみるとこれは自分の要求をかなり満たしてくれるのではという考えが生まれ、いつか試してみようと考えていました。
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-3.html
https://github.com/Microsoft/TypeScript/wiki/Type-Checking-JavaScript-Files
この機能は簡単に言うと、JSDocを書けばJavaScriptを静的解析してくれるという機能です。
JSDocはただのコメントなので実コードには何も影響しません。
似たコンセプトにflowtypeがありますが、独自文法ではなくJSDocの形をとっているところが自分好みです。
また、JSDocなので最終的にドキュメントを自動出力するポテンシャルがあるとも感じました。(これはまだ試せていません)
実際に試してみた
最近、仕事で0からコードを書く機会があり、せっかくなのでこの機能をフルに使って開発してみました。
実際に試してみると意外と自分の感覚にあっていました。
サンプルでQiitaのAPIを叩き、ファイルに保存するコードを作成しました。
下記GitHubのリポジトリにアップしてあります。
https://github.com/koh110/qiita-api-test
型チェックを体験する
メインコードはとてもシンプルです。
qiita.jsで叩いた結果をlogファイルに書き出します。
先頭に @ts-check
が入っているのでこのファイルも静的解析されます。
ですが、アプリケーションのコードはすべて .js
なのでビルドしなくても node index.js
で挙動が確認できます。
// @ts-check
const path = require('path')
const fs = require('fs')
const util = require('util')
const writeFile = util.promisify(fs.writeFile)
const qiita = require('./qiita')
const main = async () => {
const data = await qiita.getUser('koh110', { Authorization: `Bearer ${process.env.TOKEN}` })
console.log(data)
await writeFile(path.join(__dirname, './response.log'), JSON.stringify(data))
}
main().then(() => console.log('done')).catch(err => console.error(err))
レスポンスの data
に続いてドットを打ち込むと qiita.getUser
が返すパラメータがサジェストされます。
存在しないパラメータを参照しようとするとエラーが表示されます。
tscを実行してみると失敗し、正しく型を参照できていることがわかります。
JSDocで型を定義する
型の定義はJSDocの記法に従います。
基本は @param
で引数と型を指定し @returns
で返り値の型を指定するだけです。
functionの入出力以外でも型を定義できますが、基本的に入出力のみで十分な事が多いのでそこだけの定義にとどめています。(型を定義することが効きそうな時には書くこともあります)
再利用する型やワンライナーで書くには長い型は @typedef
で名前をつけることができます。
また、これはTypeScript + axios + requireの問題なのですが、import文以外でaxiosを読み込むと型定義がなされていなくてエラーになります。
そこで仕方なくrequest.jsだけはあえて @ts-nocheck
でTypeScriptによるチェックをしないようにしています。
回避方法があれば知りたい。
// @ts-nocheck
const axios = require('axios')
/**
* @typedef {object} Response
* @property {object} data
* @property {number} status
*/
/**
* @param url {string} - url
* @param options {object} - options
* @returns {Promise<Response>} data
*/
exports.get = async (url, options = {}) => {
return await axios.get(url, { timeout: 1000, ...options })
}
/**
* @param url {string} - url
* @param data {object} - post data
* @param options {object} - options
* @returns {Promise<{data: object, status: string}>} data
*/
exports.post = async (url, data={}, options = {}) => {
return await axios.post(url, data, { timeout: 1000, ...options })
}
引数もJSDocに記載した通りにきちんと型が定義されています。
外部ファイルに型を定義する
TypeScriptは2.9から型定義を別ファイルからimportする import types
という機能が追加されました。
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-9.html
これはJSDocにも適用することができるので、JavaScriptのファイルに書くには長すぎる定義などを別ファイルに分離することができます。
この .ts
ファイルはコメント内でしか読み込まれないのでアプリケーションコードには影響しません。
が、ここまでするならTypeScriptで書けばいいのでは?という気はします。ここはまだ自分でもどうしたらいいか悩み中です。
// @ts-check
const request = require('./request')
/**
* @param user {string} - user id
* @param headers {object} - headers
* @param [headers.Authorization] {string} - bearer token (optional)
* @returns {Promise<import('./qiita').User>} data
*/
exports.getUser = async (user, headers = {}) => {
const res = await request.get(`https://qiita.com/api/v2/users/${user}`, {
headers: { Authorization: `Bearer ${process.env.TOKEN}`, ...headers }
})
return res.data
}
export type User = {
description: string,
facebook_id: string,
followees_count: number,
followers_count: number,
github_login_name: string,
id: string,
items_count: number,
linkedin_id: string,
location: string,
name: string,
organization: string,
permanent_id: number,
profile_image_url: string,
twitter_screen_name: string,
website_url: string
}
まとめ
まだいくつかの問題点は残っていますが、下記の点で自分にとっては十分嬉しい状況を作ることができました。
型があると後からコードを見た時にオブジェクトの中身を思い出しやすいのはやはりいいなぁと思いました。
- Node.jsでそのまま実行できる
- 型を定義して静的解析のテストができる
- プロパティがサジェストされるので開発時に嬉しい
- コメントを書くようになった