TypeScript Guidelines

Types and Values

Fuzzy types

以下の型はすべてまたはほとんどの型が代入可能となり型制約として機能しない場合があるため注意を要する。

  • void
  • {}
  • Object
function f<T extends Object>(p: T, cb: (p: {}) => void) {
}
f<number>(0, (n: number) => 0);

void type

void型に定義された値を得た場合はその値を使用せず直ちに破棄する。 void型は他の型を代入可能な経路があるため信用できない。

function f(): void {
  return void g();
}
function g(): void {
}

undefined

undefined変数その他一切の格納されたundefinedの使用を禁止する。 undefinedは状態を未定義状態に戻す場合にvoid演算子または空により逐次生成する方法によってのみ格納を許可し、値としての使用を禁止する。

  • 生成:可能
  • 書込:未定義状態を定義する場合のみ可能
  • 読込:評価のみ可能、外部への搬出不可
function f(n: number): void {
  n = n || void 0;
  if (n < 0 || n === void 0) return void g(n);
  return;

  function g(n: number): void {
  }
}

null

nullの使用をnullの検出とObject.create(null)などnullが必須である言語または外部機能を使用する場合を除き禁止する。

String literals

文字列の生成は原則としてシングルクオートおよびバッククオートを使用し、ダブルクオートを使用しない。 シングルクオートをエスケープする場合はダブルクオートを使用してよい。 その他JSONやHTMLなど必要に応じて使い分ける。

Big number

10進数で15桁(正確には2^53)を超える整数は精度が保証されないため使用禁止とし、文字列化して管理する。

JSON.parse('{"id": 9999999999999999}'); // Object {id: 10000000000000000}

ビット演算可能なのは10進数で9桁まで。

999999999.9|0; // 999999999
~~999999999.9; // 999999999
9999999999|0; // 1410065407
~~9999999999; // 1410065407

Enums

Enumは特に必要のない限りストリングリテラルタイプで置換する。

Operators

Equality operators

厳密等価演算子のみ使用する。 等価演算子の使用を禁止する。

null === null;

Increment and Decrement operators

必要のない限り常に前置記法で記述する。

++n;
--n;

Operand order of binary operators

二項演算子のオペランドは、actual > expectedの順に記述する。

if (typeof n === 'number') {
}

Conditional operators across multiple lines

複数行にわたる三項演算子は、各演算子の前で改行しインデントを1レベル下げる。 return文とともに使用する場合は常に複数行で記述する。

function f() {
  return expr
    ? true
    : false;
}

Disallow in operator

自身のメンバーの有無はメンバーに直接アクセスして確認し、必要のない限りin演算子を使用しない。 オーバーヘッドが非常に大きい。

if (obj.member === void 0) {
}

Indents with closing parentheses

閉じ括弧は末尾の要素と同じ行に続ける。

f(() =>
  g(() =>
    h()));

Functions and Objects

Function definition order

関数は粒度(目的としての抽象度)の大きい順に定義する。

f();

function f() {
  g();
}
function g() {
}

Closure definition positions

クロージャはreturn文の後ろに1行空けて記述する。

function f() {
  return g();

  function g() {
  }
}

ループの中のような極めて短い間隔で繰り返し実行される場所およびそこで呼ばれる関数内でクロージャを作らない。 関数定義はコストが高い。

function f() {
  return g();
}
function g() {
}

Adapter functions and method definitions

関数の役割が実体である別の関数の事前処理または事後処理、あるいはインターフェイスの隠蔽であり、実体関数が他の関数から使われない場合、自身のクロージャとして実装する。 クロージャの名前が親と同じである場合は名前の先頭または末尾にアンダースコアを追加ないし削除して名前を変える。 メソッドにおいても同様とする。

function f() {
  return f_(1);

  function f_(cnt: number) {
    return cnt > 10
      ? void 0
      : f_(cnt + 1);
  }
}

パフォーマンスを要求される場合は逆に外部へ露出する。

function f() {
  return f_(1);
}
function f_(cnt: number) {
  return cnt > 10
    ? void 0
    : f_(cnt + 1);
}

Arrow functions across multiple lines

ブロックを作らない複数行にわたるアロー関数は、アローの後で改行しインデントを1レベル下げる。 アロー関数から通常関数に変更しても末尾のreturn文の追加以外関数本体が変更されない。

dispatch(ns =>
  ns
    .filter(n => !isNaN(n))
    .map(parseInt);
);
dispatch(ns => {
  return ns
    .filter(n => !isNaN(n))
    .map(parseInt);
});

Destructuring parameter declarations

引数のオブジェクトの一部の要素しか使わない場合、引数にとる時点で分解して使用する要素のみ宣言する。 意図しない要素に触れられる余地を作らない。

function f({textContent, innerText}: HTMLElement) {
  return textContent || innerText || '';
}

Indents of function parameters

呼び出し関数と同じ行で複数行にわたる関数をパラメータとして渡す場合、以降のパラメータは改行せず同じ行に記述する。 このとき関数本体は常に何らかの括弧で囲み垂直アラインをそろえる。

[]
  .reduce(b => {
    return b;
  }, 0);
[]
  .reduce(b => [
    b
  ], 0);
[]
  .reduce(b => (
    b
  ), 0);

呼び出し関数とパラメータを改行して分ける場合は通常通りコロンを行末とする。

[]
  .reduce(
    b =>
      b,
    0);
[]
  .reduce(
    b => {
      return b;
    },
    0);

Function return value types

関数の戻り値は原則としてプリミティブ型をはじめとするビルトインタイプおよびパラメーターの型のみとする。 メソッドの戻り値は関数で許可される型および自身の型のみとする。 void型はこの規則に則った戻り値を返せない場合のデフォルト型とする。 オブジェクトはデザインパターンやアルゴリズムなど明確な規範に則って設計し、独自のデータ構造の使用は控える。 レコードなど内部プロトコルとしてのオブジェクトは乱用を控える。

function f(): boolean {
  return true;
}
class C {
  dispatch(): C {
    return this;
  }
}

Command query segregation

コマンドの戻り値は原則として空、個別の実行結果の識別子ないし操作方法のいずれかに限る。 自身に破壊的変更を加えながら自身を返すメソッドは不適切である。 副作用を持つ操作が深いスタックを持ち垂直方向に複雑化すると副作用の管理が困難となるため副作用を持つ操作は可能な限りネストさせず浅い操作にしなければならない。 コマンドに副作用を連鎖させる戻り値を禁止する制約は副作用の複雑化を水平方向に止め垂直的複雑化を抑止する。 なお不変オブジェクトを返すメソッドチェインは一般的に副作用を持たないためこの制約対象に該当しない。

Conditional expressions across multiple lines in return statement

複数行にわたり末尾のreturn文に条件分岐を持たせる場合は以下のフォーマットを使用する。 このフォーマットで表現できない複雑な条件式の使用は基本的に不適切である。

Conditional expressions

function f() {
  return expr
    ? 0
    : 1;
}

Logical operators

function f() {
  return 1
      && 2
      && 3;
}
function g() {
  return 1
      || 2
      || 3;
}
function h() {
  return 1
      || 2 && 2.1
      || 3 && 3.1;
}

Order of branch on condition

条件分岐は基本状態を最初に記述し、変化形を変化率の小さい順に記述していく。 アルゴリズムの条件分岐は停止条件を最初に記述し、その他の条件を最後に記述する。

function fib(n: number): number {
  switch (n) {
    case 0: {
      return 0;
    }
    case 1: {
      return 1;
    }
    default: {
      return fib(n - 1) + fib(n - 2);
    }
  }
}

Ad-hoc polymorphic functions

アドホック多相関数は単一のswitch文で関数を満たしてcase句で多相性を表現する。 共通パラメーターなどの宣言的定義はswitch文のほかに関数に含めてよい。

function f(str: string): string
function f(num: number): string
function f(p) {
  switch (typeof p) {
    case 'string': {
      return p;
    }
    case 'number': {
      return p + '';
    }
    default: {
      throw new TypeError('Invalid type parameter.');
    }
  }
  assert(false);
}

単純なものは最初のステートメントでreturnして三項演算子で表現する。

function f(str: string): string
function f(num: number): string
function f(p) {
  return typeof p === 'string'
    ? p
    : p + '';
}

Function call and apply methods

applyメソッドはスプレッドオペレーターを利用してcallメソッドに統一する。 スプレッドオペレーターを使用できない場合のみapplyメソッドを使用する。 なおトランスパイルしないネイティブのスプレッドオペレータはv8で非常にオーバーヘッドが大きいため別途考慮が必要となる。

f.call(this, ...args);
f.apply(this, arguments);

Disallow arguments parameter

argumentsオブジェクトでなくレストパラメーターを使用する。 オーバーヘッドが大きい。

function f(...args) {
  g(args);
}

Method chaining

メソッドチェインごとに改行しインデントを1レベル下げる。 コンテキストの変更やチェインの追加により既存チェインが変更されない。 1チェインのみであればフォーマットを整えるためにワンライナーで記述してよい。

obj.arr
  .filter(_ => true);
obj => obj.arr.filter(_ => true);

Disallow immutable arrays

配列を不変オブジェクトとして扱うプログラミングスタイルを原則として禁止する。 v8でのコストが高く、計算量を管理せずに使用すると容易に重大なボトルネックとなる。 許可は厳格な管理の下で局所的かつ個別に行う。

Disallow array methods

同じくコストが高いため計算量を管理して限定的に使用する。

Disallow the negative number index access in arrays

配列要素への負数インデクスによる不正なアクセスは事前に回避する。 オーバーヘッドが非常に大きい。

if (0 <= index && index < arr.length) {
  arr[index];
}

Class member definition order

この順序は他に基準がない場合の目安に止めるものとする。 static member > constructor > instance member or static member only for instance の順で定義する。 各区分内は重要度の高い順に関連するメンバを隣接させて定義する。 関連するプロパティとメソッドはプロパティを先に定義する。 インスタンスメソッドからのみ使用されるプライベートなクラスメンバはインスタンスメンバと同等とする。

class C {
  private static prop_;
  public static method() {
  }

  constructor() {
  }

  private prop_;
  public method() {
  }
  private util_;
  public util() {
  }
  private static throwError_() {
  }

}

Statements and Expressions

Union/Intersection type declarations across multiple lines

複数行にわたるUnion/Intersection型の宣言は以下のフォーマットを使用する。

type union
  = A
  | B
  | C;

Variable definitions

constおよびletのみ使用する。 varの使用はアルゴリズムまたはES5の制約で必要な場合のみに止める。

Unused variables

不要な変数をアンダースコアで宣言する。 関数に2つ以上省略できない引数がある場合は分割代入を利用して宣言を省略する。 2つ以上省略できない仮引数が生じる状況自体が基本的に不適切であるため空の分割代入はそのアノテーションとみなす。

function f([, , x], _, [], {}, y) {
  return [x, y];
}
f([1, 2, 3], 0, [], NaN, 4);
const g = _ => 0;
g(NaN);

String literal types as Enums

Enumとしてのストリングリテラルタイプは以下のフォーマットで記述する。

type Type
  = Type.put
  | Type.del
  | Type.get;
namespace Type {
  export type put = 'put';
  export const put: put = 'put';
  export type del = 'del';
  export const del: del = 'del';
  export type get = 'get';
  export const get: get = 'get';
}

予約文字を含めたい場合は全体をオブジェクトとして定義すれば可能。 ただし型空間での個別の型定義はできない。

const Type = {
  put: <'put'>'put',
  delete: <'delete'>'delete',
  get: <'get'>'get'
};
type Type
  = typeof Type.put
  | typeof Type.delete
  | typeof Type.get;

Early returns

関数的記述を心がけ、ブロックをreturnで開始させるよう努める。

Do-like parentheses

丸括弧を利用して冗長なブロックを削除する。

[['a', 1], ['b', 2]]
  .reduce((m, [k, v]) => (m[k] = v, m), {});

Evaluations of conditional expressions

条件式で評価してよい値の組み合わせは以下の2つのパターンのみとする。

true/false

比較演算、論理演算または評価関数の結果である真理値。

if (arr.indexOf(n) > -1) {}
if (flag === false && !flag) {}
if (is()) {}

Object/(undefined|null)

オブジェクト(非プリミティブ値)の有無。

if (err) {}
if (elem.parent) {}

Line breaks of multiple block statement

複数ブロックを持つ構文はブロックの終了ごとに改行する。

if (expr) {
  f();
}
else {
  g();
}

Guard statements by one-liner non-brace if statements

ガード節のためのブロックを明示しないif文を以下の条件をすべて満たす場合のみ許可する。

  • ワンライナーである。
  • elseおよびelse if文を持たない。
  • return, continue, break, throwといったコードフローを制御する予約語で開始する。
function f() {
  if (!expr) return;
}

Case clauses of switch statements

switch文のcase句の文がコードフローを制御する予約語で開始されない場合は常にブロック化する。

switch (n) {
  case 0:
    return 0;
  default: {
    const m = 1;
    return n * m;
  }
}

Switch statements being with other statements

switch文の他の文との混在を非推奨とする。 if文に置換または関数に切りだすべきものである場合が多い。

Annotate command calls [experimental]

副作用のある命令的関数の呼びだしにvoid演算子を付与して副作用のある命令の発行に注釈をつける。 コード上に存在する副作用がvoid演算子によりマークアップされ可視化される。 代入操作に注釈をつけることができないのが欠点。 この項目は単純なスクリプトなど設計のないコードには適用しなくともよい。

void ++count;
void ns.push(1);
void fs.pop()();

Promise

Promiseの失敗文脈では非同期の例外のみ扱い、回復可能または予測された失敗および例外は多値の成功文脈で表現する。 失敗文脈は例外のみを扱う例外文脈として設計しなければならない。 Promiseの失敗文脈は任意の失敗と予期せぬ例外を分離できないため基本的に失敗文脈として使用できない。 失敗文脈をそれとして使用する場合は絶対に予期せぬ例外が混入しないよう信頼性を高めなければならない。 よって原則として組み込み関数のような最小単位の関数以外で失敗文脈を使用するべきではない。

多値表現にはタプルを使用し最初の要素に成否判定のフラグを入れるフォーマットを推奨する。

new Promise(resolve => resolve([arr.length > 0, arr.pop(), arr]));

Promise functions [experimental]

Promise値を返す関数は例外の発生元が明白である低水準で最小単位の関数および例外発生の通知を必要とする特殊な関数のみに限定する。 このためPromise値を返し失敗文脈を持つ組み込み関数を単に模倣して失敗文脈を持つ関数を作ることは誤りである。 Promiseの使用は原則として関数のコードフローの末尾であるべきであり、Promise値の生成後に後処理があるべきではない。

function sleep(n: number): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, n));
}

async/await

async/awaitはPromise同様失敗を多値の成功文脈で表現し、失敗文脈が例外のみ扱うように設計することでtry-catch文が不要となるよう設計する。 try-catch文によりawait式の失敗文脈を捕捉する場合、tryブロックにはawait式のみを入れ、同期的処理の例外を混入させない。 同期処理において捕捉しない例外を非同期処理に限って捕捉しなければならない理由はない。

await sleep(1);
var n = 100;
try {
  await sleep(n);
}
catch (err) {
  throw err;
}

async/await functions [experimental]

Promise同様、await式は原則として関数のコードフローの末尾となり戻り値が直接関数の戻り値となるべきである。

function f() {
  return await g();

  function g() {
    return Promise.resolve();
  }
}

Module imports

外部ライブラリ > 依存度 の順でインポートする。 基本的にインポート数の多いモジュールが依存度の高いモジュールとなる。

import lib from 'lib';
import util from 'util';
import {Entity} from './entity';
import {App} from './app';

Module exports

デフォルトエクスポートの使用をパッケージモジュールなど独立した構成単位のインターフェイスのみに制限し、これ以外を禁止する。 デフォルトインポートの識別子は一括してリネームできないためリファクタリングが困難となる。

export class App {
}

Modules and Components

Divide by functions

関連性があっても異なる独立した機能は個別のファイルに分割する。 関連をまとめて取り扱う場合は集約のみ行うモジュールファイルを用意する。

Divide of classes

クラスの分割と抽象化は設計時および1クラスの実装が100行を大きく超えたときに検討する。 やみくもな分割と抽象化は控える。

Namespacing

各種定義にコンテキストを与える、共通の名前空間を使用しトップレベルの名前空間の使用を減らす、名前の共通部分を省略した簡潔な命名を行うなどする場合はnamespaceを利用する。

class Entity {
  public value: Entity.Value = new Entity.Value();
}
namespace Entity {
  export class Value {
  }
}

Code Design

Max line length

100文字。 80文字から160文字程度。 ドキュメントおよびコメントでは制限を緩和してよい。 タイル型WMの使用を考慮する。

Lines of code

約100行以下。 1画面に入る50行以下の行数が望ましい。

アルゴリズム、データ構造、デザインパターンなど一般化した抽象モデルの一部またはこれを拡張する場合は複雑な機能を実装しなければならないため行数が増えてもやむをえないが、 独自の機能やモデルが一般的な抽象モデルによって簡素化されず膨張する場合は抽象化に失敗している可能性がある。 一般的な抽象モデル以外の部分が100行以下で簡潔に記述されている場合は適切に抽象化されていると言える。

Design vs Performance

設計を優先する。 パフォーマンスのために設計を歪めるとリファクタリングとモデルの成長が困難となる。 要所における関数またはメソッドの2,3個の変更で収まる最適化は行ってよい。

O(n)以下の計算量の最適化やキャッシュ化は必要時にのみ行えばよい。 O(nm)も十分に小さければ無視できる。 早まった最適化は複雑化により作りこまれたバグを特定し修正するコストによる生産性および開発速度の低下に見合わない。

Sample products