Первая часть, судя по комментариям вызвала неоднозначное мнение, особенно что касалось части enum. Где-то я так же могу не соглашаться, как с автором оригинала, так и с некоторыми комментариями. Но как и указывалось в начальном описании к первой части, чистый код это не догма которую нужно соблюдать, это всего лишь рекомендации, соблюдение которых каждый сам выбирает под свои потребности и взгляды.

Объекты и структуры данных
Используйте иммутабельность
Система типов в TypeScript позволяет помечать отдельные свойства интерфейса/класса как readonly поля (только для чтения). Это позволяет вам работать функционально (неожиданная мутация это плохо). Для более сложных сценариев есть встроенный тип Readonly, который принимает тип T и помечает все его свойства только для чтения с использованием mapped types (смотрите mapped types).
Плохо:
interface Config { host: string; port: string; db: string; }
Хорошо:
interface Config { readonly host: string; readonly port: string; readonly db: string; }
В случае массива вы можете создать массив только для чтения, используя ReadonlyArray<T>. который не позволяет делать изменения с использованием push() и fill(), но можно использовать concat() и slice() они не меняют значения.
Плохо:
const array: number[] = [ 1, 3, 5 ]; array = []; // error array.push(100); // array will updated
Хорошо:
const array: ReadonlyArray<number> = [ 1, 3, 5 ]; array = []; // error array.push(100); // error
Объявление аргументов только для чтения TypeScript 3.4 is a bit easier.
function hoge(args: readonly string[]) { args.push(1); // error }
Предпочтение const assertions для литеральных значений.
Плохо:
const config = { hello: 'world' }; config.hello = 'world'; // значение изменено const array = [ 1, 3, 5 ]; array[0] = 10; // значение изменено // записываемые объекты возвращаются function readonlyData(value: number) { return { value }; } const result = readonlyData(100); result.value = 200; // значение изменено
Хорошо:
// объект только для чтения const config = { hello: 'world' } as const; config.hello = 'world'; // ошибка // массив только для чтения const array = [ 1, 3, 5 ] as const; array[0] = 10; // ошибка // Вы можете вернуть объект только для чтения function readonlyData(value: number) { return { value } as const; } const result = readonlyData(100); result.value = 200; // ошибка
Типы vs. интерфейсы
Используйте типы, когда вам может понадобиться объединение или пересечение. Используйте интерфейс, когда хотите использовать extends или implements. Однако строгого правила не существует, используйте то, что работает у вас. Для более подробного объяснения посмотрите это ответы о различиях между type and interface в TypeScript.
Плохо:
interface EmailConfig { // ... } interface DbConfig { // ... } interface Config { // ... } //... type Shape = { // ... }
Хорошо:
type EmailConfig = { // ... } type DbConfig = { // ... } type Config = EmailConfig | DbConfig; // ... interface Shape { // ... } class Circle implements Shape { // ... } class Square implements Shape { // ... }
Классы
Классы должны быть маленькими
Размер класса измеряется его ответственностью. Следуя Принципу единственной ответственности класс должен быть маленьким.
Плохо:
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 { /* ... */ } // ... }
Хорошо:
class Dashboard { disable(): void { /* ... */ } enable(): void { /* ... */ } getVersion(): string { /* ... */ } } // разделить обязанности, переместив оставшиеся методы в другие классы // ...
Высокая сплоченность низкая связь
Сплоченность определяет степень, в которой члены класса связаны друг с другом. В идеале все поля в классе должны использоваться каждым методом. Мы говорим, что класс максимально связный. На практике это, однако, не всегда возможно и даже нежелательно. Однако вы должны добиваться, того чтобы сплоченность была высокой.
Связанность относится и к тому, как связаны или зависимы два класса друг от друга. Классы считаются слабосвязанными если изменения в одном из них не влияют на другой.
Плохо:
class UserManager { // Плохо: каждая закрытая переменная используется той или иной группой методов. // Это ясно показывает, что класс несет больше, чем одну ответственность // Если мне нужно только создать сервис, чтобы получить транзакции для пользователя, // Я все еще вынужден передавать экземпляр `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> { // ... } }
Хорошо:
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> { // ... } }
Предпочитайте композицию наследованию
Как сказано в Design Patterns от банды черытех вы должны
Предпочитать композицию наследованию где можете. Есть много веских причин использовать наследование и много хороших причин использовать композицию. Суть этого принципа в том, что если ваш ум инстинктивно идет на наследование, попробуйте подумать, может ли композиция лучше смоделировать вашу проблему. В некоторых случаях может.
Тогда вы можете спросить: "Когда я должен использовать наследование?" Это зависит от вашей проблемы, но это достойный список, когда наследование имеет больше смысла, чем композиция:
- Ваше наследование представляет собой "is-a" отношения а не "has-a" отношения (Human->Animal vs. User->UserDetails).
- Вы можете повторно использовать код из базовых классов (Люди могут двигаться как все животные).
- Вы хотите внести глобальные изменения в производные классы, изменив базовый класс. (Изменение расхода калорий у всех животных при их перемещении).
Плохо:
class Employee { constructor( private readonly name: string, private readonly email: string) { } // ... } // Плохо, потому что Employees "имеют" налоговые данные. EmployeeTaxData не является типом Employee class EmployeeTaxData extends Employee { constructor( name: string, email: string, private readonly ssn: string, private readonly salary: number) { super(name, email); } // ... }
Хорошо:
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) { } // ... }
Используйте цепочки вызовов
Этот паттерн очень полезен и обычно используется во многих библиотеках. Это позволяет вашему коду быть выразительным и менее многословным. По этой причине используйте цепочку методов и посмотрите, насколько чистым будет ваш код.
Плохо:
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();
Хорошо:
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();
