📓
TypeScriptのコーディング規約を策定してみた!
はじめに
少し前の話ですが、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
値であることが伝わるため
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;
定数
定数とは
-
export
されている -
const
で定義されている - 今後、開発者が変更しない値
関数
アロー関数を使用する
- 可読性が高い
-
function
の一部の機能を必要としなくても、問題なく実装できるケースが多い - 引数や命名の重複を防ぐ
- プロジェクト内ですべての関数をアロー関数で統一できる
ただし、一般的にはfunction
宣言を優先すべきという意見も多い。
function
宣言のメリット
- ReactやNext.jsの公式ドキュメントでは、
function
宣言が使われている -
MDN Web Docsでも、
function
宣言を推奨している -
eslint-plugin-reactによると、名前付きエクスポートでは
function
宣言が推奨されている -
function
と明記することで、一目で関数とわかりやすくなる - コストパフォーマンスの面でも、
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時に対象のモジュールから削除されるので、バンドルサイズが軽減されるため。
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を使用している意味がなくなるため。破壊行為。
as const
は積極的に使用する。
Type assertionは極力使用しない。ただし、-
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)
if
もしくはswitch
文を使用する。
三項演算子は単1行にする。どうしてもネストする場合は、- 可読性が落ちるため
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
はプロトタイプチェーン全体を反復処理するので、基本的に望んだ結果にはならないため。
bad
for (const x in someObj) {
}
good
for (const x of Object.keys(someObj)) {
}
null
は使用せずundefined
に統一する。
基本的に-
undefined
に統一した方が楽であるため。
ただし、以下の場合はnull
の使用を認める。
- APIにおいて
null
を使用している場合。例えばDOMのAPIの多くはnull
を使用している。 -
null
チェックを行う場合 - JSXの
return
にはnull
を使用する。
条件によっては全て実行する必要がない場合、早期リターンを使用する
- 関数の処理がより効率的で明確になるため。
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;
}
参考資料
Discussion
突然のコメント失礼します。APIの効率的なテスト方法についての洞察がとても参考になりました。私も最近、APIテストツールの重要性を実感していて、特にオフラインでのテストができるツールが必要だと感じています。ECHOAPIというツールを使ってみたのですが、VS Codeとの統合がスムーズで、さまざまな機能が一つのツールにまとまっているのが魅力的です。
ありがとうございます。これからも素敵な記事を楽しみにしています!