TypeScriptの為のクリーンコード
clean-code-javascriptを見て閃きました。
- Introduction
- Variables
- Functions
- Objects and Data Structures
- Classes
- SOLID
- Testing
- Concurrency
- Error Handling
- Formatting
- Comments
- Translations
Robert C. Martinの書籍 Clean CodeをTypeScriptに対応させたソフトウェア工学の原則です。 翻訳書籍(amazonへのリンク) これはスタイルガイドではありません。 TypeScriptで可読性が高く、再利用可能であり、リファクタブルなソフトウェアを生産するためのガイドラインです。
すべての原則に厳密に従う必要はありません、さらに一般に合意されているものはさらに少くなります。 これらはガイドライン以上でしかありませんが、Clean Code の著者達による長年の経験を集めて文書化したものです。
ソフトウェア工学の歴史はほんの50年を少し超えた程度であり、未だに私達は多くのことを学び続けています。 ソフトウェアアーキテクチャが建築と同くらい歴史を持っていたならば、おそらく従うべき原則はより厳しくなっていたでしょう。 現時点では、このガイドラインは、あなたとあなたのチームが作成したTypeScriptコードの品質を評価するための基準として役立つでしょう。
それからもう一つ: これらを知ったからと言ってすぐに優秀なソフトウェア開発者となるわけではありませんし、長年これに従って作業を行っても間違いを犯さないわけではありません。 湿った粘土が最終的な形になるように、コードの各部分は最初のドラフト(ルール)になります。 最終的に同僚とこれをレビューする時、不完全な部分を取り除いていきます。 最初のドラフトに改善が必要となった時、自分自身を責めないでください。 代わりにコードを責めましょう!
何を意味してるかを読み手が区別できる名前を付けましょう。
Bad:
function between<T>(a1: T, a2: T, a3: T): boolean {
return a2 <= a1 && a1 <= a3;
}
Good:
function between<T>(value: T, left: T, right: T): boolean {
return left <= value && value <= right;
}
あなたがそれを発音できないなら、まぬけに聞こえてまともな論議になりません。
Bad:
type DtaRcrd102 = {
genymdhms: Date;
modymdhms: Date;
pszqint: number;
}
Good:
type Customer = {
generationTimestamp: Date;
modificationTimestamp: Date;
recordId: number;
}
Bad:
function getUserInfo(): User;
function getUserDetails(): User;
function getUserData(): User;
Good:
function getUser(): User;
私達は書いた以上のコードを読むでしょう。 そのため書いたコードは読みやすく探しやすいコードであることが重要になってきます。 プログラムを理解するのに重要な意味がある変数に名前を付けないことによって、私達は読み手を傷つけています。 名前を付ける時は検索しやすいものにしましょう。 TSLintのようなツールは、名前のない定数を識別するのに役立ちます。
Bad:
// What the heck is 86400000 for?
// 一体何が86400000なのか?
setTimeout(restart, 86400000);
Good:
// Declare them as capitalized named constants.
// 定数の名前は大文字で宣言してください。
const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000;
setTimeout(restart, MILLISECONDS_IN_A_DAY);
Bad:
declare const users: Map<string, User>;
for (const keyValue of users) {
// iterate through users map
// user map を反復処理する
}
Good:
declare const users: Map<string, User>;
for (const [id, user] of users) {
// iterate through users map
// user map を反復処理する
}
明示的は暗黙的より優れています。 明快さは王様です。
Bad:
const u = getUser();
const s = getSubscription();
const t = charge(u, s);
Good:
const user = getUser();
const subscription = getSubscription();
const transaction = charge(user, subscription);
もしあなたの class/type/object の名前が何かを伝えているのなら、あなたの変数名の中でそのことを繰り返さないでください。
Bad:
type Car = {
carMake: string;
carModel: string;
carColor: string;
}
function print(car: Car): void {
console.log(`${car.carMake} ${car.carModel} (${car.carColor})`);
}
Good:
type Car = {
make: string;
model: string;
color: string;
}
function print(car: Car): void {
console.log(`${car.make} ${car.model} (${car.color})`);
}
デフォルト引数は、短絡評価よりもきれいなことがよくあります。
Bad:
function loadPages(count?: number) {
const loadCount = count !== undefined ? count : 10;
// ...
}
Good:
function loadPages(count: number = 10) {
// ...
}
列挙型は、コードの目的を明文化するのに役立ちます。 例えば、私達は値の正確さよりも、その値が違うものを指していないかを心配します。
Bad:
const GENRE = {
ROMANTIC: 'romantic',
DRAMA: 'drama',
COMEDY: 'comedy',
DOCUMENTARY: 'documentary',
}
projector.configureFilm(GENRE.COMEDY);
class Projector {
// delactation of Projector
// 映写機を楽しむ
configureFilm(genre) {
switch (genre) {
case GENRE.ROMANTIC:
// some logic to be executed
// 実行されるいくつかのロジック
}
}
}
Good:
enum GENRE {
ROMANTIC,
DRAMA,
COMEDY,
DOCUMENTARY,
}
projector.configureFilm(GENRE.COMEDY);
class Projector {
// delactation of Projector
// 映写機を楽しむ
configureFilm(genre) {
switch (genre) {
case GENRE.ROMANTIC:
// some logic to be executed
// 実行されるいくつかのロジック
}
}
}
関数における引数の数を制限することは非常に重要です。 なぜならそれは貴方の関数のテストをシンプルにするからです。 3つ以上になると、引数ごとの数だけ違うケースをテストしなければならず、組み合わせは爆発的に増加します。
理想的な引数の数は1〜2個であり、3つは避けるべきです。 それ以上の数になるならば結合するべきです。 普通は2つ以上の引数がある場合、関数がやりすぎています。 そうでない場合は、上位のオブジェクトを引数にすれば十分です。
たくさんの引数が必要な場合はオブジェクトリテラルの利用を検討してください。
関数がどのようなプロパティを持っているかを明示的にするた めに、destructuring構文を使うことができます。
これにはいくつかの利点があります:
-
誰かが関数の入出力を見た時に、どのプロパティが利用されているのかがすぐにわかります。
-
分割代入は、関数に渡された引数オブジェクトのプリミティブ型の値を複製します。これは副作用を防ぐのに役立ちます 注釈:引数オブジェクトから分割代入されたObjectとArrayは複製されません。
-
TypeScriptは未使用のプロパティについて警告します、これは分割代入なしでは不可能でしょう。
Bad:
function createMenu(title: string, body: string, buttonText: string, cancellable: boolean) {
// ...
}
createMenu('Foo', 'Bar', 'Baz', true);
Good:
function createMenu(options: { title: string, body: string, buttonText: string, cancellable: boolean }) {
// ...
}
createMenu({
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
});
タイプエイリアスを使うことで、さらに読みやすさ向上させることができます。
type MenuOptions = { title: string, body: string, buttonText: string, cancellable: boolean };
function createMenu(options: MenuOptions) {
// ...
}
createMenu({
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
});
これはソフトウェア・エンジニアリングにおいて、とても重要なルールです。 関数が1つ以上のことをするとき、それは作成してテストし、推測することをより困難にします。 関数を一つの振る舞いに分離することができれば、それらは簡単にリファクタリングすることができるようになり、あなたのコードはとても綺麗になります。 このガイドのこの項以外を何もしなかったとしても、あなたは他の多くの開発者よりも一歩先を行ってるでしょう。
Bad:
function emailClients(clients: Client[]) {
clients.forEach((client) => {
const clientRecord = database.lookup(client);
if (clientRecord.isActive()) {
email(client);
}
});
}
Good:
function emailClients(clients: Client[]) {
clients.filter(isActiveClient).forEach(email);
}
function isActiveClient(client: Client) {
const clientRecord = database.lookup(client);
return clientRecord.isActive();
}
Bad:
function addToDate(date: Date, month: number): Date {
// ...
}
const date = new Date();
// It's hard to tell from the function name what is added
// 何が追加されたかを、関数名から予測することができません。
addToDate(date, 1);
Good:
function addMonthToDate(date: Date, month: number): Date {
// ...
}
const date = new Date();
addMonthToDate(date, 1);
あなたが1つ以上の抽象化を行っている時、関数はやりすぎています。 機能を分割すれば再利用性とテスト容易性を向上させることができます。
Bad:
function parseCode(code: string) {
const REGEXES = [ /* ... */ ];
const statements = code.split(' ');
const tokens = [];
REGEXES.forEach((regex) => {
statements.forEach((statement) => {
// ...
});
});
const ast = [];
tokens.forEach((token) => {
// lex...
});
ast.forEach((node) => {
// parse...
});
}
Good:
const REGEXES = [ /* ... */ ];
function parseCode(code: string) {
const tokens = tokenize(code);
const syntaxTree = parse(tokens);
syntaxTree.forEach((node) => {
// parse...
});
}
function tokenize(code: string): Token[] {
const statements = code.split(' ');
const tokens: Token[] = [];
REGEXES.forEach((regex) => {
statements.forEach((statement) => {
tokens.push( /* ... */ );
});
});
return tokens;
}
function parse(tokens: Token[]): SyntaxTree {
const syntaxTree: SyntaxTree[] = [];
tokens.forEach((token) => {
syntaxTree.push( /* ... */ );
});
return syntaxTree;
}
コードの重複を避ける事に最善を尽くしてください。 重複したコードがあるということは、ロジックの変更を行う時に複数の箇所に同じ変更をする必要があるため、よくありません。
貴方がレストランを経営していたとしましょう、そして在庫整理しているとします: トマト、玉ねぎ、ニンニク、スパイスなど、すべてあなたの物です。 貴方がこの在庫を管理するリストを複数持っていた場合、トマト料理を提供したらリストすべてを更新する必要がでてきます。 リストが1つだけなら、更新は1箇所済むでしょう!
共通点が多いが、2つ以上の小さな違いがあるために、コードが重複してしまうことはよくあります。 しかしその結果、その違いによってほとんど同じことを行う2つ以上の関数が必要になってしまいます。 重複したコードを削除するということは、一つの関数/モジュール/クラスで、これらの僅かに異なる一連のものを処理できる抽象化を作るということを意味します。
抽象化を正しく行うことが重要です。 そのため、SOLID の原則に従う必要があります。 悪い抽象化はコードの重複よりも悪い場合があるので注意してください! とは言うものの、あなたが良い抽象化をすることができるのであれば、是非行うべきです! 同じことをしないでください、そうしなければ何か一つを変更したい時、複数の場所を変更することになってしまいます。
Bad:
function showDeveloperList(developers: Developer[]) {
developers.forEach((developer) => {
const expectedSalary = developer.calculateExpectedSalary();
const experience = developer.getExperience();
const githubLink = developer.getGithubLink();
const data = {
expectedSalary,
experience,
githubLink
};
render(data);
});
}
function showManagerList(managers: Manager[]) {
managers.forEach((manager) => {
const expectedSalary = manager.calculateExpectedSalary();
const experience = manager.getExperience();
const portfolio = manager.getMBAProjects();
const data = {
expectedSalary,
experience,
portfolio
};
render(data);
});
}
Good:
class Developer {
// ...
getExtraDetails() {
return {
githubLink: this.githubLink,
}
}
}
class Manager {
// ...
getExtraDetails() {
return {
portfolio: this.portfolio,
}
}
}
function showEmployeeList(employee: Developer | Manager) {
employee.forEach((employee) => {
const expectedSalary = employee.calculateExpectedSalary();
const experience = employee.getExperience();
const extra = employee.getExtraDetails();
const data = {
expectedSalary,
experience,
extra,
};
render(data);
});
}
あなたはコードの重複について批判的であるべきです。 不必要な抽象化を導入することによって、コードの重複と複雑さの増大との間にトレードオフがあることがあります。 2つの異なるモジュールから呼ばれている2つの実装が似ているように見えても、異なるドメインに存在する場合は重複が許容され、共通コードの抽出よりも優先される場合があります。 この時に抽出された共通コードは、2つのモジュールの間に間接的な依存関係をもたらします。
Bad:
type MenuConfig = { title?: string, body?: string, buttonText?: string, cancellable?: boolean };
function createMenu(config: MenuConfig) {
config.title = config.title || 'Foo';
config.body = config.body || 'Bar';
config.buttonText = config.buttonText || 'Baz';
config.cancellable = config.cancellable !== undefined ? config.cancellable : true;
// ...
}
createMenu({ body: 'Bar' });
Good:
type MenuConfig = { title?: string, body?: string, buttonText?: string, cancellable?: boolean };
function createMenu(config: MenuConfig) {
const menuConfig = Object.assign({
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
}, config);
// ...
}
createMenu({ body: 'Bar' });
デフォルト値を利用した分割代入を使う手もあるでしょう:
type MenuConfig = { title?: string, body?: string, buttonText?: string, cancellable?: boolean };
function createMenu({ title = 'Foo', body = 'Bar', buttonText = 'Baz', cancellable = true }: MenuConfig) {
// ...
}
createMenu({ body: 'Bar' });
明示的に undefined
や null
の値を渡して副作用や予期しない動作を避けるため、TypeScriptコンパイラにそれを許容させない設定もできます。
TypeScriptの --strictNullChecks
設定を参照
フラグは、この関数が複数の事をしていと利用者に伝えています。 関数は一つのことをするべきなので、Boolean値によって異なる処理をしている場合は関数を別にします。
Bad:
function createFile(name: string, temp: boolean) {
if (temp) {
fs.create(`./temp/${name}`);
} else {
fs.create(name);
}
}
Good:
function createTempFile(name: string) {
createFile(`./temp/${name}`);
}
function createFile(name: string) {
fs.create(name);
}
関数が値を受け取り何かを返す以外の事をした場合、副作用を引き起こします。 副作用とはファイルへの書き込みや、グローバル変数の変更、間違って全財産を見知らぬ他人に振り込んでしまうような事です。
前に上げた例のように、ファイルに書き込む必要があるかもしれません。 やるべきことは、それを行う場所を一つの場所に留めることです。 特定のファイルに書き込む関数やクラスが複数存在しないようにしてください。 それをする、唯一無二のサービスを作ります。
重要なのは、構造を持たずオブジェクト間で状態を共有したり、任意のものに書き込み可能な可変データ型を使ったり、副作用が発生する場所を一箇所にしないなどの、一般的な落とし穴を避けることです。 貴方がこれをできるようになれば、大多数の他のプログラマーより幸せになれるでしょう。
Bad:
// Global variable referenced by following function.
// 以下の関数で利用されるグローバル変数
let name = 'Robert C. Martin';
function toBase64() {
name = btoa(name);
}
toBase64();
// If we had another function that used this name, now it'd be a Base64 value
// 変数 name を使用した別の関数がある場合は、その中身はBase64になります。
console.log(name); // expected to print 'Robert C. Martin' but instead 'Um9iZXJ0IEMuIE1hcnRpbg=='
// 'Robert C. Martin' と表示されるはずが、'Um9iZXJ0IEMuIE1hcnRpbg=='と表示されてしまう
Good:
const name = 'Robert C. Martin';
function toBase64(text: string): string {
return btoa(text);
}
const encodedName = toBase64(name);
console.log(name);
JavaScriptではプリミティブ型は値で渡され、オブジェクトや配列は参照によって渡されます。 オブジェクトや配列の場合、商品を購入するなどしてショッピングカート配列を更新した場合、ショッピングカート配列を使用する他のすべての関数が追加の影響を受けてしまいます。 それは素晴らしく思えますが、悪いことも起こります。 悪い状況を想像してみましょう。
ユーザーが"購入"ボタンをクリックすると、ネットワークのリクエストを生成してカート配列をサーバーに送信する、purchase 関数が呼ばれます。 ネットワークの状況が悪いため、purchase 関数は要求を繰り返し続けなければいけません。 そして、ネットワークのリクエストが成功する前にユーザーがほしくないアイテムを"カートに入れる"ボタンを誤って押してしまったらどうなるでしょうか? ネットワークのリクエストが成功した時 addItemToCart 関数はカート配列への参照を持っていたため、 不要なアイテムをカート配列へ追加してしまいます、purchase 関数は間違って追加された商品を送信してしまいます。
良い解決策は addItemToCart 関数が常にカートのクローンを作成し、それを編集してさらにクローンを返すことです。 これで、ショッピングカート配列の参照を保持している他の関数が変更の影響を受けなくなります。
このアプローチにおける2つの注意点:
-
時には渡されたオブジェクト型を変更したい場合があるケースがありますが、このプログラミング手法を採用している場合にそういったケースは稀ということに気づくでしょう。 そして殆どの場合、副作用がないようなリファクタリングを行うことが可能です。 (純粋関数を参照)
-
大きなオブジェクトの複製を作成すると、パフォーマンスの面で非常に高コストになってしまう可能性があります。幸運なことに、これは実際には大きな問題ではありません。 なぜなら、手動でオブジェクトや配列を複製するのとは異なり、この種のプログラミング手法をより高速でメモリ使用量を抑えた素晴らしいライブラリがあるからです。
Bad:
function addItemToCart(cart: CartItem[], item: Item): void {
cart.push({ item, date: Date.now() });
};
Good:
function addItemToCart(cart: CartItem[], item: Item): CartItem[] {
return [...cart, { item, date: Date.now() }];
};
グローバルを汚染するのはJavaScriptのバッドプラクティスです。
なぜなら他のライブラリをクラッシュさせるかもしれないし、あなたのAPIを使ってるユーザーは本番環境で例外が発生するまで何が起こってるか分からないからです。
例を考えてみましょう。
2つの配列の違いを差分を出すdiffメソッドを、既存JavaScriptのArrayに追加したらどうなりますか?
新しい関数を Array.prototype
に書くことはできますが、同じことをしている他のライブラリと衝突する可能性があります。
他のライブラリが単に配列の最初の要素と最後の要素の違いを見つけるために diff
を使っていたとしたらどうなるでしょうか。
これが、グローバルのArray
を拡張するより class
を使ったほうが良い理由です。
Bad:
declare global {
interface Array<T> {
diff(other: T[]): Array<T>;
}
}
if (!Array.prototype.diff) {
Array.prototype.diff = function <T>(other: T[]): T[] {
const hash = new Set(other);
return this.filter(elem => !hash.has(elem));
};
}
Good:
class MyArray<T> extends Array<T> {
diff(other: T[]): T[] {
const hash = new Set(other);
return this.filter(elem => !hash.has(elem));
};
}
できるなら、あなたはこのプログラミングスタイルを好きになってください。
Bad:
const contributions = [
{
name: 'Uncle Bobby',
linesOfCode: 500
}, {
name: 'Suzie Q',
linesOfCode: 1500
}, {
name: 'Jimmy Gosling',
linesOfCode: 150
}, {
name: 'Gracie Hopper',
linesOfCode: 1000
}
];
let totalOutput = 0;
for (let i = 0; i < contributions.length; i++) {
totalOutput += contributions[i].linesOfCode;
}
Good:
const contributions = [
{
name: 'Uncle Bobby',
linesOfCode: 500
}, {
name: 'Suzie Q',
linesOfCode: 1500
}, {
name: 'Jimmy Gosling',
linesOfCode: 150
}, {
name: 'Gracie Hopper',
linesOfCode: 1000
}
];
const totalOutput = contributions
.reduce((totalLines, output) => totalLines + output.linesOfCode, 0);
Bad:
if (subscription.isTrial || account.balance > 0) {
// ...
}
Good:
function canActivateService(subscription: Subscription, account: Account) {
return subscription.isTrial || account.balance > 0
}
if (canActivateService(subscription, account)) {
// ...
}
Bad:
function isEmailNotUsed(email: string): boolean {
// ...
}
if (isEmailNotUsed(email)) {
// ...
}
Good:
function isEmailUsed(email): boolean {
// ...
}
if (!isEmailUsed(node)) {
// ...
}
これは一見不可能な作業に見えます。 これを聞いた時、ほとんどの人は「if文を使わないとしたらどうすれば良いのか?」と言います。 この答えは、多くの場合、同じタスクをするためにポリモーフィズム(多態性・多様性)を利用するということです。 2つめの質問としてよくあるのは「素晴らしいことだと思うが、なんでそれをする必要があるのか?」といったものです。 この答えは、私達が先に学んだクリーンコードの概念「関数はただ一つのことをするべき」だからです。 貴方のクラスや関数がif文を持っている時、この関数は複数のことを示しています。 関数は唯一つのことをやるということを覚えておいてください。
Bad:
class Airplane {
private type: string;
// ...
getCruisingAltitude() {
switch (this.type) {
case '777':
return this.getMaxAltitude() - this.getPassengerCount();
case 'Air Force One':
return this.getMaxAltitude();
case 'Cessna':
return this.getMaxAltitude() - this.getFuelExpenditure();
default:
throw new Error('Unknown airplane type.');
}
}
private getMaxAltitude(): number {
// ...
}
}
Good:
abstract class Airplane {
protected getMaxAltitude(): number {
// shared logic with subclasses ...
}
// ...
}
class Boeing777 extends Airplane {
// ...
getCruisingAltitude() {
return this.getMaxAltitude() - this.getPassengerCount();
}
}
class AirForceOne extends Airplane {
// ...
getCruisingAltitude() {
return this.getMaxAltitude();
}
}
class Cessna extends Airplane {
// ...
getCruisingAltitude() {
return this.getMaxAltitude() - this.getFuelExpenditure();
}
}
TypeScriptはJavaScriptの厳密な構文スーパセットであり、言語へ静的型チェックのオプションを追加します。 TypeScriptの機能を最大限に活用するには、常に変数、パラメータ、戻り値の型を指定する事をおすすめします。 これはリファクタリングをより簡単にします。
Bad:
function travelToTexas(vehicle: Bicycle | Car) {
if (vehicle instanceof Bicycle) {
vehicle.pedal(currentLocation, new Location('texas'));
} else if (vehicle instanceof Car) {
vehicle.drive(currentLocation, new Location('texas'));
}
}
Good:
type Vehicle = Bicycle | Car;
function travelToTexas(vehicle: Vehicle) {
vehicle.move(currentLocation, new Location('texas'));
}
今のブラウザは実行時に内部で多くの最適化を行っています。 貴方が最適化に多数の時間を費やしてるなら、あなたは時間を無駄にしています。 最適化ができていないところを見るための良い資料があります。 それらが最適化されるまでは、そこだけを最適化の対象にしてください。
Bad:
// On old browsers, each iteration with uncached `list.length` would be costly
// because of `list.length` recomputation. In modern browsers, this is optimized.
// 古いブラウザは、キャッシュされていない `list.length` を使った繰り返しはコストがかかるでしょう
// なぜなら、呼ばれるたびに `list.length` が再計算されるから。 モダンブラウザは最適化されている
for (let i = 0, len = list.length; i < len; i++) {
// ...
}
Good:
for (let i = 0; i < list.length; i++) {
// ...
}
使われていないコードは重複したコードと同じくらい悪いものです。 あなたのコードにそれを残しておく理由はありません。 もし参照がないなら取り除きましょう!必要と感じていたとしてもバージョン管理ツールの履歴に残っているだけで十分でしょう。
Bad:
function oldRequestModule(url: string) {
// ...
}
function requestModule(url: string) {
// ...
}
const req = requestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');
Good:
function requestModule(url: string) {
// ...
}
const req = requestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');
ストリームデータを持つコレクションを扱うときは、ジェネレーターとイテレーターを使ってください。 いくつかの理由があります:
- 呼び出し先でどの項目にアクセスするかを決めさせることで、呼び出し先とジェネレータの実装を切り離します
- 遅延実行、アイテムはユーザーの要求毎にストリーミングされます
for-of
構文を使った組み込みの反復処理をサポートしています- iterables により最適化されたイテレータパターンを実装することがでるようになります
Bad:
function fibonacci(n: number): number[] {
if (n === 1) return [0];
if (n === 2) return [0, 1];
const items: number[] = [0, 1];
while (items.length < n) {
items.push(items[items.length - 2] + items[items.length - 1]);
}
return items;
}
function print(n: number) {
fibonacci(n).forEach(fib => console.log(fib));
}
// Print first 10 Fibonacci numbers.
// フィボナッチ数の最初の10個をprintする
print(10);
Good:
// Generates an infinite stream of Fibonacci numbers.
// The generator doesn't keep the array of all numbers.
// フィボナッチ数の無限のストリームを生成します
// ジェネレータは全数の配列を保持しません
function* fibonacci(): IterableIterator<number> {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
function print(n: number) {
let i = 0;
for (const fib of fibonacci()) {
if (i++ === n) break;
console.log(fib);
}
}
// Print first 10 Fibonacci numbers.
// フィボナッチ数の最初の10個をprintする
print(10);
map
、 slice
、 forEach
などのメソッドをチェーンさせることで、ネイティブの配列と同じようなイテラブルを扱うことを可能にするライブラリもあります。
(イテラブルを使用した高度な操作の例についてはitiririを参照、または非同期イテラブルの操作については itiriri-async を参照)
import itiriri from 'itiriri';
function* fibonacci(): IterableIterator<number> {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
itiriri(fibonacci())
.take(10)
.forEach(fib => console.log(fib));
TypeScriptはgetter/setter構文をサポートしています。 getterとsetterを使って振る舞いをカプセル化してオブジェクトにアクセスするほうが、単純なプロパティでオブジェクトにアクセスするよりも優れている可能性があります。 「何故?」と思われるかもしれませんが、以下がその理由の一覧です:
- もしオブジェクトのプロパティを取得する以上のことをしてる場合、コード内のすべてのアクセサを調べて変更する必要がありません。
- set を使うとバリデーションが追加できます。
- 内部をカプセル化できます。
- 値を取得や設定する時にログやエラー処理を追加するのが容易になります。
- オブジェクトのプロパティを遅延ロードすることができるようになります、例えばサーバから値を取得する時などです。
Bad:
type BankAccount = {
balance: number;
// ...
}
const value = 100;
const account: BankAccount = {
balance: 0,
// ...
};
if (value < 0) {
throw new Error('Cannot set negative balance.');
}
account.balance = value;
Good:
class BankAccount {
private accountBalance: number = 0;
get balance(): number {
return this.accountBalance;
}
set balance(value: number) {
if (value < 0) {
throw new Error('Cannot set negative balance.');
}
this.accountBalance = value;
}
// ...
}
// Now `BankAccount` encapsulates the validation logic.
// If one day the specifications change, and we need extra validation rule,
// we would have to alter only the `setter` implementation,
// leaving all dependent code unchanged.
// これで `BankAccount` はバリデーション処理をカプセル化しました。
// ある日仕様が変更されて、追加のバリデーションが必要になった場合にも
// `setter`の実装だけを変更すればよく
// すべての依存したコードを変更する必要はありません。
const account = new BankAccount();
account.balance = 100;
TypeScriptはクラスメンバーに対してpublic
、(default)、protected
、private
アクセサをサポートしています。
Bad:
class Circle {
radius: number;
constructor(radius: number) {
this.radius = radius;
}
perimeter() {
return 2 * Math.PI * this.radius;
}
surface() {
return Math.PI * this.radius * this.radius;
}
}
Good:
class Circle {
constructor(private readonly radius: number) {
}
perimeter() {
return 2 * Math.PI * this.radius;
}
surface() {
return Math.PI * this.radius * this.radius;
}
}
TypeScriptの型システムではインタフェース/クラスのプロパティに readonly とマークすることができます。
これを用いると、あなたは関数型で書くことができるようになります。
(予想外の変更は良くないものです)
より高度なシナリオでは、組み込み型の Readonly
があります。これはT
型 と map型を併用してすべてのプロパティを読み取り専用としてマークします。
(マップ型を参照)
Bad:
interface Config {
host: string;
port: string;
db: string;
}
Good:
interface Config {
readonly host: string;
readonly port: string;
readonly db: string;
}
Arrayの場合にはReadonlyArray<T>
を使って読み取り専用の配列を作ることができます。
push()
や fill()
のような変更はできませんが、値を変えない concat()
や slice()
と言った機能は使うことができます。
Bad:
const array: number[] = [ 1, 3, 5 ];
array = []; // error
array.push(100); // array will updated
Good:
const array: ReadonlyArray<number> = [ 1, 3, 5 ];
array = []; // error
array.push(100); // error
TypeScript 3.4で読み取り専用引数を宣言するのが少し簡単になりました。
function hoge(args: readonly string[]) {
args.push(1); // error
}
リテラル値にはconst assertionsを好んで使うと良いでしょう。
Bad:
const config = {
hello: 'world'
};
config.hello = 'world'; // value is changed
const array = [ 1, 3, 5 ];
array[0] = 10; // value is changed
// writable objects is returned
function readonlyData(value: number) {
return { value };
}
const result = readonlyData(100);
result.value = 200; // value is changed
Good:
// read-only object
const config = {
hello: 'world'
} as const;
config.hello = 'world'; // error
// read-only array
const array = [ 1, 3, 5 ] as const;
array[0] = 10; // error
// You can return read-only objects
function readonlyData(value: number) {
return { value } as const;
}
const result = readonlyData(100);
result.value = 200; // error
union や intersection が必要な場合は type を使用してください。
extends や implements がほしいときにはinterfaceを使います。
厳密なルールはありませんが、その時に合ったものを使ってください。
TypeScriptの type
と interface
の違いについてのより詳細な説明はこちらの 回答 を参照してください。
Bad:
interface EmailConfig {
// ...
}
interface DbConfig {
// ...
}
interface Config {
// ...
}
//...
type Shape = {
// ...
}
Good:
type EmailConfig = {
// ...
}
type DbConfig = {
// ...
}
type Config = EmailConfig | DbConfig;
// ...
interface Shape {
// ...
}
class Circle implements Shape {
// ...
}
class Square implements Shape {
// ...
}
責任の範囲によってクラスの大きさを定めます。単一責任の原則 に従ってクラスは小さくなければいけません。
Bad:
class Dashboard {
getLanguage(): string { /* ... */ }
setLanguage(language: string): void { /* ... */ }
showProgress(): void { /* ... */ }
hideProgress(): void { /* ... */ }
isDirty(): boolean { /* ... */ }
disable(): void { /* ... */ }
enable(): void { /* ... */ }
addSubscription(subscription: Subscription): void { /* ... */ }
removeSubscription(subscription: Subscription): void { /* ... */ }
addUser(user: User): void { /* ... */ }
removeUser(user: User): void { /* ... */ }
goToHomePage(): void { /* ... */ }
updateProfile(details: UserDetails): void { /* ... */ }
getVersion(): string { /* ... */ }
// ...
}
Good:
class Dashboard {
disable(): void { /* ... */ }
enable(): void { /* ... */ }
getVersion(): string { /* ... */ }
}
// split the responsibilities by moving the remaining methods to other classes
// 他のクラスにメソッドを移動して責任を分割する
// ...
凝集度は、クラスメンバーが互いにどの程度関連しているかを定義します。理想的には、プログラム内の全てのフィールドがクラスの各メソッドを使用できることです。 このような状態を、クラスは 凝集度が高い と言います。ただ、実際これは常に可能ではありませんが、可能な限り凝集度を高くすることが賢明といえます。
結合度とは、2つのクラスが互いにどの程度依存しているかを指します。片方の変更がもう片方に影響しない場合、互いのクラスは疎結合と言われます。
優れたソフトウェア設計は、 高い凝集度 と 低い結合度 を内包しています。
Bad:
class UserManager {
// Bad: each private variable is used by one or another group of methods.
// It makes clear evidence that the class is holding more than a single responsibility.
// If I need only to create the service to get the transactions for a user,
// I'm still forced to pass and instance of `emailSender`.
// 悪い: 各プライベート変数は、メソッドの1つまたは別のグループによって使用されます。
// それは、クラスが1つ以上の責任を保持しているという明確な証拠を表しています。
// ユーザーのトランザクションを取得するためだけにサービスを作成する必要がある場合は、
// `emailSender` のインスタンスを渡すことを余儀なくされています。
constructor(
private readonly db: Database,
private readonly emailSender: EmailSender) {
}
async getUser(id: number): Promise<User> {
return await db.users.findOne({ id });
}
async getTransactions(userId: number): Promise<Transaction[]> {
return await db.transactions.find({ userId });
}
async sendGreeting(): Promise<void> {
await emailSender.send('Welcome!');
}
async sendNotification(text: string): Promise<void> {
await emailSender.send(text);
}
async sendNewsletter(): Promise<void> {
// ...
}
}
Good:
class UserService {
constructor(private readonly db: Database) {
}
async getUser(id: number): Promise<User> {
return await this.db.users.findOne({ id });
}
async getTransactions(userId: number): Promise<Transaction[]> {
return await this.db.transactions.find({ userId });
}
}
class UserNotifier {
constructor(private readonly emailSender: EmailSender) {
}
async sendGreeting(): Promise<void> {
await this.emailSender.send('Welcome!');
}
async sendNotification(text: string): Promise<void> {
await this.emailSender.send(text);
}
async sendNewsletter(): Promise<void> {
// ...
}
}
有名なGang of Four(四人組)によるデザインパターンのように、 可能な場所では継承よりも合成集約を優先するべきです。 継承を利用する良い理由も合成集約を利用する良い理由もたくさんあります。 この章の要点は、もしあなたが本能的に継承を使うように心が動くのであれば、合成集約がその問題をよりよくモデル化できるかどうか考えてみてください。 いくつかの場合、それができます。
あなたは、「いつ継承を使うべきか?」について疑問に思うかもしれません。 それは、あなたの持つ問題次第です。ただ、これは、継承が合成集約よりも理にかなってる場合の一覧です。
-
継承が「has-a」ではなくて「is-a」を表している場合(人間は動物である と 人は属性情報を含んでいる)
-
基底クラスからコードを再利用できる(人は動物のように動くことができる)
-
基底クラスを変更することで、派生クラスを全体的に変更したい(全ての動物の移動中の消費カロリーを変更する)
Bad:
class Employee {
constructor(
private readonly name: string,
private readonly email: string) {
}
// ...
}
// Bad because Employees "have" tax data. EmployeeTaxData is not a type of Employee
// よくない。なぜなら、従業員(Employee)は税情報を持っている。しかし、従業員税情報(EmployeeTaxData)は従業員ではない
class EmployeeTaxData extends Employee {
constructor(
name: string,
email: string,
private readonly ssn: string,
private readonly salary: number) {
super(name, email);
}
// ...
}
Good:
class Employee {
private taxData: EmployeeTaxData;
constructor(
private readonly name: string,
private readonly email: string) {
}
setTaxData(ssn: string, salary: number): Employee {
this.taxData = new EmployeeTaxData(ssn, salary);
return this;
}
// ...
}
class EmployeeTaxData {
constructor(
public readonly ssn: string,
public readonly salary: number) {
}
// ...
}
このパターンは非常に便利なので、多くのライブラリでよく使われています。これにより、貴方のコードは表現力豊かで、冗長ではなくなります。というわけで、メソッドチェーンを使って、あなたのコードがどれくらい綺麗になるか見てください。
Bad:
class QueryBuilder {
private collection: string;
private pageNumber: number = 1;
private itemsPerPage: number = 100;
private orderByFields: string[] = [];
from(collection: string): void {
this.collection = collection;
}
page(number: number, itemsPerPage: number = 100): void {
this.pageNumber = number;
this.itemsPerPage = itemsPerPage;
}
orderBy(...fields: string[]): void {
this.orderByFields = fields;
}
build(): Query {
// ...
}
}
// ...
const queryBuilder = new QueryBuilder();
queryBuilder.from('users');
queryBuilder.page(1, 100);
queryBuilder.orderBy('firstName', 'lastName');
const query = queryBuilder.build();
Good:
class QueryBuilder {
private collection: string;
private pageNumber: number = 1;
private itemsPerPage: number = 100;
private orderByFields: string[] = [];
from(collection: string): this {
this.collection = collection;
return this;
}
page(number: number, itemsPerPage: number = 100): this {
this.pageNumber = number;
this.itemsPerPage = itemsPerPage;
return this;
}
orderBy(...fields: string[]): this {
this.orderByFields = fields;
return this;
}
build(): Query {
// ...
}
}
// ...
const query = new QueryBuilder()
.from('users')
.page(1, 100)
.orderBy('firstName', 'lastName')
.build();
Clean Code では「クラス変更される理由は1つ以上あってはならない」と述べられています。 飛行機へ乗る時にスーツケースを一つしか持てないように、多くの機能をクラスに詰め込むのは魅力的に見えます。 これに関する問題は、あなたのクラスが概念的にまとまりがなく、変化を許容する理由をたくさん持ってしまうということです。 クラスを変更する理由を最小限に留めることが重要になります。 ここが重要な理由は、1つのクラスにたくさんの機能がある場合に、その一部を変更した時、変更箇所がコードベース内の他の依存モジュールにどう影響するのかを理解するのが難しくなってしまうからです。
Bad:
class UserSettings {
constructor(private readonly user: User) {
}
changeSettings(settings: UserSettings) {
if (this.verifyCredentials()) {
// ...
}
}
verifyCredentials() {
// ...
}
}
Good:
class UserAuth {
constructor(private readonly user: User) {
}
verifyCredentials() {
// ...
}
}
class UserSettings {
private readonly auth: UserAuth;
constructor(private readonly user: User) {
this.auth = new UserAuth(user);
}
changeSettings(settings: UserSettings) {
if (this.auth.verifyCredentials()) {
// ...
}
}
}
Bertrand Meyer 氏曰く、「ソフトウェアの関連(クラス、モジュール、関数など)は拡張のために開かれているべきですが、変更のためには閉じられているべき」と言われています。 これはどういう意味かというと、この原則は基本的に、既存のコードを変更せずにユーザーが新しい機能を追加できるようにするべきと言う意味です。
Bad:
class AjaxAdapter extends Adapter {
constructor() {
super();
}
// ...
}
class NodeAdapter extends Adapter {
constructor() {
super();
}
// ...
}
class HttpRequester {
constructor(private readonly adapter: Adapter) {
}
async fetch<T>(url: string): Promise<T> {
if (this.adapter instanceof AjaxAdapter) {
const response = await makeAjaxCall<T>(url);
// transform response and return
} else if (this.adapter instanceof NodeAdapter) {
const response = await makeHttpCall<T>(url);
// transform response and return
}
}
}
function makeAjaxCall<T>(url: string): Promise<T> {
// request and return promise
}
function makeHttpCall<T>(url: string): Promise<T> {
// request and return promise
}
Good:
abstract class Adapter {
abstract async request<T>(url: string): Promise<T>;
// code shared to subclasses ...
}
class AjaxAdapter extends Adapter {
constructor() {
super();
}
async request<T>(url: string): Promise<T>{
// request and return promise
}
// ...
}
class NodeAdapter extends Adapter {
constructor() {
super();
}
async request<T>(url: string): Promise<T>{
// request and return promise
}
// ...
}
class HttpRequester {
constructor(private readonly adapter: Adapter) {
}
async fetch<T>(url: string): Promise<T> {
const response = await this.adapter.request<T>(url);
// transform response and return
}
}
これは非常に単純な概念と言うには恐ろしいものです。 S型がT型の派生型の場合、T型のオブジェクトはプログラムの特性(正確さや処理など)を変更すること無く、S型で置き換えることができる(つまり、S型のオブジェクトをT型のオブジェクトと置き換えることができる)というやっかいな定義です。
これに対する良い説明は、親クラスと子クラスがある場合に、親クラスと子クラスは異なる振る舞いを起こさず、互換性を持っているということです。 まだ難しいと思うので、古典的な正方形と長方形の例を見てみましょう。 数学的には正方形は長方形ですが、継承による「is-a」関係を使いモデル化すると、問題が発生しやすくなります。
Bad:
class Rectangle {
constructor(
protected width: number = 0,
protected height: number = 0) {
}
setColor(color: string): this {
// ...
}
render(area: number) {
// ...
}
setWidth(width: number): this {
this.width = width;
return this;
}
setHeight(height: number): this {
this.height = height;
return this;
}
getArea(): number {
return this.width * this.height;
}
}
class Square extends Rectangle {
setWidth(width: number): this {
this.width = width;
this.height = width;
return this;
}
setHeight(height: number): this {
this.width = height;
this.height = height;
return this;
}
}
function renderLargeRectangles(rectangles: Rectangle[]) {
rectangles.forEach((rectangle) => {
const area = rectangle
.setWidth(4)
.setHeight(5)
.getArea(); // BAD: Returns 25 for Square. Should be 20.
rectangle.render(area);
});
}
const rectangles = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles(rectangles);
Good:
abstract class Shape {
setColor(color: string): this {
// ...
}
render(area: number) {
// ...
}
abstract getArea(): number;
}
class Rectangle extends Shape {
constructor(
private readonly width = 0,
private readonly height = 0) {
super();
}
getArea(): number {
return this.width * this.height;
}
}
class Square extends Shape {
constructor(private readonly length: number) {
super();
}
getArea(): number {
return this.length * this.length;
}
}
function renderLargeShapes(shapes: Shape[]) {
shapes.forEach((shape) => {
const area = shape.getArea();
shape.render(area);
});
}
const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)];
renderLargeShapes(shapes);
ISPでは「クライアントは彼らが利用していないインタフェースへの依存を強要してはならない」と述べられています。 この原則は、単一責任原則と非常に密接な関係にあります。 これが本当に意味することは、公開されたメソッドを使用しているクライアントが、すべてのパイを取得しないよう抽象化を常に設計する必要があるという事です。 これは実際には必要としないメソッドを実装するという負荷をクライアント側に課す事も含まれます。
Bad:
interface SmartPrinter {
print();
fax();
scan();
}
class AllInOnePrinter implements SmartPrinter {
print() {
// ...
}
fax() {
// ...
}
scan() {
// ...
}
}
class EconomicPrinter implements SmartPrinter {
print() {
// ...
}
fax() {
throw new Error('Fax not supported.');
}
scan() {
throw new Error('Scan not supported.');
}
}
Good:
interface Printer {
print();
}
interface Fax {
fax();
}
interface Scanner {
scan();
}
class AllInOnePrinter implements Printer, Fax, Scanner {
print() {
// ...
}
fax() {
// ...
}
scan() {
// ...
}
}
class EconomicPrinter implements Printer {
print() {
// ...
}
}
この原則では2つの重要な事を述べています:
-
上位モジュールは下位モジュールに依存してはいけません、どちらも抽象化に依存するべきです。
-
抽象化は実装に依存してはいけません、実装は抽象化に依存するべきです。
最初にこれを理解するのは難しいかもしれませんが Angularで作業したことがある場合はこの原則が依存性注入(DI)の形で実装されるのを見たことがあるでしょう。 それは同一の概念ではありませんが、DIPは高レベルのモジュールが低レベルのモジュールの詳細を知るが、それらが設定されるのを防ぎます。 これはDIで成し遂げることが出来ます。 これはモジュール間の結合を減らすことが大きなメリットになります。 密結合はコードのリファクタリングを困難にするので、非常に良くない開発パターンです。
Bad:
import { readFile as readFileCb } from 'fs';
import { promisify } from 'util';
const readFile = promisify(readFileCb);
type ReportData = {
// ..
}
class XmlFormatter {
parse<T>(content: string): T {
// Converts an XML string to an object T
// XML文字列をTオブジェクトに変換します。
}
}
class ReportReader {
// BAD: We have created a dependency on a specific request implementation.
// We should just have ReportReader depend on a parse method: `parse`
// 悪い:特定のリクエストに依存する実装を作りました。
// ReportReaderを parseメソッドに依存させています:`parse`
private readonly formatter = new XmlFormatter();
async read(path: string): Promise<ReportData> {
const text = await readFile(path, 'UTF8');
return this.formatter.parse<ReportData>(text);
}
}
// ...
const reader = new ReportReader();
await report = await reader.read('report.xml');
Good:
import { readFile as readFileCb } from 'fs';
import { promisify } from 'util';
const readFile = promisify(readFileCb);
type ReportData = {
// ..
}
interface Formatter {
parse<T>(content: string): T;
}
class XmlFormatter implements Formatter {
parse<T>(content: string): T {
// Converts an XML string to an object T
// XML文字列をTオブジェクトに変換します。
}
}
class JsonFormatter implements Formatter {
parse<T>(content: string): T {
// Converts a JSON string to an object T
// json文字列をTオブジェクトに変換します。
}
}
class ReportReader {
constructor(private readonly formatter: Formatter) {
}
async read(path: string): Promise<ReportData> {
const text = await readFile(path, 'UTF8');
return this.formatter.parse<ReportData>(text);
}
}
// ...
const reader = new ReportReader(new XmlFormatter());
await report = await reader.read('report.xml');
// or if we had to read a json report
// JSONレポートを読む場合
const reader = new ReportReader(new JsonFormatter());
await report = await reader.read('report.json');
テストはリリースよりも重要です。 あなたのテストをしていないか、不十分であった場合、あなたがリリースするたびに何も壊していないという確信を得ることは無いでしょう。 何が適切な量を構成するのは貴方のチーム次第ですが、カバレッジ100%(すべてのステートメントと分岐を含め)にするということは、非常に高い信頼性とともに、開発者の安心を持つことが出来ます。 つまり、優れたテストフレームワークとカバレッジツールを使う必要があります。
テストを書かない理由はありません。 TypeScriptの型をサポートする優れたJSテストフレームワークはたくさんあるので、チームが好むものを見つけてください。 自分のチームに合ったものが見つかったら導入するすべての新しい機能やモジュールに対して常にテストを書くことを目標にします。 もし貴方がテスト駆動開発(TDD)であるなら素晴らしいでしょう。 しかし、重要なのは機能を動かす前や既存の機能をリファクタする前にカバレッジの目標を達成していることを確実にすることです。
-
失敗した単体テストに合格しない限り、実コードを書くことはできません。
-
失敗させるためにしか単体テストは書いてはいけません。コンパイルエラーは失敗に数えます。
-
単体テストを1つだけ成功させる以上に、 プロダクトコードを書いてはならない。
綺麗なテストは以下の規則に従います:
-
Fast テストは頻繁に実行したいので早いはずです。
-
Independent テストは互いに依存してはいけません。それらは独立して実行されても任意の順番でまとめて実行されても同じ結果を返します。
-
Repeatable テストはどのような環境でも繰り返し実行可能であるべきで、何故失敗するかについての言い訳をしてはいけません。
-
Self-Validating テストは 合格 と 不合格 のどちらかしかありえません。テストに合格した場合にログファイルを比較して回答したりしないでください。
-
Timely 単体テストはプロダクトコードの前に書かれるべきです。プロダクトコードの後にテストを書くとテストを書くのは難しくなる可能性があります。
テストは 単一責任原則 に従うべきです。単体テスト毎にアサーションを1つ持ちましょう。
Bad:
import { assert } from 'chai';
describe('AwesomeDate', () => {
it('handles date boundaries', () => {
let date: AwesomeDate;
date = new AwesomeDate('1/1/2015');
assert.equal('1/31/2015', date.addDays(30));
date = new AwesomeDate('2/1/2016');
assert.equal('2/29/2016', date.addDays(28));
date = new AwesomeDate('2/1/2015');
assert.equal('3/1/2015', date.addDays(28));
});
});
Good:
import { assert } from 'chai';
describe('AwesomeDate', () => {
it('handles 30-day months', () => {
const date = new AwesomeDate('1/1/2015');
assert.equal('1/31/2015', date.addDays(30));
});
it('handles leap year', () => {
const date = new AwesomeDate('2/1/2016');
assert.equal('2/29/2016', date.addDays(28));
});
it('handles non-leap year', () => {
const date = new AwesomeDate('2/1/2015');
assert.equal('3/1/2015', date.addDays(28));
});
});
テストが失敗した場合に、何が間違ってたか気づけるような名前が重要です。
Bad:
describe('Calendar', () => {
it('2/29/2020', () => {
// ...
});
it('throws', () => {
// ...
});
});
Good:
describe('Calendar', () => {
it('should handle leap year', () => {
// ...
});
it('should throw when format is invalid', () => {
// ...
});
});
コールバックは綺麗では無く、過度のネスト(コールバック地獄)を引き起こします。
コールバックスタイルの既存関数をPromiseスタイルに変更するユーティリティがあります。
Node.js に関しては util.promisify
、一般的な目的には pify, es6-promisify
Bad:
import { get } from 'request';
import { writeFile } from 'fs';
function downloadPage(url: string, saveTo: string, callback: (error: Error, content?: string) => void) {
get(url, (error, response) => {
if (error) {
callback(error);
} else {
writeFile(saveTo, response.body, (error) => {
if (error) {
callback(error);
} else {
callback(null, response.body);
}
});
}
});
}
downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html', (error, content) => {
if (error) {
console.error(error);
} else {
console.log(content);
}
});
Good:
import { get } from 'request';
import { writeFile } from 'fs';
import { promisify } from 'util';
const write = promisify(writeFile);
function downloadPage(url: string, saveTo: string): Promise<string> {
return get(url)
.then(response => write(saveTo, response));
}
downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html')
.then(content => console.log(content))
.catch(error => console.error(error));
Promisesは、コードをより簡潔にするためのヘルパーメソッドをいくつかサポートします。
Pattern | Description |
---|---|
Promise.resolve(value) |
値をpromiseの resolve に変換する |
Promise.reject(error) |
エラーをpromiseの reject に変換する |
Promise.all(promises) |
渡されたプロミスの配列がすべて正常に完了するか、一部でrejectとなった場合 Promise を完了させます。 |
Promise.race(promises) |
渡されたプロミスの配列の最初に resolve/rejectされた結果で新しいプロミスを返します。 |
Promise.all
はタスクを並行して実行する必要がある時に特に便利です。
Promise.race
はPromiseにタイムアウトのようなものを実装する時に、それを簡単に実現できます。
async
/await
構文を使うと、promiseチェーンを作るよりはるかに綺麗で理解しやすいコードを書くことが出来ます。
async
キーワードを接頭辞とした関数内では (promiseが使用されている場合) await
キーワードでコードの実行を一時停止するようjavascriptランタイムに指示することが出来ます。
Bad:
import { get } from 'request';
import { writeFile } from 'fs';
import { promisify } from 'util';
const write = util.promisify(writeFile);
function downloadPage(url: string, saveTo: string): Promise<string> {
return get(url).then(response => write(saveTo, response));
}
downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html')
.then(content => console.log(content))
.catch(error => console.error(error));
Good:
import { get } from 'request';
import { writeFile } from 'fs';
import { promisify } from 'util';
const write = promisify(writeFile);
async function downloadPage(url: string, saveTo: string): Promise<string> {
const response = await get(url);
await write(saveTo, response);
return response;
}
// somewhere in an async function
try {
const content = await downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html');
console.log(content);
} catch (error) {
console.error(error);
}
例外が発生するのは良いことです! これはプログラムの何かが正常に動作しなかった事をランタイムが正常に判別出来たことを意味します。 そして現在のスタックで関数の実行を停止して(Node内の)プロセスを強制終了し、コンソールにスタックトレースを表示して通知します。
JavaScriptとTypeScriptは任意のオブジェクトを throw
できます。
Promiseの場合は reject
することができます。
Error
型と throw
構文を使うようにしましょう。
これは貴方のエラーが catch
構文を使ってより高いレベルのコードで補足されるかもしれないからです。
その時に文字列でメッセージを出すのはとても混乱を招き、
デバッグをより厄介にするでしょう。
同じ理由で Error
型の reject
を行うべきです。
Bad:
function calculateTotal(items: Item[]): number {
throw 'Not implemented.';
}
function get(): Promise<Item[]> {
return Promise.reject('Not implemented.');
}
Good:
function calculateTotal(items: Item[]): number {
throw new Error('Not implemented.');
}
function get(): Promise<Item[]> {
return Promise.reject(new Error('Not implemented.'));
}
// or equivalent to:
async function get(): Promise<Item[]> {
throw new Error('Not implemented.');
}
Error
型を使う利点は、それが try/catch/finally
構文によってサポートされており、暗黙的に stack
プロパティを持つためデバッグにおいて非常に強力だからです。
throw
構文を使わずに常にカスタムエラーオブジェクトを返すという方法もあります。
TypeScriptはそれを更に簡単にします。
次の例を参考にしてみてください:
type Result<R> = { isError: false, value: R };
type Failure<E> = { isError: true, error: E };
type Failable<R, E> = Result<R> | Failure<E>;
function calculateTotal(items: Item[]): Failable<number, 'empty'> {
if (items.length === 0) {
return { isError: true, error: 'empty' };
}
// ...
return { isError: false, value: 42 };
}
このアイディアの詳細については元の記事を参照してください。
捉えられた例外に対して何もしないというのは、例外が発生しても例外を発見したり、修正することができなくなります。
コンソール (console.log
)にエラーを表示するのは、頻繁にコンソール表示の海に溺れてしまうためあまり良いことではありません。
コードの一部でも try/catch
で囲むとそこでエラーが発生する可能性があり、発生したエラーに備えた計画を立て、例外が発生したときのコードパスを作る必要があります。
Bad:
try {
functionThatMightThrow();
} catch (error) {
console.log(error);
}
// or even worse
try {
functionThatMightThrow();
} catch (error) {
// ignore error
}
Good:
import { logger } from './logging'
try {
functionThatMightThrow();
} catch (error) {
logger.log(error);
}
同じ理由で、try/catch
された例外を無視してはいけません。
Bad:
getUser()
.then((user: User) => {
return sendEmail(user.email, 'Welcome!');
})
.catch((error) => {
console.log(error);
});
Good:
import { logger } from './logging'
getUser()
.then((user: User) => {
return sendEmail(user.email, 'Welcome!');
})
.catch((error) => {
logger.log(error);
});
// or using the async/await syntax:
// もしくは async/await 構文を使う
try {
const user = await getUser();
await sendEmail(user.email, 'Welcome!');
} catch (error) {
logger.log(error);
}
フォーマットは主観的です。 ここにある多くの規則のように、貴方が従わなければならない厳格なルールではありません。 重要な点は、フォーマットについて 論議しないこと です。 これを自動化するツールはたくさんあります。 それを使ってくさい。 エンジニアがフォーマットについて論議するのは時間とお金の無駄です。 従うべき一般的な規則は 一貫したフォーマットルールを守る ことです。
TypeScriptにはTSLintという強力なツールがあります。 コードの読みやすさと保守性を劇的に向上させるのに役立つ静的分析ツールです。 プロジェクトですぐに使えるTSLint構成です:
-
TSLint Config Standard - standard style rules
-
TSLint Config Airbnb - Airbnb style guide
-
TSLint Clean Code - TSLint rules inspired by the Clean Code: A Handbook of Agile Software Craftsmanship
-
TSLint react - lint rules related to React & JSX
-
TSLint + Prettier - lint rules for Prettier code formatter
-
ESLint rules for TSLint - ESLint rules for TypeScript
-
Immutable - rules to disable mutation in TypeScript
TypeScriptスタイルガイドとコーディング規約も参照してください。
大文字と小文字は変数や関数について多くのことを教えてくれます。 このルールは主観的なものなので、チームは必要に応じて好きなものを選ぶことができます。 重要なのは貴方が選んだどんなことであってもシンプルに 一貫性を持つ ことです。
Bad:
const DAYS_IN_WEEK = 7;
const daysInMonth = 30;
const songs = ['Back In Black', 'Stairway to Heaven', 'Hey Jude'];
const Artists = ['ACDC', 'Led Zeppelin', 'The Beatles'];
function eraseDatabase() {}
function restore_database() {}
type animal = { /* ... */ }
type Container = { /* ... */ }
Good:
const DAYS_IN_WEEK = 7;
const DAYS_IN_MONTH = 30;
const SONGS = ['Back In Black', 'Stairway to Heaven', 'Hey Jude'];
const ARTISTS = ['ACDC', 'Led Zeppelin', 'The Beatles'];
function eraseDatabase() {}
function restoreDatabase() {}
type Animal = { /* ... */ }
type Container = { /* ... */ }
クラス、インタフェース、タイプ、名前空間には PascalCase
の利用が好ましいでしょう。
変数、関数、クラスメンバーには camelCase
が好ましいでしょう。
関数が別の関数を呼び出す場合は、それらの関数をソースファイル内のすぐ近くに定義してください。 理想的には、呼び出し元を定義の真上に置いてください。 私たちは新聞のように上から下へとコードを読みます。 このため、あなたのコードもそのように読めるようにします。
Bad:
class PerformanceReview {
constructor(private readonly employee: Employee) {
}
private lookupPeers() {
return db.lookup(this.employee.id, 'peers');
}
private lookupManager() {
return db.lookup(this.employee, 'manager');
}
private getPeerReviews() {
const peers = this.lookupPeers();
// ...
}
review() {
this.getPeerReviews();
this.getManagerReview();
this.getSelfReview();
// ...
}
private getManagerReview() {
const manager = this.lookupManager();
}
private getSelfReview() {
// ...
}
}
const review = new PerformanceReview(employee);
review.review();
Good:
class PerformanceReview {
constructor(private readonly employee: Employee) {
}
review() {
this.getPeerReviews();
this.getManagerReview();
this.getSelfReview();
// ...
}
private getPeerReviews() {
const peers = this.lookupPeers();
// ...
}
private lookupPeers() {
return db.lookup(this.employee.id, 'peers');
}
private getManagerReview() {
const manager = this.lookupManager();
}
private lookupManager() {
return db.lookup(this.employee, 'manager');
}
private getSelfReview() {
// ...
}
}
const review = new PerformanceReview(employee);
review.review();
わかりやすく読みやすいimport文を使用すると、現在のコードの依存関係をすばやく確認できます。
import
構文には以下の良いプラクティスを必ず適用してください。
- インポート文はアルファベット順に並べ、グループ化する必要があります。
- 未使用のインポートは削除する必要があります。
- 名前付きインポートはアルファベット順にする必要があります(例:
import {A, B, C} from 'foo';
) - インポート元はグループ内でアルファベット順になっている必要があります。 例:
import * as foo from 'a'; import * as bar from 'b';
- importのグループは空行で区切られています。
- importのグループは以下の順になるようにする:
- ポリフィル(
import 'reflect-metadata';
) - NodeJSの組み込みモジュール(例:
import fs from 'fs';
) - 外部モジュール(例:
import { query } from 'itiriri';
) - 内部モジュール(例:
import { UserService } from 'src/services/userService';
) - 親ディレクトリからのモジュール(例:
import { UserService } from 'src/services/userService';
) - 同じディレクトリや兄弟ディレクトリからのモジュール(例:
import bar from './bar'; import baz from './bar/baz';
)
- ポリフィル(
Bad:
import { TypeDefinition } from '../types/typeDefinition';
import { AttributeTypes } from '../model/attribute';
import { ApiCredentials, Adapters } from './common/api/authorization';
import fs from 'fs';
import { ConfigPlugin } from './plugins/config/configPlugin';
import { BindingScopeEnum, Container } from 'inversify';
import 'reflect-metadata';
Good:
import 'reflect-metadata';
import fs from 'fs';
import { BindingScopeEnum, Container } from 'inversify';
import { AttributeTypes } from '../model/attribute';
import { TypeDefinition } from '../types/typeDefinition';
import { ApiCredentials, Adapters } from './common/api/authorization';
import { ConfigPlugin } from './plugins/config/configPlugin';
tsconfig.json
のcompilerOptions
セクションで、path
とbaseUrl
プロパティを定義するとより綺麗なimportを書くことができます。
これにより、import時に長い相対パスの使用を避けることができます。
Bad:
import { UserService } from '../../../services/UserService';
Good:
import { UserService } from '@services/UserService';
// tsconfig.json
...
"compilerOptions": {
...
"baseUrl": "src",
"paths": {
"@services": ["services/*"]
}
...
}
...
コメントを使用するのは、それなしでは表現できなかったことを表します。 コードが唯一の真実であるべきです。
悪いコードにコメントをしていないで書き換えてください。 — Brian W. Kernighan and P. J. Plaugher
コメントは弁明であり、必須ではありません。 良いコードはほとんどの場合文章のようになっています。
Bad:
// Check if subscription is active.
// subscriptionが有効かを確認
if (subscription.endDate > Date.now) { }
Good:
const isSubscriptionActive = subscription.endDate > Date.now;
if (isSubscriptionActive) { /* ... */ }
バージョン管理ツールが存在する理由です、古いコードは履歴に残しましょう。
Bad:
type User = {
name: string;
email: string;
// age: number;
// jobPosition: string;
}
Good:
type User = {
name: string;
email: string;
}
バージョン管理ツールを使うことを覚えましょう!
使ってないコード、コメントアウトされたコード、日記のようなコメントは不要です。
gitの履歴を取得するのに git log
を使ってください。
Bad:
/**
* 2016-12-20: Removed monads, didn't understand them (RM)
* 2016-10-01: Improved using special monads (JP)
* 2016-02-03: Added type-checking (LI)
* 2015-03-14: Implemented combine (JR)
*/
function combine(a: number, b: number): number {
return a + b;
}
Good:
function combine(a: number, b: number): number {
return a + b;
}
これは単純にノイズです。 関数と変数名を適切なインデント、フォーマットで使用して、コードの視覚的な構造を持ちましょう。 ほとんどのIDEはコードの折りたたみ機能をサポートしているので、コードブロックを折りたたむ/展開することができます。(Visual Studio Code folding regionsを参照).
Bad:
////////////////////////////////////////////////////////////////////////////////
// Client class
////////////////////////////////////////////////////////////////////////////////
class Client {
id: number;
name: string;
address: Address;
contact: Contact;
////////////////////////////////////////////////////////////////////////////////
// public methods
////////////////////////////////////////////////////////////////////////////////
public describe(): string {
// ...
}
////////////////////////////////////////////////////////////////////////////////
// private methods
////////////////////////////////////////////////////////////////////////////////
private describeAddress(): string {
// ...
}
private describeContact(): string {
// ...
}
};
Good:
class Client {
id: number;
name: string;
address: Address;
contact: Contact;
public describe(): string {
// ...
}
private describeAddress(): string {
// ...
}
private describeContact(): string {
// ...
}
};
後で改善が必要と思われるコードがあった場合 // TODO
コメントを使ってください。
ほとんどの IDEはこれらの種類のコメントを特別にサポートしているため、あなたはTODOリストをすばやく探すことが出来ます。
ただし TODO コメントを悪いコードの言い訳にしないでください。
Bad:
function getActiveSubscriptions(): Promise<Subscription[]> {
// ensure `dueDate` is indexed.
return db.subscriptions.find({ dueDate: { $lte: new Date() } });
}
Good:
function getActiveSubscriptions(): Promise<Subscription[]> {
// TODO: ensure `dueDate` is indexed.
return db.subscriptions.find({ dueDate: { $lte: new Date() } });
}
他の言語でも見ることができます:
Brazilian Portuguese: vitorfreitas/clean-code-typescript
Chinese:
Japanese: MSakamaki/clean-code-typescript
他の言語の翻訳作業も進行中です:
翻訳が完了すれば参考文献として追加されます。
詳細と進捗状況はこのissuesを見てください。 この資料をあなたの言語に翻訳をすることは、Clean Code コミュニティーにとって何ものにも代えがたい貢献となるでしょう。