📓

TypeScriptのコーディング規約を策定してみた!

2024/10/24に公開1

はじめに

少し前の話ですが、TypeScriptのコーディング規約を策定する機会がありました。
そこで、チームで実際に採用しているルールを具体例も交えながら紹介します!💫
また、もしこの記事を読んで別の観点や改善点があれば、ぜひアドバイスを頂きたいです!

経緯

私たちのチームは急速に拡大しており、ルールや規約の整備が追いついていない状況でした。
その結果、開発者によってコードの書き方がバラバラになり、可読性やメンテナンス性を損なう懸念が生じていました。
このような背景から、コーディングスタイルを確立し、全員が共通のルールに従って作業できるようにする必要性を感じ、規約の策定に取り組むこととなりました。

策定した結果

PRレビュー時の議論が少なくなったかな?くらいで目にみえる影響は現れませんでした。
現在、既存のプロジェクトは徐々にこの規約に合わせていき、新規プロジェクトに関しては初めから規約に従って進められています。
時間が経つにつれて、これらの取り組みがより大きな効果をもたらしてくれたら良いなと思っております。

命名規則

1文字の命名は避ける

  • 名前から意図を汲み取りやすくするため
    eslint: id-length

bad

const q = () => {
  // ...
}

good

const query = () => {
  // ...
}

オブジェクト、関数、インスタンスにはキャメルケース(小文字から始まる)を使用する

  • これは基本的なJavaScriptのスタイルであるため
    eslint: camelcase

bad

const OBJEcttsssss = {};
const this_is_my_object = {};
const c = () => {}

good

const thisIsMyObject = {};
const thisIsMyFunction = () => {}

型(type)にはパスカルケース(大文字から始まる)を使用する

  • JavaScriptのclassがパスカルケースで書かれるため、型も同じくパスカルケースにする

bad

type colorStyle {
}

good

type ColorStyle {
}

定数にはすべて大文字のスネークケース(snake_case)を使用する

  • JavaScriptの一般的なスタイルであるため

bad

export const RequiredMessage = 'を入力してください';

good

export const REQUIRED_MESSAGE = 'を入力してください';

単数形と複数形を使い分ける

  • 名前から内容を把握できるようにするため

bad

const user = ['a', 'b', 'c'];

good

const users = ['a', 'b', 'c'];

booleanの命名にはisまたはhasを使用する

  • 直感的にboolean値であることが伝わるため

https://qiita.com/KeithYokoma/items/2193cf79ba76563e3db6

bad

const enabled: boolean = true;
const canObservers: boolean = true;

good

const isEnabled: boolean = true;
const hasObservers: boolean = true;

文字列操作

文字列の連結には+ではなく、テンプレート文字列(`${}`)を使用する

  • テンプレート文字列のほうがシンプルで、可読性が高いため
    eslint:prefer-template

bad

const sayHi = (name) => {
  return 'How are you, ' + name + '?';
}

function sayHi(name) {
  return ['How are you, ', name, '?'].join();
}

good

const sayHi = (name) => {
  return `How are you, ${name}?`;
}

文字列

+ではなく、テンプレートリテラルを使用する

  • シンプルで可読性が高いため
    eslint: prefer-template

bad

const sayHi = (name) => {
  return 'How are you, ' + name + '?';
}

function sayHi(name) {
  return ['How are you, ', name, '?'].join();
}

good

const sayHi = (name) => {
  return `How are you, ${name}?`;
}

配列

コンストラクタではなくリテラルを使用する

  • より直感的でシンプルなため
    eslint: no-array-constructor

bad

const myArray = new Array(length);

good

const myArray = [];

配列をコピーする際は、拡張演算子...を使用する

  • 簡潔で可読性が高いため

bad

const len = items.length;
const itemsCopy = [];
let i;

for (i = 0; i < len; i++) {
  itemsCopy[i] = items[i];
}

good

const itemsCopy = [...items];

配列の要素を取得する際は、分割代入を使用する

  • 配列への直接参照を減らせるため
    eslint: prefer-destructuring

bad

const first = arr[0];
const second = arr[1];

good

const [first, second] = arr;

変数

基本的にconstを使用し、再代入が必要な場合のみletを使用する

  • varはスコープを無視して宣言でき、予期せぬ結果を引き起こしやすいため
    eslint: no-var

bad

const varTest = () => {
  var i = 31;
  if (true) {
    var i = 71;
    console.log(i);  // -> 71
  }
  console.log(i);  // -> 71
}

good

const letTest = () => {
  let i = 31;
  if (true) {
    i = 71;
    console.log(i);  // -> 71
  }
  console.log(i);  // -> 71
}

変数は必要な場所で宣言する

  • 無駄な初期化を避け、コードの保守性を向上させるため

bad

const checkName = (hasName) => {
  //ここで宣言する必要はない
  const name = getName();

  if (hasName === 'test') {
    return false;
  }

  if (name === 'test') {
    this.setName('');
    return false;
  }

  return name;
}

good

const checkName = (hasName) => {
  if (hasName === 'test') {
    return false;
  }
  
  //必要な箇所で宣言する
  const name = getName();

  if (name === 'test') {
    return false;
  }

  return name;
}

インクリメント(++)やデクリメント(--)を使わない

  • 予期せぬバグの原因になりやすいため
    eslint: no-plusplus

bad

const array = [1, 2, 3];
let num = 1;
num++;
--num;

let sum = 0;
let truthyCount = 0;
for (let i = 0; i < array.length; i++) {
  let value = array[i];
  sum += value;
  if (value) {
    truthyCount++;
  }
}

good

const array = [1, 2, 3];
let num = 1;
num += 1;
num -= 1;

const sum = array.reduce((a, b) => a + b, 0);
const truthyCount = array.filter(Boolean).length;

定数

定数とは

  1. exportされている
  2. constで定義されている
  3. 今後、開発者が変更しない値

関数

アロー関数を使用する

  1. 可読性が高い
  2. functionの一部の機能を必要としなくても、問題なく実装できるケースが多い
  3. 引数や命名の重複を防ぐ
  4. プロジェクト内ですべての関数をアロー関数で統一できる

ただし、一般的にはfunction宣言を優先すべきという意見も多い。

function宣言のメリット

  1. ReactやNext.jsの公式ドキュメントでは、function宣言が使われている
  2. MDN Web Docsでも、function宣言を推奨している
  3. eslint-plugin-reactによると、名前付きエクスポートではfunction宣言が推奨されている
  4. functionと明記することで、一目で関数とわかりやすくなる
  5. コストパフォーマンスの面でも、function宣言の方が良いとされる場合がある

bad

function someFnc () {
    console.log(1234)
}

good

const someFunc = (amount:number) => {
    const computeTax = (amount: number) => amount * 0.12;
}

//export defaultと併用する場合は、export defaultを最下部に配置する必要がある。
const Todo = () => {
....
}

export default Todo

引数がオプショナルである場合は、デフォルト引数を使用する。ただし、副作用のあるデフォルト引数の使用は避ける。

  • パラメータを突然変異させることは、バグに繋がるおそれがあるため。

bad

const handleThings = (opts) => {
  if (opts === void 0) {
    opts = {};
  }
  // ...
}

var b = 1;
const count = (a = b++) => {
  console.log(a);
}
count();  // 1
count();  // 2
count(3); // 3
count();  // 3

good

const handleThings = (opts = {}) => {
  // ...
}

引数へ再代入を行わない

  • バグのおそれがあるため

bad

const first = (a) => {
  a = 1;
  // ...
}

const second = (a) => {
  if (!a) { a = 1; }
  // ...
}

good

const third = (a) => {
  const b = a || 1;
  // ...
}

const forth = (a = 1) => {
  // ...
}

複数の値を返却する場合は、配列の分割代入ではなく、オブジェクトの分割代入を使用する

  • 呼び出し元を気にする必要がなくなるため

bad

const processInput = (input) => {
  return [left, right, top, bottom];
}
// 呼び出し元で返却されるデータの順番を考慮する必要がある。
const [left, __, top] = processInput(input);

good

const processInput = (input) => {
  return { left, right, top, bottom };
}
// 呼び出し元は必要なデータのみ選択すればいい。
const { left, top } = processInput(input);

オブジェクト

コンストラクターではなくリテラルを使用する。

  • 直感的であるため
    eslint: no-new-object

bad

const myObject = new Object();

good

const myObject = {};

短縮構文を使用する。

  • よりシンプルであるため
    eslint: object-shorthand

bad

const lukeSkywalker = 'Luke Skywalker';
const atom = {
  value: 1,
  lukeSkywalker: lukeSkywalker,
  addValue: function (value) {
    return atom.value + value;
  },
};

good

const lukeSkywalker = 'Luke Skywalker';
const obj = {
  lukeSkywalker,
  value: 1,
  addValue(value) {
    return atom.value + value;
  },
};

複数のプロパティからなるオブジェクトを代入する場合は、分割代入を使用する

  • プロパティへの中間的な参照を減らすことができるため
    eslint: prefer-destructuring

bad

const getFullName = (user) => {
  const firstName = user.firstName;
  const lastName = user.lastName;

  return `${firstName} ${lastName}`;
}

good

// good
const getFullName = (obj) => {
  const { firstName, lastName } = obj;
  return `${firstName} ${lastName}`;
}

// best
const getFullName = ({ firstName, lastName }) => {
  return `${firstName} ${lastName}`;
}

プロパティにアクセスする場合は、.を使用する。ただし、変数をしてアクセスする場合は、[]を使用する

  • より直感的であるため
    eslint: dot-notation

bad

const isJedi = luke['jedi'];

good

const isJedi = luke.jedi;

const getProp = (prop) => {
  return luke[prop];
}
const isJedi = getProp('jedi');

モジュール

importは最上部に置く。

  • バグを引き起こすおそれがあるため

bad

import foo from 'foo';
foo.init();

import bar from 'bar'

good

import foo from 'foo';
import bar from 'bar';

foo.init();

標準ライブラリ→サードパーティに関連するもの→ローカル特有なものの順番でグループ化し、各グループの間は1行空ける。

  • 可読性の向上を図るため
    eslint:newlines-between

bad

import { useState } from 'react';
import { SEARCH_FORM_DEFAULT_VALUE } from '../../constants';
import { useForm } from 'react-hook-form';

good

import { useState } from 'react';

import { useForm } from 'react-hook-form';

import { SEARCH_FORM_DEFAULT_VALUE } from '../../constants';

パスが同じである場合、1箇所にまとめる

  • 保守性の向上を図るため
    eslint: no-duplicate-imports

bad

import foo from 'foo';
// … some other imports … //
import { named1, named2 } from 'foo';

good

import foo, { named1, named2 } from 'foo';

ワイルドカードインポートは使用しない

  • 使う必要のないモジュールまでimportされ、バグを引き起こす可能性があるため

bad

import * as AirbnbStyleGuide from './AirbnbStyleGuide';

good

import AirbnbStyleGuide from './AirbnbStyleGuide';

名前付きexportを使用する。

  • export defaultでは、exportしている変数名とinport側の命名が一致しなくても、エラーが発生しないため、予期せぬバグを引き起こす恐れがある。
    ただし、Next.jsを使用している場合、app(pages routerの場合はpages)フォルダ配下はexport defaultでないとエラーが発生する。

bad

export default const Title = () => {
    return(
    <h1>title</h1>
    )
} 

good

export const Title = () => {
    return(
    <h1>title</h1>
    )
} 

型情報をimportする場合は、import typeを使用する

  • 明示的であるため。また、build時に対象のモジュールから削除されるので、バンドルサイズが軽減されるため。

https://azukiazusa.dev/blog/import-type-from-module/

bad

export type User {
  firstName: string
  lastName: string
}

import {User} from './types'

good

export type User {
  firstName: string
  lastName: string
}

import type {User} from './types'

タイプ

基本的にはtypeを使用する。ただし、継承が必要な時はinterfaceを使用する。

  • ユニオン型に対応できるtypeを基本的に使用するのが無難であるため。
    typeでも継承と似たようなこと(交差型)はできるが、交差型だとエラーになることが度々あった(僕だけ、、?)

any unknownは使用しない

  • TypeScriptを使用している意味がなくなるため。破壊行為。

Type assertionは極力使用しない。ただし、as constは積極的に使用する。

  • asでキャストすることは、型の不整合を握りつぶし、コンパイラを無理やり黙らせる行為であるため。ただし、as constは手軽に精緻な型を推論できるため、使用を推奨する。

bad

//アサーションが安全であると確信する場合のみ、使用する。
(x as Foo).foo();
y!.bar();

//worst
(x as unknown as Foo).foo();

good

if (x instanceof Foo) {
  x.foo();
}

if (y) {
  y.bar();
}

//as const の例
const ary = [1, 2, 3];          // number[]
const ary = [1, 2, 3] as const; // readonly [1, 2, 3]
const obj = {
  a: 1,
  b: ['hello', 'world'],
};
// {
//   a: number;
//   b: string[];
// }
const obj = {
  a: 1,
  b: ['hello', 'world'],
} as const;
// 変更の可能性が無い場合は、こちらの方が型推論の精度が高い。
// {
//   readonly a: 1;
//   readonly b: readonly ["hello", "world"];
// }
const message = `answer: ${42}`;          // string
const message = `answer: ${42}` as const; // answer: 42

単純な配列の型である場合は、リテラル([])で定義する。より長く複雑である場合は、Array<T>を使用する。

  • 場合分けすることで、より可読性を向上させるため

bad

let a: Array<string>;  // The syntax sugar is shorter.
let b: ReadonlyArray<string>;
let c: Array<ns.MyObj>;
let d: Array<string[]>;
let e: {n: number, s: string}[];  // The braces make it harder to read.
let f: (string|number)[];         // Likewise with parens.
let g: readonly (string | number)[];
let h: InjectionToken<Array<string>>;
let i: readonly string[][];
let j: (readonly string[])[];

good

let a: string[];
let b: readonly string[];
let c: ns.MyObj[];
let d: string[][];
let e: Array<{n: number, s: string}>;
let f: Array<string|number>;
let g: ReadonlyArray<string|number>;
let h: InjectionToken<string[]>;  // Use syntax sugar for nested types.
let i: ReadonlyArray<string[]>;
let j: Array<readonly string[]>;

比較・if

===を使用する。ただし、null/undefinedチェックの場合のみ!=を使用する。

  • 型安全を保証するため。ただし、null/undefinedチェックの場合、!==だとundefinedが除外されないため、!=とする。

bad

if(res == 'hoge')

//undefinedが除外されない
if(res !== null)

good

if(res === 'hoge')

//undefinedも除外される
if(res != null)

真偽値にはショートカットを使用する。

  • よりシンプルであるため

bad

if (isValid === true)

good

if (isValid)

三項演算子は単1行にする。どうしてもネストする場合は、ifもしくはswitch文を使用する。

  • 可読性が落ちるため
    eslint: no-nested-ternary

bad

const foo = maybe1 > maybe2
  ? "bar"
  : value1 > value2 ? "baz" : null;

good

const maybeNull = value1 > value2 ? 'baz' : null;

const foo = maybe1 > maybe2 ? 'bar' : maybeNull;

ifが常にreturnする場合、それに続くelseは不要。

  • よりシンプルであるため
    eslint: no-else-return

bad

const foo = () => {
  if (x) {
    return x;
  } else {
    return y;
  }
}

good

const foo = () => {
  if (x) {
    return x;
  }
  return y;
}

switch文には必ずdefault breakを使用する

  • 処理の終了を宣言する必要があり、ないとバグを引き起こすおそれがあるため

bad

switch (x) {
  case X:
    doSomething();
  case Y:
}

good

switch (x) {
  case X:
  case Y:
    doSomething();
    break;
  default:
}

その他

for ...inは使用せずに、for ...ofを使用する。

  • for ...inはプロトタイプチェーン全体を反復処理するので、基本的に望んだ結果にはならないため。

https://qiita.com/howdy39/items/35729490b024ca295d6c

bad

for (const x in someObj) {
}

good

for (const x of Object.keys(someObj)) { 
}

基本的にnullは使用せずundefinedに統一する。

  • undefinedに統一した方が楽であるため。
    ただし、以下の場合はnullの使用を認める。
  1. APIにおいてnullを使用している場合。例えばDOMのAPIの多くはnullを使用している。
  2. nullチェックを行う場合
  3. JSXのreturnにはnullを使用する。

https://typescriptbook.jp/reference/values-types-variables/undefined-vs-null

条件によっては全て実行する必要がない場合、早期リターンを使用する

  • 関数の処理がより効率的で明確になるため。

https://zenn.dev/media_engine/articles/early_return

bad

const today = new Date(2021, 10, 9)

const isActiveUser = (user) => {
    if (user != null) {
        if (user.startDate <= today && (user.endDate == null || today <= user.endDate)) {
            if(user.stopped) {
                return false;
            } else {
                return true;
            }
        } else {
            return false;
        }
    } else {
        return false;
    }
} 

const startDate = new Date(today);
startDate.setMonth(startDate.getMonth() - 1);

const user = {
    name: "iwa", 
    startDate: startDate,
    endDate: null,
    stopped: false
};

good

const isActiveUser = (user) => {
    if (user == null) { 
        return false;
    }
    
    if (today < user.startDate) { 
        return false;
    }
    
    if (user.endDate != null && user.endDate <= today) {
        return false;
    }
    
    if(user.stopped) {
        return false;
    }
    
    return true;
} 

参考資料

https://mitsuruog.github.io/javascript-style-guide/
https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules
https://typescriptbook.jp/
https://developer.mozilla.org/ja/docs/MDN/Writing_guidelines/Writing_style_guide/Code_style_guide/JavaScript
https://typescript-jp.gitbook.io/deep-dive/styleguide
https://google.github.io/styleguide/tsguide.html

Discussion

MeguriMeguri

突然のコメント失礼します。APIの効率的なテスト方法についての洞察がとても参考になりました。私も最近、APIテストツールの重要性を実感していて、特にオフラインでのテストができるツールが必要だと感じています。ECHOAPIというツールを使ってみたのですが、VS Codeとの統合がスムーズで、さまざまな機能が一つのツールにまとまっているのが魅力的です。
ありがとうございます。これからも素敵な記事を楽しみにしています!