JavaScript
flow
TypeScript
flowtype

型チェックは TypeScript や Flow じゃなくて JavaScript にやらせる。

開発効率のために型チェックを望む

JavaScript が動的型言語なので、静的型言語にしてしまえば開発効率があがる、というアイデアがあって、それは確かなことかとも思います。プログラムを書いていて、ある変数の型がよくわからなければ正確な処理を行わせるのって、難しいですよね。

確かに開発効率、あがると思います。

実際に自分も開発効率をあげています。しかし、型チェックは行っていますが、TypScript や Flow とかは使っていません。

TypeScript や Flow が人気を得てきていますが、そこで行う型チェックは、素のJavaScriptにやらせてしまうこともできるので、導入前に検討してみるのもよいかもしれません。

チーム開発の場合には、型チェックを自前で行わないエンジニアにも型チェックを行ってもらうという意味で、TypeScript 等を導入する価値はたしかに大きくあると思いますが、できるエンジニアならなるべくバグを減らすように変数というか引数に対する型チェックを自前で行ってバグ防御処理をいれるのは普通のことかと思います。

この普通のことが、チーム全員に浸透すれば、型チェックシステムとしてトランスパイラの導入という、形にはならなくてすむかもしれません。

さて、自前で型チェックを行う方法を示します。

このように型チェックを導入します

ということで、コードを示します。node.js あたりで動かしてください。

// -------------------------
// ライブラリ部分
// -------------------------

const assert = function (value, message) {
  if (typeof message === 'undefined' || message === null) {
    message = '';
  }
  if (value !== true) {
    throw new Error(message);
  }
};

const isArray = function (value) {
  return Object.prototype.toString.call(value) === '[object Array]';
};

const isType = function (checkFunc, argsArray) {
  assert(1 <= arguments.length);
  assert(typeof checkFunc == 'function');
  assert(isArray(argsArray));

  var l = argsArray.length;
  if (l === 0) {
    return false;
  } if (l === 1) {
    return checkFunc(argsArray[0]);
  }
  for (var i = 0; i < l; i += 1) {
    if (!checkFunc(argsArray[i])) {
      return false;
    }
  }
  return true;
};

const arrayFrom = function (argsObj) {
  return Array.prototype.slice.call(argsObj);
};

const isNumber = function (value) {
  return typeof value === 'number' && isFinite(value);
};

const isInt = function (value) {
  if (!isNumber(value)) {
    return false;
  }
  return Math.round(value) === value;
};

const isInts = function (value) {
  return isType(isInt, arrayFrom(arguments));
};

const isString = function (value) {
  return (typeof value === 'string');
};

const isStrings = function (value) {
  return isType(isString,
    arrayFrom(arguments));
};

const isFunction = function(value) {
  return (typeof value === 'function');
};

//---

const stringQuote = function(value) {
  return isString(value) ? "'" + value + "'" : value.toString();
};

const typeCheckMode = true;
const typeCheck = (func) => {
  if (typeCheckMode) {
    const result = func();
    if (Array.isArray(result)) {
      const args = result[0];
      assert(result.length - 1 === args.length, 'typeCheck arguments length');
      for (let i = 1; i < result.length; i += 1) {
        assert(isFunction(result[i]), 'typeCheck arguments not function');
        assert(result[i](args[i-1]),
          `typeCheck ${result[i].name}(${ stringQuote(args[i-1]) })`);
      }
    }
  }
};

// -------------------------
// 関数チェックを行うプログラム本体
// -------------------------
function testAdd(value1, value2) {
  typeCheck(() => {
    assert(isInts(value1, value2) || isStrings(value1, value2), 
      'testAdd arguments type');
    assert(arguments.length === 2, 'testAdd arguments length');
  });
  return value1 + value2;
}

function test1(value1, value2) {
  typeCheck(() => [[value1, value2], isInts, isStrings]);
  return 'test1Result';
};

function test2(value1, value2) {
  typeCheck(() => [arrayFrom(arguments), isInts, isStrings]);
  return 'test2Result';
};

const test3 = (value1, value2) => {
  typeCheck(() => [[value1, value2], isInts, isStrings]);
  return 'test3Result';
};

const test4 = (...args) => {
  typeCheck(() => [args, isInts, isStrings]);
  const [value1, value2] = args;
  return 'test4Result';
};

// -------------------------
// 動作確認
// -------------------------
console.info('test', testAdd(1, 2));
console.info('test', testAdd('a', 'b'));
// console.info('test', testAdd(1.1, 2));   // Error: testAdd arguments type
// console.info('test', testAdd(1, 'b'));   // Error: testAdd arguments type
// console.info('test', testAdd(1, 2, 3));  // Error: testAdd arguments length

console.info('test', test1(1, 'abc'));
console.info('test', test2(1, 'abc'));
console.info('test', test3(1, 'abc'));
console.info('test', test4(1, 'abc'));

// console.info('test', test1('1', 'abc'));  
  // Error: typeCheck isInts('1')

// console.info('test', test1(1, 2));
  // Error: typeCheck isStrings(2)

// console.info('test', test2(1, 'abc', 1));
  // Error: typeCheck arguments length

コードの説明

ライブラリ部分は準備コードとして内容を理解しなくていいです。

「// 関数チェックを行うプログラム本体」と「// 動作確認」、この部分で、使い方を示しています。

typeCheck関数で型チェックを行っているので、そこをみると使い方がわかります。

testAdd 関数では、2引数 のみを受け取る関数を作っています。引数がどちらも、整数 か 文字列 か の場合だけ動作する。そうでなければ、例外を吐くようにつくりました。

小数点値などを引数にいれると動かない関数を作ることができます。実際のお仕事なんかでこだわってくると、ときにはこのような引数の型を判定する関数が必要になる場合もありますよね。こういう時にコードで型チェックを行えば、うまく判定できてしまいます。

また、test1~4の例で、複雑な判定ではない場合の、短く書く型チェックの方法を示しておきました。引数1は整数型、引数2は文字列型、に限定した関数をつくっています。型チェックコードをこのように仕込むことができるということです。

isInt や isString のような型関数を定義して書けば、すぐに型を判定することができます。isMyClassとか、コンストラクタで判定するのは簡単でしょ?

test1~4の例がそれぞれ別なのは、関数式と アロー関数式 とで、微妙に仕様が違うために arguments オブジェクトとか、残余引数とかの指定でブレがあるのですが、 このように書けるということです。それぞれのエラーメッセージもコメントで書いてみました。

このような typeCheck関数を使うと毎回引数チェックを行うので動作が少し遅くなってしまうかもしれない、という懸念があると思いますが、typeCheckには、アロー関数を渡していて、typeCheckModeの定数で動作を制御しています。リリースビルド時に typeChecck = false にすれば そもそもこの関数内の記述は評価されません。

なので、極力遅くなるようなことはないと思います。1回の関数呼び出しとBooleanの評価、くらいのことはあるのですが、それが足かせになるようなプログラムなんてこの世にまずないでしょう。

つまりは assert 使えば 型チェックなんていつでもできるってこと。

typeCheck関数で行っていることは、基本的には、assertを使って事前にエラーになるようなものをエラーにしてしまおうということで、これは、専門的なことをいうと「assert による契約プログラミング」といいます。

これを使いこなせる人は型チェックトランスパイラを使う必要がなく、使えない人のために型チェックトランスパイラを導入しましょうという話ですが。システムで導入する前に、教育とか。

TypeScript の導入前のリスクとか。

TypeScriptの導入、あるいは、Flow の導入とかを検討しているなら、このtypeCheck関数の導入の方が簡単でしょう。一部だけをあるいは特定の関数だけに導入することも簡単です。

あまり TypeScript / Flow Type をよく知らないのに disると、反撃うけると思うのですが、、型チェックシステムなど導入する必要性がないので、ずっと知らないままなのかもしれません。TypeScriptとかは、VSCodeのコード補完がメリットだろうなというのは確かにそうなので、そのために導入する価値も検討に値するかもしれませんね。ただ、JSDocでもVSCodeはコード補完してくれるらしいよ。

ということで、少なくとも、型チェック程度のことならば、素のJSで行えますよ。という記事でした。

変なトランスパイラの挙動にまどわされずにほぼ同じことが自前のコードでしておく、というのは自分にはメリットです。WebPack や Babel だけで複雑度が高まるレイヤーをかませているのに、TSを追加するというのはますます複雑度が増えますね。また、コンパイル時間がリスクになるかもしれません。

あと、わざわざ詳細は言いませんけれども CoffeeScript や Grunt Gulp そして近い将来の Ruby とか、そういうのを思い起こさせるような TypeScript へのリスクある投資。

リスクをあえて引き受けるという 男の中の男は、TypeScriptを使っていって、リスクをはねのけて頑張るのもいいでしょうね。

リスクのある冒険を取るか、リスクをなくして足場を固めるか。

そういう面白みってありますよね。

追記

もっと簡単な方がいいかなと思い直したので改定しておきます。

const typeCheckMode = true;
const typeCheck = (func, message) => {
  if (typeCheckMode) {
    message = message ? message + ' ' : '';
    const result = func();
    if (Array.isArray(result)) {
      let errorMessage = '';
      for (let i = 0; i < result.length; i += 1) {
        assert(result[i], `${message}typeCheck return ${result} index:[${i}]`);
      } 
    }
  }
};

function test1(value1, value2) {
  typeCheck(() => [
    isInts(value1),
    isStrings(value2),
    arguments.length === 2,
  ], 'test1');

  return 'test1Result';
};

const test2 = (value1, value2, ...args) => {
  typeCheck(() => [
    isInts(value1),
    isStrings(value2),
    args.length === 0
  ], 'test2');
  return 'test2Result';
};