LoginSignup
10
9

More than 5 years have passed since last update.

Node.jsをビルドしないで型を手に入れる

Posted at

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 で挙動が確認できます。

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 が返すパラメータがサジェストされます。
サジェスト.png

存在しないパラメータを参照しようとするとエラーが表示されます。
エラー.png

tscを実行してみると失敗し、正しく型を参照できていることがわかります。
テスト.png

JSDocで型を定義する

型の定義はJSDocの記法に従います。

基本は @param で引数と型を指定し @returns で返り値の型を指定するだけです。
functionの入出力以外でも型を定義できますが、基本的に入出力のみで十分な事が多いのでそこだけの定義にとどめています。(型を定義することが効きそうな時には書くこともあります)
再利用する型やワンライナーで書くには長い型は @typedef で名前をつけることができます。

また、これはTypeScript + axios + requireの問題なのですが、import文以外でaxiosを読み込むと型定義がなされていなくてエラーになります。
そこで仕方なくrequest.jsだけはあえて @ts-nocheck でTypeScriptによるチェックをしないようにしています。
回避方法があれば知りたい。

request.js
// @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に記載した通りにきちんと型が定義されています。
args.png

外部ファイルに型を定義する

TypeScriptは2.9から型定義を別ファイルからimportする import types という機能が追加されました。
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-9.html

これはJSDocにも適用することができるので、JavaScriptのファイルに書くには長すぎる定義などを別ファイルに分離することができます。
この .ts ファイルはコメント内でしか読み込まれないのでアプリケーションコードには影響しません。

が、ここまでするならTypeScriptで書けばいいのでは?という気はします。ここはまだ自分でもどうしたらいいか悩み中です。

qiita.js
// @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
}
qiita.type.ts
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
}

別ファイルの型定義が読み込めています。
type.png

まとめ

まだいくつかの問題点は残っていますが、下記の点で自分にとっては十分嬉しい状況を作ることができました。
型があると後からコードを見た時にオブジェクトの中身を思い出しやすいのはやはりいいなぁと思いました。

  • Node.jsでそのまま実行できる
  • 型を定義して静的解析のテストができる
  • プロパティがサジェストされるので開発時に嬉しい
  • コメントを書くようになった
10
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
9