# clean code
###### tags: `improve`
## Objects and Data Structures
### Use getters and setters
TypeScript支持getter/setter語法,使用getter和setter從封裝行為的對像中訪問數據可能比僅在對像上查找屬性更好。以下是一些原因:
```typescript
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;
```
示例使用了一個類型為BankAccount的對象,它包含了一個balance屬性和一些其他的屬性。在賦值之前,需要手動驗證值是否為負數,這樣做很麻煩,也容易出錯。
```typescript
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.
const account = new BankAccount();
account.balance = 100;
```
使用了一個BankAccount類,它具有一個私有的accountBalance屬性和一個公共的getter和setter來訪問balance屬性。 setter驗證要設置的值是否為負,並在值為負時拋出錯誤。這有助於確保balance屬性始終是有效的值。
使用getter和setter可以確保對象的內部表示被封裝,任何對對象屬性的更改都被驗證,以確保它們是有效的,從而預防錯誤並提高代碼的可維護性。
1. 當我們想要超出獲取對象屬性時,我們無需查找和更改代碼庫中的每個訪問器。
```typescript
class User {
private _name: string;
constructor(name: string) {
this._name = name;
}
get name(): string {
return this._name.toUpperCase();
}
}
const user = new User('John');
console.log(user.name); // output: "JOHN"
```
在上面的示例中,我們定義了一個User類,它具有一個私有的_name屬性和一個公共的getter來訪問該屬性。 getter將_name屬性的值轉換為大寫,並返回該值。這意味著我們可以使用user.name來獲取用戶的姓名,而無需查找和更改代碼庫中的每個訪問器。
2. 在進行設置時添加驗證非常簡單。
```typescript
class BankAccount {
private _balance: number = 0;
get balance(): number {
return this._balance;
}
set balance(value: number) {
if (value < 0) {
throw new Error('Cannot set negative balance.');
}
this._balance = value;
}
}
const account = new BankAccount();
account.balance = 100; // valid
account.balance = -50; // throws an error
```
在上面的示例中,我們定義了一個BankAccount類,它具有一個私有的_balance屬性和一個公共的getter和setter來訪問該屬性。 setter驗證要設置的值是否為負數,並在值為負數時拋出錯誤。這有助於確保_balance屬性始終是有效的值。
3. 封裝內部表示。
```typescript
class Person {
private _name: string;
private _age: number;
constructor(name: string, age: number) {
this._name = name;
this._age = age;
}
get name(): string {
return this._name;
}
set name(value: string) {
this._name = value.trim();
}
get age(): number {
return this._age;
}
set age(value: number) {
if (value < 0) {
throw new Error('Age cannot be negative.');
}
this._age = value;
}
}
const person = new Person('John', 30);
person.name = ' Jane ';
person.age = 25;
console.log(person.name); // output: "Jane"
console.log(person.age); // output: 25
```
在上面的示例中,我們定義了一個Person類,它具有一個私有的_name屬性和一個私有的_age屬性,以及公共的getter和setter來訪問這些屬性。 setter使用trim方法來刪除用戶名中的空格。這意味著我們可以使用person.name來獲取用戶的姓名,而無需關心姓名中是否包含空格。
4. 在獲取和設置時添加日誌記錄和錯誤處理非常容易。
```typescript
class Counter {
private _count: number = 0;
get count(): number {
console.log('Getting count...');
return this._count;
}
set count(value: number) {
if (value < 0) {
throw new Error('Count cannot be negative.');
}
console.log('Setting count...');
this._count = value;
}
}
const counter = new Counter();
counter.count = 10; // logs "Setting count..."
console.log(counter.count); // logs "Getting count..." and returns 10
```
在上面的示例中,我們定義了一個Counter類,它具有一個私有的_count屬性和一個公共的getter和setter來訪問該屬性。 getter和setter分別在獲取和設置屬性時記錄日誌。 setter還驗證要設置的值是否為負數,並在值為負數時拋出錯誤。這使得添加日誌記錄和錯誤處理變得非常容易。
5. 您可以惰性加載您對象的屬性,比如從服務器獲取它。
```typescript
class User {
private _name: string | null = null;
get name(): string {
if (this._name === null) {
// fetch name from server
this._name = 'John';
}
return this._name;
}
}
const user = new User();
console.log(user.name); // logs "John", fetches name from server first time
console.log(user.name); // logs "John", returns cached name second time
```
我們可以惰性加載用戶的屬性,例如從服務器獲取它,而不必在對象創建時立即獲取該屬性的值。這可以提高性能,並減少網絡請求。 提高 cache hit 減少 cache miss
```typescript
type Member = {
id: string,
name: string,
}
type Team = {
name: string,
members: Member[],
currentMember: Member
}
function mustFind<T>(arr: T[], predicate: (t: T) => boolean) {
const item = arr.find(predicate)
if (item) return item
throw new Error('item not find')
}
function problemGetMemberById(memberId: string): Team {
const Members: Member[] = [
{ id: '1', name: 'Danny' },
{ id: '2', name: 'Alex' },
{ id: '3', name: 'Blob' },
]
return {
name: 'My_team',
members: Members,
currentMember: Members.find(item => item.id === memberId)
}
}
function getMemberById(memberId: string): Team {
const Members: Member[] = [
{ id: '1', name: 'Danny' },
{ id: '2', name: 'Alex' },
{ id: '3', name: 'Blob' },
]
const currentMember = mustFind(Members, (item) => item.id === memberId)
return {
name: 'My_team',
members: Members,
currentMember
}
}
function getMemberById2(memberId: string): Team {
const Members: Member[] = [
{ id: '1', name: 'Danny' },
{ id: '2', name: 'Alex' },
{ id: '3', name: 'Blob' },
]
return {
name: 'My_team',
members: Members,
get currentMember() {
const member = Members.find(i => i.id === memberId)
if (member) return member
throw new Error('Member not find')
}
}
}
```
### Make objects have private/protected members
```typescript!
//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中,支持使用public(默認)、protected和private訪問器來訪問類成員。
上面的示例中,我們定義了一個Circle類,用於計算圓的周長和麵積。在“Bad”示例中,我們將radius屬性設置為公共屬性,這意味著它可以被外部代碼直接訪問和更改。這違反了封裝的原則,因為我們希望將radius屬性的訪問限制在類內部。
在“Good”示例中,我們將radius屬性設置為只讀的私有屬性。這意味著我們可以在類內部訪問和更改它,但是外部代碼不能訪問或更改它。這符合封裝的原則,因為我們可以控制radius屬性的訪問權限,並防止外部代碼對它進行非法操作。
### Prefer immutability
```typescript
//bad
interface Config {
host: string;
port: string;
db: string;
}
//good
interface Config {
readonly host: string;
readonly port: string;
readonly db: string;
}
```
```typescript
//bad
const array: number[] = [ 1, 3, 5 ];
array = []; // error
array.push(100); // array will be updated
//good
const array: ReadonlyArray<number> = [ 1, 3, 5 ];
array = []; // error
array.push(100); // error
```
```typescript!
//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
```
以下是 TypeScript 中的不可變性的 5 個重點:
1. 使用 `readonly` 關鍵字來標記只讀屬性
2. 使用 `ReadonlyArray<T>` 來創建只讀數組
3. 在函數中使用 `readonly` 關鍵字來標記只讀參數
4. 使用 `const` 斷言將對象標記為只讀
5. 使用 `as const` 將數組和對象字面量標記為只讀
下面給出一個示例代碼,演示瞭如何使用上述 5 個技術來實現不可變性:
```typescript
interface User {
readonly id: number;
readonly name: string;
readonly age: number;
}
function createUser(id: number, name: string, age: number): User {
return {
id,
name,
age,
} as const;
}
const users: ReadonlyArray<User> = [
createUser(1, 'Alice', 25),
createUser(2, 'Bob', 30),
createUser(3, 'Charlie', 35),
];
function printUsers(users: ReadonlyArray<User>) {
users.forEach((user) => {
console.log(`ID: ${user.id}, Name: ${user.name}, Age: ${user.age}`);
});
}
printUsers(users);
// Attempt to modify a property of a User object
users[0].name = 'Eve'; // Error: Cannot assign to 'name' because it is a read-only property.
```
在上面的代碼中,我們定義了一個只讀的 `User` 接口,它包含了 `id`、`name` 和 `age` 三個只讀屬性。我們還定義了一個 `createUser` 函數,它使用 `readonly` 關鍵字來標記返回的對象為只讀。我們使用 `as const` 將對象字面量強制轉換為只讀對象。
我們還定義了一個只讀的 `users` 數組,它包含了三個只讀的 `User` 對象。我們使用 `ReadonlyArray<User>` 來標記這個數組為只讀數組。
最後,我們定義了一個 `printUsers` 函數,它接受一個只讀的 `users` 數組作為參數,並輸出每個 `User` 對象的 `id`、`name` 和 `age` 屬性。我們嘗試修改 `users` 數組中第一個 `User` 對象的 `name` 屬性,但是 TypeScript 報告了一個錯誤,因為這個屬性是只讀的。
### type vs. interface
```typescript
//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 {
// ...
}
```
| Aspect | Type | Interface |
| --- | --- | --- |
| Can describe functions | ✅ | ✅ |
| Can describe constructors | ✅ | ✅ |
| Can describe tuples | ✅ | ✅ |
| Interfaces can extend it | ⚠️ | ✅ |
| Classes can extend it | 🚫 | ✅ |
| Classes can implement it (implements) | ⚠️ | ✅ |
| Can intersect another one of its kind | ✅ | ⚠️ |
| Can create a union with another one of its kind | ✅ | 🚫 |
| Can be used to create mapped types | ✅ | 🚫 |
| Can be mapped over with mapped types | ✅ | ✅ |
| Expands in error messages and logs | ✅ | 🚫 |
| Can be augmented | 🚫 | ✅ |
| Can be recursive | ⚠️ | ✅ |
### Can intersect another one of its kind:
```typescript
type A = {
propA: string;
};
type B = {
propB: number;
};
type C = A & B;
const c: C = {
propA: 'hello',
propB: 123,
};
```
### Can create a union with another one of its kind:
```typescript
type ResultType = number | string;
function processResult(result: ResultType) {
if (typeof result === 'number') {
console.log(`The result is a number: ${result}`);
} else {
console.log(`The result is a string: ${result}`);
}
}
processResult(123);
processResult('hello');
```
### Can be used to create mapped types:
```typescript
type Person = {
name: string;
age: number;
};
type PersonRecord = {
[K in keyof Person]: string;
}
const person: Person = { name: 'Alice', age: 30 };
const personRecord: PersonRecord = { name: 'Alice', age: '30' };
```
### Can be mapped over with mapped types:
```typescript
type Person = {
name: string;
age: number;
};
type PersonPartial = {
[K in keyof Person]?: Person[K];
}
const person: Person = { name: 'Alice', age: 30 };
const personPartial: PersonPartial = { name: 'Alice' };
type PersonPartials<T> = {
[k in keyof T]?:T[k]
}
```
### Expands in error messages and logs:
```typescript
type Person = {
name: string;
age: number;
};
function greet(person: Person) {
console.log(`Hello, ${person.name}! You are ${person.age} years old.`);
}
const person = { name: 'Alice', age: '30' };
greet(person);
```
`person.age` 的类型错误。
### Can be augmented:
```typescript
interface Person {
name: string;
age: number;
}
interface Person {
gender: string;
}
const person: Person = { name: 'Alice', age: 30, gender: 'female' };
```
### Can be recursive:
```typescript
type TreeNode<T> = {
value: T;
left?: TreeNode<T>;
right?: TreeNode<T>;
};
const root: TreeNode<number> = {
value: 1,
left: {
value: 2,
left: { value: 4 },
right: { value: 5 },
},
right: {
value: 3,
left: { value: 6 },
right: { value: 7 },
},
};
```