{"id":13490711,"url":"https://github.com/pipiliang/clean-code-typescript","last_synced_at":"2025-03-28T06:31:51.550Z","repository":{"id":50442847,"uuid":"165261621","full_name":"pipiliang/clean-code-typescript","owner":"pipiliang","description":"TypeScript 代码整洁之道","archived":false,"fork":false,"pushed_at":"2024-04-11T07:51:32.000Z","size":114,"stargazers_count":230,"open_issues_count":1,"forks_count":33,"subscribers_count":4,"default_branch":"master","last_synced_at":"2024-10-31T04:34:53.440Z","etag":null,"topics":["typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pipiliang.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-01-11T15:01:33.000Z","updated_at":"2024-09-26T17:31:16.000Z","dependencies_parsed_at":"2024-01-16T09:26:13.843Z","dependency_job_id":"ab0b8242-1cc3-4079-8501-c8622f5d4f98","html_url":"https://github.com/pipiliang/clean-code-typescript","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pipiliang%2Fclean-code-typescript","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pipiliang%2Fclean-code-typescript/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pipiliang%2Fclean-code-typescript/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pipiliang%2Fclean-code-typescript/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pipiliang","download_url":"https://codeload.github.com/pipiliang/clean-code-typescript/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245984582,"owners_count":20704794,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["typescript"],"created_at":"2024-07-31T19:00:50.308Z","updated_at":"2025-03-28T06:31:51.540Z","avatar_url":"https://github.com/pipiliang.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"# TypeScript 代码整洁之道\n[中文](https://github.com/pipiliang/clean-code-typescript) | [English](https://github.com/labs42io/clean-code-typescript)\n\n\u003e将 Clean Code 的概念适用到 TypeScript，灵感来自 [clean-code-javascript](https://github.com/ryanmcdermott/clean-code-javascript)。\n\n## 目录\n  1. [简介](#简介)\n  2. [变量](#变量)\n  3. [函数](#函数)\n  4. [对象与数据结构](#对象和数据结构)\n  5. [类](#类)\n  6. [SOLID原则](#SOLID原则)\n  7. [测试](#测试)\n  8. [并发](#并发)\n  9. [错误处理](#错误处理)\n  10. [格式化](#格式化)\n  11. [注释](#注释)\n\n## 简介\n\n![Humorous image of software quality estimation as a count of how many expletives you shout when reading code](https://www.osnews.com/images/comics/wtfm.jpg)\n\n这不是一份 TypeScript 编码风格规范，而是将 Robert C. Martin 的软件工程著作 [《Clean Code》](https://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882) 适用到 TypeScript，引导读者使用 TypeScript 编写[易读、复用和可扩展](https://github.com/ryanmcdermott/3rs-of-software-architecture)的软件。\n\n实际上，并不是每一个原则都要严格遵守，能被广泛认同的原则就更少了。这看起来虽然只是一份指导原则，但却是 *Clean Code* 作者对多年编程经验的凝练。\n\n软件工程技术已有50多年的历史了，我们仍然要学习很多的东西。当软件架构和架构本身一样古老的时候，也许我们需要遵守更严格的规则。但是现在，让这些指导原则作为评估您和您的团队代码质量的试金石。\n\n另外，理解这些原则不会立即让您变的优秀，也不意味着不会犯错。每一段代码都是从不完美开始的，通过反复走查不断趋于完美，就像黏土制作成陶艺一样，享受这个过程吧!\n\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n## 变量\n\n\u003e计算机科学只存在两个难题：缓存失效和命名。—— Phil KarIton\n\n### 使用有意义的变量名\n\n做有意义的区分，让读者更容易理解变量的含义。\n\n**:-1: 反例:**\n\n```ts\n\nfunction between\u003cT\u003e(a1: T, a2: T, a3: T) {\n\n  return a2 \u003c= a1 \u0026\u0026 a1 \u003c= a3;\n\n}\n\n```\n\n**:+1: 正例:**\n\n```ts\n\nfunction between\u003cT\u003e(value: T, left: T, right: T) {\n\n  return left \u003c= value \u0026\u0026 value \u003c= right;\n\n}\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 可读的变量名\n\n如果你不能正确读出它，那么你在讨论它时听起来就会像个白痴。\n\n**:-1: 反例:**\n\n```ts\n\nclass DtaRcrd102 {\n\n  private genymdhms: Date; #  // 你能读出这个变量名么？ \n\n  private modymdhms: Date;\n\n  private pszqint = '102';\n\n}\n\n```\n\n**:+1: 正例:**\n\n```ts\n\nclass Customer {\n\n  private generationTimestamp: Date;\n\n  private modificationTimestamp: Date;\n\n  private recordId = '102';\n\n}\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 合并功能一致的变量\n\n**:-1: 反例:**\n\n```ts\n\nfunction getUserInfo(): User;\n\nfunction getUserDetails(): User;\n\nfunction getUserData(): User;\n\n```\n\n**:+1: 正例:**\n\n```ts\n\nfunction getUser(): User;\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 便于搜索的名字\n\n往往我们读代码要比写的多，所以易读性和可搜索非常重要。如果不抽取并命名有意义的变量名，那就坑了读代码的人。代码一定要便于搜索，[TSLint](https://palantir.github.io/tslint/rules/no-magic-numbers/) 就可以帮助识别未命名的常量。\n\n**:-1: 反例:**\n\n```ts\n\n//86400000 代表什么？\n\nsetTimeout(restart, 86400000);\n\n```\n\n**:+1: 正例:**\n\n```ts\n\n// 声明为常量，要大写且有明确含义。\n\nconst MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000;\n\nsetTimeout(restart, MILLISECONDS_IN_A_DAY);\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 使用自解释的变量名\n\n**:-1: 反例:**\n\n```ts\n\ndeclare const users:Map\u003cstring, User\u003e;\n\nfor (const keyValue of users) {\n  // ...\n}\n\n```\n\n**:+1: 正例:**\n\n```ts\n\ndeclare const users:Map\u003cstring, User\u003e;\n\nfor (const [id, user] of users) {\n  // ...\n}\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 避免思维映射\n\n不要让人去猜测或想象变量的含义，*明确是王道。*\n\n**:-1: 反例:**\n\n```ts\n\nconst u = getUser();\n\nconst s = getSubscription();\n\nconst t = charge(u, s);\n\n```\n\n**:+1: 正例:**\n\n```ts\n\nconst user = getUser();\n\nconst subscription = getSubscription();\n\nconst transaction = charge(user, subscription);\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 不添加无用的上下文\n\n如果类名或对象名已经表达了某些信息，在内部变量名中不要再重复表达。\n\n**:-1: 反例:**\n\n```ts\n\ntype Car = {\n\n  carMake: string;\n\n  carModel: string;\n\n  carColor: string;\n\n}\n\nfunction print(car: Car): void {\n\n  console.log(`${this.carMake} ${this.carModel} (${this.carColor})`);\n\n}\n\n```\n\n**:+1: 正例:**\n\n```ts\n\ntype Car = {\n\n  make: string;\n\n  model: string;\n\n  color: string;\n\n}\n\nfunction print(car: Car): void {\n\n  console.log(`${this.make} ${this.model} (${this.color})`);\n\n}\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 使用默认参数，而非短路或条件判断\n\n通常，默认参数比短路更整洁。\n\n**:-1: 反例:**\n\n```ts\n\nfunction loadPages(count: number) {\n\n  const loadCount = count !== undefined ? count : 10;\n\n  // ...\n\n}\n\n```\n\n**:+1: 正例:**\n\n```ts\n\nfunction loadPages(count: number = 10) {\n\n  // ...\n\n}\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n## 函数\n\n### 参数越少越好 (理想情况不超过2个)\n\n限制参数个数，这样函数测试会更容易。超过三个参数会导致测试复杂度激增，需要测试众多不同参数的组合场景。\n理想情况，只有一两个参数。如果有两个以上的参数，那么您的函数可能就太过复杂了。\n\n如果需要很多参数，请您考虑使用对象。为了使函数的属性更清晰，可以使用[解构](https://basarat.gitbooks.io/typescript/docs/destructuring.html)，它有以下优点：\n\n1. 当有人查看函数签名时，会立即清楚使用了哪些属性。\n2. 解构对传递给函数的参数对象做深拷贝，这可预防副作用。(注意：**不会克隆**从参数对象中解构的对象和数组)\n3. TypeScript 会对未使用的属性显示警告。\n\n**:-1: 反例:**\n\n```ts\n\nfunction createMenu(title: string, body: string, buttonText: string, cancellable: boolean) {\n\n  // ...\n\n}\n\ncreateMenu('Foo', 'Bar', 'Baz', true);\n\n```\n\n**:+1: 正例:**\n\n```ts\n\nfunction createMenu(options: {title: string, body: string, buttonText: string, cancellable: boolean}) {\n\n  // ...\n\n}\n\ncreateMenu(\n  {\n    title: 'Foo',\n    body: 'Bar',\n    buttonText: 'Baz',\n    cancellable: true\n  }\n);\n\n```\n通过 TypeScript 的[类型别名](https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-aliases)，可以进一步提高可读性。\n\n```ts\n\ntype MenuOptions = {title: string, body: string, buttonText: string, cancellable: boolean};\n\nfunction createMenu(options: MenuOptions) {\n\n  // ...\n\n}\n\ncreateMenu(\n  {\n    title: 'Foo',\n    body: 'Bar',\n    buttonText: 'Baz',\n    cancellable: true\n  }\n);\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 只做一件事\n\n这是目前软件工程中最重要的规则。如果函数做不止一件事，它就更难组合、测试以及理解。反之，函数只有一个行为，它就更易于重构、代码就更清晰。如果能做好这一点，你一定很优秀！\n\n**:-1: 反例:**\n\n```ts\n\nfunction emailClients(clients: Client[]) {\n\n  clients.forEach((client) =\u003e {\n\n    const clientRecord = database.lookup(client);\n\n    if (clientRecord.isActive()) {\n\n      email(client);\n\n    }\n\n  });\n\n}\n\n```\n\n**:+1: 正例:**\n\n```ts\n\nfunction emailClients(clients: Client[]) {\n\n  clients.filter(isActiveClient).forEach(email);\n\n}\n\nfunction isActiveClient(client: Client) {\n\n  const clientRecord = database.lookup(client);\n\n  return clientRecord.isActive();\n\n}\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 名副其实\n\n通过函数名就可以看得出函数实现的功能。\n\n**:-1: 反例:**\n\n```ts\n\nfunction addToDate(date: Date, month: number): Date {\n  // ...\n}\n\nconst date = new Date();\n\n// 从函数名很难看的出需要加什么？\naddToDate(date, 1);\n\n```\n\n**:+1: 正例:**\n\n```ts\n\nfunction addMonthToDate(date: Date, month: number): Date {\n  // ...\n}\n\nconst date = new Date();\n\naddMonthToDate(date, 1);\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 每个函数只包含同一个层级的抽象\n\n当有多个抽象级别时，函数应该是做太多事了。拆分函数以便可复用，也让测试更容易。\n\n**:-1: 反例:**\n\n```ts\n\nfunction parseCode(code:string) {\n\n  const REGEXES = [ /* ... */ ];\n  const statements = code.split(' ');\n  const tokens = [];\n\n  REGEXES.forEach((regex) =\u003e {\n\n    statements.forEach((statement) =\u003e {\n      // ...\n    });\n\n  });\n\n  const ast = [];\n\n  tokens.forEach((token) =\u003e {\n    // lex...\n  });\n\n  ast.forEach((node) =\u003e {\n    // 解析 ...\n  });\n\n}\n\n```\n\n**:+1: 正例:**\n\n```ts\n\nconst REGEXES = [ /* ... */ ];\n\nfunction parseCode(code:string) {\n\n  const tokens = tokenize(code);\n\n  const syntaxTree = parse(tokens);\n\n  syntaxTree.forEach((node) =\u003e {\n\n    // parse...\n\n  });\n\n}\n\nfunction tokenize(code: string):Token[] {\n\n  const statements = code.split(' ');\n\n  const tokens:Token[] = [];\n\n  REGEXES.forEach((regex) =\u003e {\n\n    statements.forEach((statement) =\u003e {\n\n      tokens.push( /* ... */ );\n\n    });\n\n  });\n\n  return tokens;\n\n}\n\nfunction parse(tokens: Token[]): SyntaxTree {\n\n  const syntaxTree:SyntaxTree[] = [];\n\n  tokens.forEach((token) =\u003e {\n\n    syntaxTree.push( /* ... */ );\n\n  });\n\n  return syntaxTree;\n\n}\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 删除重复代码\n\n重复乃万恶之源！重复意味着如果要修改某个逻辑，需要修改多处代码:cry:。\n想象一下，如果你经营一家餐厅，要记录你的库存:所有的西红柿、洋葱、大蒜、香料等等。如果要维护多个库存列表，那是多么痛苦的事!\n\n存在重复代码，是因为有两个或两个以上很近似的功能，只有一点不同，但是这点不同迫使你用多个独立的函数来做很多几乎相同的事情。删除重复代码，则意味着创建一个抽象，该抽象仅用一个函数/模块/类就可以处理这组不同的东西。\n\n合理的抽象至关重要，这就是为什么您应该遵循[SOLID原则](#SOLID原则)。糟糕的抽象可能还不如重复代码，所以要小心！话虽如此，还是要做好抽象！尽量不要重复。\n\n**:-1: 反例:**\n\n```ts\n\nfunction showDeveloperList(developers: Developer[]) {\n\n  developers.forEach((developer) =\u003e {\n\n    const expectedSalary = developer.calculateExpectedSalary();\n\n    const experience = developer.getExperience();\n\n    const githubLink = developer.getGithubLink();\n\n    const data = {\n\n      expectedSalary,\n\n      experience,\n\n      githubLink\n\n    };\n\n    render(data);\n\n  });\n\n}\n\nfunction showManagerList(managers: Manager[]) {\n\n  managers.forEach((manager) =\u003e {\n\n    const expectedSalary = manager.calculateExpectedSalary();\n\n    const experience = manager.getExperience();\n\n    const portfolio = manager.getMBAProjects();\n\n    const data = {\n\n      expectedSalary,\n\n      experience,\n\n      portfolio\n\n    };\n\n    render(data);\n\n  });\n\n}\n\n```\n\n**:+1: 正例:**\n\n```ts\n\nclass Developer {\n\n  // ...\n\n  getExtraDetails() {\n\n    return {\n\n      githubLink: this.githubLink,\n\n    }\n\n  }\n\n}\n\nclass Manager {\n\n  // ...\n\n  getExtraDetails() {\n\n    return {\n\n      portfolio: this.portfolio,\n\n    }\n\n  }\n\n}\n\nfunction showEmployeeList(employee: Developer | Manager) {\n\n  employee.forEach((employee) =\u003e {\n\n    const expectedSalary = developer.calculateExpectedSalary();\n\n    const experience = developer.getExperience();\n\n    const extra = employee.getExtraDetails();\n\n    const data = {\n\n      expectedSalary,\n\n      experience,\n\n      extra,\n\n    };\n\n    render(data);\n\n  });\n\n}\n\n```\n\n有时，在重复代码和引入不必要的抽象而增加的复杂性之间，需要做权衡。当来自不同领域的两个不同模块，它们的实现看起来相似，复制也是可以接受的，并且比抽取公共代码要好一点。因为抽取公共代码会导致两个模块产生间接的依赖关系。\n\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 使用`Object.assign`或`解构`来设置默认对象\n\n**:-1: 反例:**\n\n```ts\n\ntype MenuConfig = {title?: string, body?: string, buttonText?: string, cancellable?: boolean};\n\nfunction createMenu(config: MenuConfig) {\n\n  config.title = config.title || 'Foo';\n\n  config.body = config.body || 'Bar';\n\n  config.buttonText = config.buttonText || 'Baz';\n\n  config.cancellable = config.cancellable !== undefined ? config.cancellable : true;\n\n}\n\nconst menuConfig = {\n\n  title: null,\n\n  body: 'Bar',\n\n  buttonText: null,\n\n  cancellable: true\n\n};\n\ncreateMenu(menuConfig);\n\n```\n\n**:+1: 正例:**\n\n```ts\n\ntype MenuConfig = {title?: string, body?: string, buttonText?: string, cancellable?: boolean};\n\nfunction createMenu(config: MenuConfig) {\n\n  const menuConfig = Object.assign({\n\n    title: 'Foo',\n\n    body: 'Bar',\n\n    buttonText: 'Baz',\n\n    cancellable: true\n\n  }, config);\n\n}\n\ncreateMenu({ body: 'Bar' });\n\n```\n\n或者，您可以使用默认值的解构:\n\n```ts\n\ntype MenuConfig = {title?: string, body?: string, buttonText?: string, cancellable?: boolean};\n\nfunction createMenu({title = 'Foo', body = 'Bar', buttonText = 'Baz', cancellable = true}: MenuConfig) {\n\n  // ...\n\n}\n\ncreateMenu({ body: 'Bar' });\n\n```\n\n为了避免副作用，不允许显式传递`undefined`或`null`值。参见 TypeScript 编译器的`--strictnullcheck`选项。\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 不要使用Flag参数\n\nFlag参数告诉用户这个函数做了不止一件事。如果函数使用布尔值实现不同的代码逻辑路径，则考虑将其拆分。\n\n**:-1: 反例:**\n\n```ts\n\nfunction createFile(name:string, temp:boolean) {\n\n  if (temp) {\n\n    fs.create(`./temp/${name}`);\n\n  } else {\n\n    fs.create(name);\n\n  }\n\n}\n\n```\n\n**:+1: 正例:**\n\n```ts\n\nfunction createFile(name:string) {\n\n  fs.create(name);\n\n}\n\nfunction createTempFile(name:string) {\n\n  fs.create(`./temp/${name}`);\n\n}\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 避免副作用 (part1)\n\n当函数产生除了“一个输入一个输出”之外的行为时，称该函数产生了副作用。比如写文件、修改全局变量或将你的钱全转给了一个陌生人等。\n\n在某些情况下，程序需要一些副作用。如先前例子中的写文件，这时应该将这些功能集中在一起，不要用多个函数/类修改某个文件。用且只用一个 service 完成这一需求。\n\n重点是要规避常见陷阱，比如，在无结构对象之间共享状态、使用可变数据类型，以及不确定副作用发生的位置。如果你能做到这点，你才可能笑到最后！\n\n**:-1: 反例:**\n\n```ts\n\n// Global variable referenced by following function.\n\n// If we had another function that used this name, now it'd be an array and it could break it.\n\nlet name = 'Robert C. Martin';\n\nfunction toBase64() {\n\n  name = btoa(name);\n\n}\n\ntoBase64(); // produces side effects to `name` variable\n\nconsole.log(name); // expected to print 'Robert C. Martin' but instead 'Um9iZXJ0IEMuIE1hcnRpbg=='\n\n```\n\n**:+1: 正例:**\n\n```ts\n\n// Global variable referenced by following function.\n\n// If we had another function that used this name, now it'd be an array and it could break it.\n\nconst name = 'Robert C. Martin';\n\nfunction toBase64(text:string):string {\n\n  return btoa(text);\n\n}\n\nconst encodedName = toBase64(name);\n\nconsole.log(name);\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 避免副作用 (part2)\n\n在 JavaScript 中，原类型是值传递，对象、数组是引用传递。\n\n有这样一种情况，如果您的函数修改了购物车数组，用来添加购买的商品，那么其他使用该`cart`数组的函数都将受此添加操作的影响。想象一个糟糕的情况:\n\n用户点击“购买”按钮，该按钮调用`purchase`函数，函数请求网络并将`cart`数组发送到服务器。由于网络连接不好，购买功能必须不断重试请求。恰巧在网络请求开始前，用户不小心点击了某个不想要的项目上的“Add to Cart”按钮，该怎么办？而此时网络请求开始，那么`purchase`函数将发送意外添加的项，因为它引用了一个购物车数组，`addItemToCart`函数修改了该数组，添加了不需要的项。\n\n一个很好的解决方案是`addItemToCart`总是克隆`cart`，编辑它，并返回克隆。这确保引用购物车的其他函数不会受到任何更改的影响。\n\n注意两点:\n\n1. 在某些情况下，可能确实想要修改输入对象，这种情况非常少见。且大多数可以重构，确保没副作用！(见[纯函数](https://en.wikipedia.org/wiki/Pure_function))\n\n2. 性能方面，克隆大对象代价确实比较大。还好有一些很好的库，它提供了一些高效快速的方法，且不像手动克隆对象和数组那样占用大量内存。\n\n\n**:-1: 反例:**\n\n```ts\n\nfunction addItemToCart(cart: CartItem[], item:Item):void {\n\n  cart.push({ item, date: Date.now() });\n\n};\n\n```\n\n**:+1: 正例:**\n\n```ts\n\nfunction addItemToCart(cart: CartItem[], item:Item):CartItem[] {\n\n  return [...cart, { item, date: Date.now() }];\n\n};\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 不要写全局函数\n\n在 JavaScript 中污染全局的做法非常糟糕，这可能导致和其他库冲突，而调用你的 API 的用户在实际环境中得到一个 exception 前对这一情况是一无所知的。\n\n考虑这样一个例子：如果想要扩展 JavaScript 的 `Array`，使其拥有一个可以显示两个数组之间差异的 `diff`方法，该怎么做呢？可以将新函数写入`Array.prototype` ，但它可能与另一个尝试做同样事情的库冲突。如果另一个库只是使用`diff`来查找数组的第一个元素和最后一个元素之间的区别呢？\n\n更好的做法是扩展`Array`，实现对应的函数功能。\n\n**:-1: 反例:**\n\n```ts\n\ndeclare global {\n\n  interface Array\u003cT\u003e {\n\n    diff(other: T[]): Array\u003cT\u003e;\n\n  }\n\n}\n\nif (!Array.prototype.diff){\n\n  Array.prototype.diff = function \u003cT\u003e(other: T[]): T[] {\n\n    const hash = new Set(other);\n\n    return this.filter(elem =\u003e !hash.has(elem));\n\n  };\n\n}\n\n```\n\n**:+1: 正例:**\n\n```ts\n\nclass MyArray\u003cT\u003e extends Array\u003cT\u003e {\n\n  diff(other: T[]): T[] {\n\n    const hash = new Set(other);\n\n    return this.filter(elem =\u003e !hash.has(elem));\n\n  };\n\n}\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 函数式编程优于命令式编程\n\n尽量使用函数式编程！\n\n**:-1: 反例:**\n\n```ts\n\nconst contributions = [\n\n  {\n\n    name: 'Uncle Bobby',\n\n    linesOfCode: 500\n\n  }, {\n\n    name: 'Suzie Q',\n\n    linesOfCode: 1500\n\n  }, {\n\n    name: 'Jimmy Gosling',\n\n    linesOfCode: 150\n\n  }, {\n\n    name: 'Gracie Hopper',\n\n    linesOfCode: 1000\n\n  }\n\n];\n\nlet totalOutput = 0;\n\nfor (let i = 0; i \u003c contributions.length; i++) {\n\n  totalOutput += contributions[i].linesOfCode;\n\n}\n\n```\n\n**:+1: 正例:**\n\n```ts\n\nconst contributions = [\n\n  {\n\n    name: 'Uncle Bobby',\n\n    linesOfCode: 500\n\n  }, {\n\n    name: 'Suzie Q',\n\n    linesOfCode: 1500\n\n  }, {\n\n    name: 'Jimmy Gosling',\n\n    linesOfCode: 150\n\n  }, {\n\n    name: 'Gracie Hopper',\n\n    linesOfCode: 1000\n\n  }\n\n];\n\nconst totalOutput = contributions\n\n  .reduce((totalLines, output) =\u003e totalLines + output.linesOfCode, 0)\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 封装判断条件\n\n**:-1: 反例:**\n\n```ts\n\nif (subscription.isTrial || account.balance \u003e 0) {\n\n  // ...\n\n}\n\n```\n\n**:+1: 正例:**\n\n```ts\n\nfunction canActivateService(subscription: Subscription, account: Account) {\n\n  return subscription.isTrial || account.balance \u003e 0\n\n}\n\nif (canActivateService(subscription, account)) {\n\n  // ...\n\n}\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 避免“否定”的判断\n\n**:-1: 反例:**\n\n```ts\n\nfunction isEmailNotUsed(email: string) {\n\n  // ...\n\n}\n\nif (isEmailNotUsed(email)) {\n\n  // ...\n\n}\n\n```\n\n**:+1: 正例:**\n\n```ts\n\nfunction isEmailUsed(email) {\n\n  // ...\n\n}\n\nif (!isEmailUsed(node)) {\n\n  // ...\n\n}\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 避免判断条件\n\n这看起来似乎不太可能完成啊。大多数人听到后第一反应是，“没有if语句怎么实现功能呢？” 在多数情况下，可以使用多态性来实现相同的功能。接下来的问题是 “为什么要这么做？” 原因就是之前提到的：函数只做一件事。\n\n**:-1: 反例:**\n\n```ts\n\nclass Airplane {\n\n  private type: string;\n\n  // ...\n\n  getCruisingAltitude() {\n\n    switch (this.type) {\n\n      case '777':\n\n        return this.getMaxAltitude() - this.getPassengerCount();\n\n      case 'Air Force One':\n\n        return this.getMaxAltitude();\n\n      case 'Cessna':\n\n        return this.getMaxAltitude() - this.getFuelExpenditure();\n\n      default:\n\n        throw new Error('Unknown airplane type.');\n\n    }\n\n  }\n\n}\n\n```\n\n**:+1: 正例:**\n\n```ts\n\nclass Airplane {\n\n  // ...\n\n}\n\nclass Boeing777 extends Airplane {\n\n  // ...\n\n  getCruisingAltitude() {\n\n    return this.getMaxAltitude() - this.getPassengerCount();\n\n  }\n\n}\n\nclass AirForceOne extends Airplane {\n\n  // ...\n\n  getCruisingAltitude() {\n\n    return this.getMaxAltitude();\n\n  }\n\n}\n\nclass Cessna extends Airplane {\n\n  // ...\n\n  getCruisingAltitude() {\n\n    return this.getMaxAltitude() - this.getFuelExpenditure();\n\n  }\n\n}\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 避免类型检查\n\nTypeScript 是 JavaScript 的一个严格的语法超集，具有静态类型检查的特性。所以指定变量、参数和返回值的类型，以充分利用此特性，能让重构更容易。\n\n**:-1: 反例:**\n\n```ts\n\nfunction travelToTexas(vehicle: Bicycle | Car) {\n\n  if (vehicle instanceof Bicycle) {\n\n    vehicle.pedal(this.currentLocation, new Location('texas'));\n\n  } else if (vehicle instanceof Car) {\n\n    vehicle.drive(this.currentLocation, new Location('texas'));\n\n  }\n\n}\n\n```\n\n**:+1: 正例:**\n\n```ts\n\ntype Vehicle = Bicycle | Car;\n\nfunction travelToTexas(vehicle: Vehicle) {\n\n  vehicle.move(this.currentLocation, new Location('texas'));\n\n}\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 不要过度优化\n\n现代浏览器在运行时进行大量的底层优化。很多时候，你做优化只是在浪费时间。有些优秀[资源](https://github.com/petkaantonov/bluebird/wiki/Optimization-killers)可以帮助定位哪里需要优化，找到并修复它。\n\n**:-1: 反例:**\n\n```ts\n\n// On old browsers, each iteration with uncached `list.length` would be costly\n\n// because of `list.length` recomputation. In modern browsers, this is optimized.\n\nfor (let i = 0, len = list.length; i \u003c len; i++) {\n\n  // ...\n\n}\n\n```\n\n**:+1: 正例:**\n\n```ts\n\nfor (let i = 0; i \u003c list.length; i++) {\n\n  // ...\n\n}\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 删除无用代码\n\n无用代码和重复代码一样无需保留。如果没有地方调用它，请删除！如果仍然需要它，可以查看版本历史。\n\n**:-1: 反例:**\n\n```ts\n\nfunction oldRequestModule(url: string) {\n\n  // ...\n\n}\n\nfunction requestModule(url: string) {\n\n  // ...\n\n}\n\nconst req = requestModule;\n\ninventoryTracker('apples', req, 'www.inventory-awesome.io');\n\n```\n\n**:+1: 正例:**\n\n```ts\n\nfunction requestModule(url: string) {\n\n  // ...\n\n}\n\nconst req = requestModule;\n\ninventoryTracker('apples', req, 'www.inventory-awesome.io');\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 使用迭代器和生成器\n\n像使用流一样处理数据集合时，请使用生成器和迭代器。\n\n理由如下:\n- 将调用者与生成器实现解耦，在某种意义上，调用者决定要访问多少项。\n- 延迟执行，按需使用。\n- 内置支持使用`for-of`语法进行迭代\n- 允许实现优化的迭代器模式\n\n**:-1: 反例:**\n\n```ts\nfunction fibonacci(n: number): number[] {\n  if (n === 1) return [0];\n  if (n === 2) return [0, 1];\n\n  const items: number[] = [0, 1];\n  while (items.length \u003c n) {\n    items.push(items[items.length - 2] + items[items.length - 1]);\n  }\n\n  return items;\n}\n\nfunction print(n: number) {\n  fibonacci(n).forEach(fib =\u003e console.log(fib));\n}\n\n// Print first 10 Fibonacci numbers.\nprint(10);\n```\n\n**:+1: 正例:**\n\n```ts\n// Generates an infinite stream of Fibonacci numbers.\n// The generator doesn't keep the array of all numbers.\nfunction* fibonacci(): IterableIterator\u003cnumber\u003e {\n  let [a, b] = [0, 1];\n\n  while (true) {\n    yield a;\n    [a, b] = [b, a + b];\n  }\n}\n\nfunction print(n: number) {\n  let i = 0;\n  for (const fib of fibonacci()) {\n    if (i++ === n) break;  \n    console.log(fib);\n  }  \n}\n\n// Print first 10 Fibonacci numbers.\nprint(10);\n```\n\n有些库通过链接“map”、“slice”、“forEach”等方法，达到与原生数组类似的方式处理迭代。参见 [itiriri](https://www.npmjs.com/package/itiriri) 里面有一些使用迭代器的高级操作示例（或异步迭代的操作 [itiriri-async](https://www.npmjs.com/package/itiriri-async)）。\n\n```ts\nimport itiriri from 'itiriri';\n\nfunction* fibonacci(): IterableIterator\u003cnumber\u003e {\n  let [a, b] = [0, 1];\n \n  while (true) {\n    yield a;\n    [a, b] = [b, a + b];\n  }\n}\n\nitiriri(fibonacci())\n  .take(10)\n  .forEach(fib =\u003e console.log(fib));\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n## 对象和数据结构\n\n### 使用`getters`和`setters`\n\nTypeScript 支持 getter/setter 语法。使用 getter 和 setter 从对象中访问数据比简单地在对象上查找属性要好。原因如下:\n\n* 当需要在获取对象属性之前做一些事情时，不必在代码中查找并修改每个访问器。\n* 执行`set`时添加验证更简单。\n* 封装内部表示。\n* 更容易添加日志和错误处理。\n* 可以延迟加载对象的属性，比如从服务器获取它。\n\n**:-1: 反例:**\n\n```ts\n\nclass BankAccount {\n\n  balance: number = 0;\n\n  // ...\n\n}\n\nconst value = 100;\n\nconst account = new BankAccount();\n\nif (value \u003c 0) {\n\n  throw new Error('Cannot set negative balance.');\n\n}\n\naccount.balance = value;\n\n```\n\n**:+1: 正例:**\n\n```ts\n\nclass BankAccount {\n\n  private accountBalance: number = 0;\n\n  get balance(): number {\n\n    return this.accountBalance;\n\n  }\n\n  set balance(value: number) {\n\n    if (value \u003c 0) {\n\n      throw new Error('Cannot set negative balance.');\n\n    }\n\n    this.accountBalance = value;\n\n  }\n\n  // ...\n\n}\n\nconst account = new BankAccount();\n\naccount.balance = 100;\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 让对象拥有 private/protected 成员\n\nTypeScript 类成员支持 `public`*(默认)*、`protected` 以及 `private`的访问限制。\n\n**:-1: 反例:**\n\n```ts\n\nclass Circle {\n\n  radius: number;\n\n  \n\n  constructor(radius: number) {\n\n    this.radius = radius;\n\n  }\n\n  perimeter(){\n\n    return 2 * Math.PI * this.radius;\n\n  }\n\n  surface(){\n\n    return Math.PI * this.radius * this.radius;\n\n  }\n\n}\n\n```\n\n**:+1: 正例:**\n\n```ts\n\nclass Circle {\n\n  constructor(private readonly radius: number) {\n\n  }\n\n  perimeter(){\n\n    return 2 * Math.PI * this.radius;\n\n  }\n\n  surface(){\n\n    return Math.PI * this.radius * this.radius;\n\n  }\n\n}\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 不变性\n\nTypeScript 类型系统允许将接口、类上的单个属性设置为只读，能以函数的方式运行。\n\n还有个高级场景，可以使用内置类型`Readonly`，它接受类型 T 并使用[映射类型](https://www.typescriptlang.org/docs/handbook/advanced-types.html#mapped-types)将其所有属性标记为只读。\n\n**:-1: 反例:**\n\n```ts\n\ninterface Config {\n\n  host: string;\n\n  port: string;\n\n  db: string;\n\n}\n\n```\n\n**:+1: 正例:**\n\n```ts\n\ninterface Config {\n\n  readonly host: string;\n\n  readonly port: string;\n\n  readonly db: string;\n\n}\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 类型 vs 接口\n\n当可能需要联合或交集时，请使用类型。如果需要`扩展`或`实现`，请使用接口。然而，没有严格的规则，只有适合的规则。\n\n详细解释参考关于 Typescript 中`type`和`interface`区别的[解答](https://stackoverflow.com/questions/37233735/typescript-interfaces-vs-types/54101543#54101543) 。\n\n**:-1: 反例:**\n\n```ts\n\ninterface EmailConfig {\n\n  // ...\n\n}\n\ninterface DbConfig {\n\n  // ...\n\n}\n\ninterface Config {\n\n  // ...\n\n}\n\n//...\n\ntype Shape {\n\n  // ...\n\n}\n\n```\n\n**:+1: 正例:**\n\n```ts\n\ntype EmailConfig {\n\n  // ...\n\n}\n\ntype DbConfig {\n\n  // ...\n\n}\n\ntype Config  = EmailConfig | DbConfig;\n\n// ...\n\ninterface Shape {\n\n}\n\nclass Circle implements Shape {\n\n  // ...\n\n}\n\nclass Square implements Shape {\n\n  // ...\n\n}\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n\n## 类\n\n### 小、小、小！要事情说三遍\n\n类的大小是由它的职责来度量的。按照*单一职责原则*，类要小。\n\n**:-1: 反例:**\n\n```ts\n\nclass Dashboard {\n\n  getLanguage(): string { /* ... */ }\n\n  setLanguage(language: string): void { /* ... */ }\n\n  showProgress(): void { /* ... */ }\n\n  hideProgress(): void { /* ... */ }\n\n  isDirty(): boolean { /* ... */ }\n\n  disable(): void { /* ... */ }\n\n  enable(): void { /* ... */ }\n\n  addSubscription(subscription: Subscription): void { /* ... */ }\n\n  removeSubscription(subscription: Subscription): void { /* ... */ }\n\n  addUser(user: User): void { /* ... */ }\n\n  removeUser(user: User): void { /* ... */ }\n\n  goToHomePage(): void { /* ... */ }\n\n  updateProfile(details: UserDetails): void { /* ... */ }\n\n  getVersion(): string { /* ... */ }\n\n  // ...\n\n}\n\n```\n\n**:+1: 正例:**\n\n```ts\n\nclass Dashboard {\n\n  disable(): void { /* ... */ }\n\n  enable(): void { /* ... */ }\n\n  getVersion(): string { /* ... */ }\n\n}\n\n// split the responsibilities by moving the remaining methods to other classes\n\n// ...\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 高内聚低耦合\n\n内聚：定义了类成员之间相互关联的程度。理想情况下，高内聚类的每个方法都应该使用类中的所有字段，实际上这不可能也不可取。但我们依然提倡高内聚。\n\n耦合：指的是两个类之间的关联程度。如果其中一个类的更改不影响另一个类，则称为低耦合类。\n\n好的软件设计具有**高内聚性**和**低耦合性**。\n\n**:-1: 反例:**\n\n```ts\n\nclass UserManager {\n\n  // Bad: each private variable is used by one or another group of methods.\n\n  // It makes clear evidence that the class is holding more than a single responsibility.\n\n  // If I need only to create the service to get the transactions for a user,\n\n  // I'm still forced to pass and instance of emailSender.\n\n  constructor(\n\n    private readonly db: Database,\n\n    private readonly emailSender: EmailSender) {\n\n  }\n\n  async getUser(id: number): Promise\u003cUser\u003e {\n\n    return await db.users.findOne({ id })\n\n  }\n\n  async getTransactions(userId: number): Promise\u003cTransaction[]\u003e {\n\n    return await db.transactions.find({ userId })\n\n  }\n\n  async sendGreeting(): Promise\u003cvoid\u003e {\n\n    await emailSender.send('Welcome!');\n\n  }\n\n  async sendNotification(text: string): Promise\u003cvoid\u003e {\n\n    await emailSender.send(text);\n\n  }\n\n  async sendNewsletter(): Promise\u003cvoid\u003e {\n\n    // ...\n\n  }\n\n}\n\n```\n\n**:+1: 正例:**\n\n```ts\n\nclass UserService {\n\n  constructor(private readonly db: Database) {\n\n  }\n\n  async getUser(id: number): Promise\u003cUser\u003e {\n\n    return await db.users.findOne({ id })\n\n  }\n\n  async getTransactions(userId: number): Promise\u003cTransaction[]\u003e {\n\n    return await db.transactions.find({ userId })\n\n  }\n\n}\n\nclass UserNotifier {\n\n  constructor(private readonly emailSender: EmailSender) {\n\n  }\n\n  async sendGreeting(): Promise\u003cvoid\u003e {\n\n    await emailSender.send('Welcome!');\n\n  }\n\n  async sendNotification(text: string): Promise\u003cvoid\u003e {\n\n    await emailSender.send(text);\n\n  }\n\n  async sendNewsletter(): Promise\u003cvoid\u003e {\n\n    // ...\n\n  }\n\n}\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 组合大于继承\n\n正如“四人帮”在[设计模式](https://en.wikipedia.org/wiki/Design_Patterns)中所指出的那样，您尽可能使用组合而不是继承。组合和继承各有优劣。这个准则的主要观点是，如果你潜意识地倾向于继承，试着想想组合是否能更好地给你的问题建模，在某些情况下可以。  \n\n什么时候应该使用继承？这取决于你面临的问题。以下场景使用继承更好:\n\n1. 继承代表的是“is-a”关系，而不是“has-a”关系 (人 -\u003e 动物 vs. 用户 -\u003e 用户详情)。\n2. 可复用基类的代码 (人类可以像所有动物一样移动)。\n3. 希望通过更改基类对派生类进行全局更改(改变所有动物在运动时的热量消耗)。\n\n**:-1: 反例:**\n\n```ts\n\nclass Employee {\n\n  constructor(\n\n    private readonly name: string, \n\n    private readonly email:string) {\n\n  }\n\n  // ...\n\n}\n\n// Bad because Employees \"have\" tax data. EmployeeTaxData is not a type of Employee\n\nclass EmployeeTaxData extends Employee {\n\n  constructor(\n\n    name: string, \n\n    email:string,\n\n    private readonly ssn: string, \n\n    private readonly salary: number) {\n\n    super(name, email);\n\n  }\n\n  // ...\n\n}\n\n```\n\n**:+1: 正例:**\n\n```ts\n\nclass Employee {\n\n  private taxData: EmployeeTaxData;\n\n  constructor(\n\n    private readonly name: string, \n\n    private readonly email:string) {\n\n  }\n\n  setTaxData(ssn: string, salary: number): Employee {\n\n    this.taxData = new EmployeeTaxData(ssn, salary);\n\n    return this;\n\n  }\n\n  // ...\n\n}\n\nclass EmployeeTaxData {\n\n  constructor(\n\n    public readonly ssn: string, \n\n    public readonly salary: number) {\n\n  }\n\n  // ...\n\n}\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 使用方法链\n\n非常有用的模式，在许多库中都可以看到。它让代码表达力更好，也更简洁。\n\n**:-1: 反例:**\n\n```ts\n\nclass QueryBuilder {\n\n  private collection: string;\n\n  private pageNumber: number = 1;\n\n  private itemsPerPage: number = 100;\n\n  private orderByFields: string[] = [];\n\n  from(collection: string): void {\n\n    this.collection = collection;\n\n  }\n\n  page(number: number, itemsPerPage: number = 100): void {\n\n    this.pageNumber = number;\n\n    this.itemsPerPage = itemsPerPage;\n\n  }\n\n  orderBy(...fields: string[]): void {\n\n    this.orderByFields = fields;\n\n  }\n\n  build(): Query {\n\n    // ...\n\n  }\n\n}\n\n// ...\n\nconst query = new QueryBuilder();\n\nquery.from('users');\n\nquery.page(1, 100);\n\nquery.orderBy('firstName', 'lastName');\n\nconst query = queryBuilder.build();\n\n```\n\n**:+1: 正例:**\n\n```ts\n\nclass QueryBuilder {\n\n  private collection: string;\n\n  private pageNumber: number = 1;\n\n  private itemsPerPage: number = 100;\n\n  private orderByFields: string[] = [];\n\n  from(collection: string): this {\n\n    this.collection = collection;\n\n    return this;\n\n  }\n\n  page(number: number, itemsPerPage: number = 100): this {\n\n    this.pageNumber = number;\n\n    this.itemsPerPage = itemsPerPage;\n\n    return this;\n\n  }\n\n  orderBy(...fields: string[]): this {\n\n    this.orderByFields = fields;\n\n    return this;\n\n  }\n\n  build(): Query {\n\n    // ...\n\n  }\n\n}\n\n// ...\n\nconst query = new QueryBuilder()\n\n  .from('users')\n\n  .page(1, 100)\n\n  .orderBy('firstName', 'lastName')\n\n  .build();\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n## SOLID原则\n\n### 单一职责原则 (SRP)\n\n正如 Clean Code 中所述，“类更改的原因不应该超过一个”。将很多功能打包在一个类看起来很诱人，就像在航班上您只能带一个手提箱。这样带来的问题是，在概念上类不具有内聚性，且有很多原因去修改类。而我们应该尽量减少修改类的次数。如果一个类功能太多，修改了其中一处很难确定对代码库中其他依赖模块的影响。\n\n**:-1: 反例:**\n\n```ts\n\nclass UserSettings {\n\n  constructor(private readonly user: User) {\n\n  }\n\n  changeSettings(settings: UserSettings) {\n\n    if (this.verifyCredentials()) {\n\n      // ...\n\n    }\n\n  }\n\n  verifyCredentials() {\n\n    // ...\n\n  }\n\n}\n\n```\n\n**:+1: 正例:**\n\n```ts\n\nclass UserAuth {\n\n  constructor(private readonly user: User) {\n\n  }\n\n  verifyCredentials() {\n\n    // ...\n\n  }\n\n}\n\nclass UserSettings {\n\n  private readonly auth: UserAuth;\n\n  constructor(private readonly user: User) {\n\n    this.auth = new UserAuth(user);\n\n  }\n\n  changeSettings(settings: UserSettings) {\n\n    if (this.auth.verifyCredentials()) {\n\n      // ...\n\n    }\n\n  }\n\n}\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 开闭原则 (OCP)\n\n正如 Bertrand Meyer 所说，“软件实体(类、模块、函数等)应该对扩展开放，对修改封闭。” 换句话说，就是允许在不更改现有代码的情况下添加新功能。\n\n**:-1: 反例:**\n\n```ts\n\nclass AjaxAdapter extends Adapter {\n\n  constructor() {\n\n    super();\n\n  }\n\n  // ...\n\n}\n\nclass NodeAdapter extends Adapter {\n\n  constructor() {\n\n    super();\n\n  }\n\n  // ...\n\n}\n\nclass HttpRequester {\n\n  constructor(private readonly adapter: Adapter) {\n\n  }\n\n  async fetch\u003cT\u003e(url: string): Promise\u003cT\u003e {\n\n    if (this.adapter instanceof AjaxAdapter) {\n\n      const response = await makeAjaxCall\u003cT\u003e(url);\n\n      // transform response and return\n\n    } else if (this.adapter instanceof NodeAdapter) {\n\n      const response = await makeHttpCall\u003cT\u003e(url);\n\n      // transform response and return\n\n    }\n\n  }\n\n}\n\nfunction makeAjaxCall\u003cT\u003e(url: string): Promise\u003cT\u003e {\n\n  // request and return promise\n\n}\n\nfunction makeHttpCall\u003cT\u003e(url: string): Promise\u003cT\u003e {\n\n  // request and return promise\n\n}\n\n```\n\n**:+1: 正例:**\n\n```ts\n\nabstract class Adapter {\n\n  abstract async request\u003cT\u003e(url: string): Promise\u003cT\u003e;\n\n}\n\nclass AjaxAdapter extends Adapter {\n\n  constructor() {\n\n    super();\n\n  }\n\n  async request\u003cT\u003e(url: string): Promise\u003cT\u003e{\n\n    // request and return promise\n\n  }\n\n  // ...\n\n}\n\nclass NodeAdapter extends Adapter {\n\n  constructor() {\n\n    super();\n\n  }\n\n  async request\u003cT\u003e(url: string): Promise\u003cT\u003e{\n\n    // request and return promise\n\n  }\n\n  // ...\n\n}\n\nclass HttpRequester {\n\n  constructor(private readonly adapter: Adapter) {\n\n  }\n\n  async fetch\u003cT\u003e(url: string): Promise\u003cT\u003e {\n\n    const response = await this.adapter.request\u003cT\u003e(url);\n\n    // transform response and return\n\n  }\n\n}\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 里氏替换原则 (LSP)\n\n对一个非常简单的概念来说，这是个可怕的术语。\n\n它的正式定义是：“如果 S 是 T 的一个子类型，那么类型 T 的对象可以被替换为类型 S 的对象，而不会改变程序任何期望的属性(正确性、执行的任务等)“。这是一个更可怕的定义。  \n\n更好的解释是，如果您有一个父类和一个子类，那么父类和子类可以互换使用，而不会出现问题。这可能仍然令人困惑，所以让我们看一看经典的正方形矩形的例子。从数学上讲，正方形是矩形，但是如果您通过继承使用 “is-a” 关系对其建模，您很快就会遇到麻烦。\n\n**:-1: 反例:**\n\n```ts\n\nclass Rectangle {\n\n  constructor(\n\n    protected width: number = 0, \n\n    protected height: number = 0) {\n\n  }\n\n  setColor(color: string) {\n\n    // ...\n\n  }\n\n  render(area: number) {\n\n    // ...\n\n  }\n\n  setWidth(width: number) {\n\n    this.width = width;\n\n  }\n\n  setHeight(height: number) {\n\n    this.height = height;\n\n  }\n\n  getArea(): number {\n\n    return this.width * this.height;\n\n  }\n\n}\n\nclass Square extends Rectangle {\n\n  setWidth(width: number) {\n\n    this.width = width;\n\n    this.height = width;\n\n  }\n\n  setHeight(height: number) {\n\n    this.width = height;\n\n    this.height = height;\n\n  }\n\n}\n\nfunction renderLargeRectangles(rectangles: Rectangle[]) {\n\n  rectangles.forEach((rectangle) =\u003e {\n\n    rectangle.setWidth(4);\n\n    rectangle.setHeight(5);\n\n    const area = rectangle.getArea(); // BAD: Returns 25 for Square. Should be 20.\n\n    rectangle.render(area);\n\n  });\n\n}\n\nconst rectangles = [new Rectangle(), new Rectangle(), new Square()];\n\nrenderLargeRectangles(rectangles);\n\n```\n\n**:+1: 正例:**\n\n```ts\n\nabstract class Shape {\n\n  setColor(color: string) {\n\n    // ...\n\n  }\n\n  render(area: number) {\n\n    // ...\n\n  }\n\n  abstract getArea(): number;\n\n}\n\nclass Rectangle extends Shape {\n\n  constructor(\n\n    private readonly width = 0, \n\n    private readonly height = 0) {\n\n    super();\n\n  }\n\n  getArea(): number {\n\n    return this.width * this.height;\n\n  }\n\n}\n\nclass Square extends Shape {\n\n  constructor(private readonly length: number) {\n\n    super();\n\n  }\n\n  getArea(): number {\n\n    return this.length * this.length;\n\n  }\n\n}\n\nfunction renderLargeShapes(shapes: Shape[]) {\n\n  shapes.forEach((shape) =\u003e {\n\n    const area = shape.getArea();\n\n    shape.render(area);\n\n  });\n\n}\n\nconst shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)];\n\nrenderLargeShapes(shapes);\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 接口隔离原则 (ISP)\n\n“客户不应该被迫依赖于他们不使用的接口。” 这一原则与单一责任原则密切相关。这意味着不应该设计一个大而全的抽象，否则会增加客户的负担，因为他们需要实现一些不需要的方法。\n\n**:-1: 反例:**\n\n```ts\n\ninterface ISmartPrinter {\n\n  print();\n\n  fax();\n\n  scan();\n\n}\n\nclass AllInOnePrinter implements ISmartPrinter {\n\n  print() {\n\n    // ...\n\n  }  \n\n  \n\n  fax() {\n\n    // ...\n\n  }\n\n  scan() {\n\n    // ...\n\n  }\n\n}\n\nclass EconomicPrinter implements ISmartPrinter {\n\n  print() {\n\n    // ...\n\n  }  \n\n  \n\n  fax() {\n\n    throw new Error('Fax not supported.');\n\n  }\n\n  scan() {\n\n    throw new Error('Scan not supported.');\n\n  }\n\n}\n\n```\n\n**:+1: 正例:**\n\n```ts\n\ninterface IPrinter {\n\n  print();\n\n}\n\ninterface IFax {\n\n  fax();\n\n}\n\ninterface IScanner {\n\n  scan();\n\n}\n\nclass AllInOnePrinter implements IPrinter, IFax, IScanner {\n\n  print() {\n\n    // ...\n\n  }  \n\n  \n\n  fax() {\n\n    // ...\n\n  }\n\n  scan() {\n\n    // ...\n\n  }\n\n}\n\nclass EconomicPrinter implements IPrinter {\n\n  print() {\n\n    // ...\n\n  }\n\n}\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 依赖反转原则(Dependency Inversion Principle)\n\n这个原则有两个要点:\n1. 高层模块不应该依赖于低层模块，两者都应该依赖于抽象。\n2. 抽象不依赖实现，实现应依赖抽象。\n\n一开始这难以理解，但是如果你使用过 Angular，你就会看到以依赖注入(DI)的方式实现了这一原则。虽然概念不同，但是 DIP 阻止高级模块了解其低级模块的细节并进行设置。它可以通过 DI 实现这一点。这样做的一个巨大好处是减少了模块之间的耦合。耦合非常糟糕，它让代码难以重构。\n\nDIP 通常是通过使用控制反转(IoC)容器来实现的。比如：TypeScript 的 IoC 容器 [InversifyJs](https://www.npmjs.com/package/inversify)\n\n**:-1: 反例:**\n\n```ts\n\nimport { readFile as readFileCb } from 'fs';\n\nimport { promisify } from 'util';\n\nconst readFile = promisify(readFileCb);\n\ntype ReportData = {\n\n  // ..\n\n}\n\nclass XmlFormatter {\n\n  parse\u003cT\u003e(content: string): T {\n\n    // Converts an XML string to an object T\n\n  }\n\n}\n\nclass ReportReader {\n\n  // BAD: We have created a dependency on a specific request implementation.\n\n  // We should just have ReportReader depend on a parse method: `parse`\n\n  private readonly formatter = new XmlFormatter();\n\n  async read(path: string): Promise\u003cReportData\u003e {\n\n    const text = await readFile(path, 'UTF8');\n\n    return this.formatter.parse\u003cReportData\u003e(text);\n\n  }\n\n}\n\n// ...\n\nconst reader = new ReportReader();\n\nawait report = await reader.read('report.xml');\n\n```\n\n**:+1: 正例:**\n\n```ts\n\nimport { readFile as readFileCb } from 'fs';\n\nimport { promisify } from 'util';\n\nconst readFile = promisify(readFileCb);\n\ntype ReportData = {\n\n  // ..\n\n}\n\ninterface Formatter {\n\n  parse\u003cT\u003e(content: string): T;\n\n}\n\nclass XmlFormatter implements Formatter {\n\n  parse\u003cT\u003e(content: string): T {\n\n    // Converts an XML string to an object T\n\n  }\n\n}\n\nclass JsonFormatter implements Formatter {\n\n  parse\u003cT\u003e(content: string): T {\n\n    // Converts a JSON string to an object T\n\n  }\n\n}\n\nclass ReportReader {\n\n  constructor(private readonly formatter: Formatter){\n\n  }\n\n  async read(path: string): Promise\u003cReportData\u003e {\n\n    const text = await readFile(path, 'UTF8');\n\n    return this.formatter.parse\u003cReportData\u003e(text);\n\n  }\n\n}\n\n// ...\n\nconst reader = new ReportReader(new XmlFormatter());\n\nawait report = await reader.read('report.xml');\n\n// or if we had to read a json report:\n\nconst reader = new ReportReader(new JsonFormatter());\n\nawait report = await reader.read('report.json');\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n## 测试\n\n测试比发布更重要。如果没有测试或测试不充分，那么每次发布代码时都无法确保不引入问题。怎样才算是足够的测试？这取决于团队，但是拥有100%的覆盖率(所有语句和分支)会让团队更有信心。这一切都要基于好的测试框架以及[覆盖率工具](https://github.com/gotwarlost/istanbul)。\n\n没有任何理由不编写测试。有[很多优秀的 JS 测试框架](http://jstherightway.org/#testing-tools)都支持 TypeScript，找个团队喜欢的。然后为每个新特性/模块编写测试。如果您喜欢测试驱动开发(TDD)，那就太好了，重点是确保在开发任何特性或重构现有特性之前，代码覆盖率已经达到要求。 \n\n### TDD（测试驱动开发）三定律\n\n1. 在编写不能通过的单元测试前，不可编写生产代码。\n2. 只可编写刚好无法通过的单元测试，不能编译也算不过。\n3. 只可编写刚好足以通过当前失败测试的生产代码。\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### F.I.R.S.T.准则\n整洁的测试应遵循以下准则:\n- **快速**（Fast），测试应该快（及时反馈出业务代码的问题）。\n- **独立**（Independent），每个测试流程应该独立。\n- **可重复**（Repeatable），测试应该在任何环境上都能重复通过。\n- **自我验证**（Self-Validating），测试结果应该明确*通过*或者*失败*。\n- **及时**（Timely），测试代码应该在产品代码之前编写。\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 单一的测试每个概念\n\n测试也应该遵循*单一职责原则*，每个单元测试只做一个断言。\n\n**:-1: 反例:**\n\n```ts\n\nimport { assert } from 'chai';\n\ndescribe('AwesomeDate', () =\u003e {\n\n  it('handles date boundaries', () =\u003e {\n\n    let date: AwesomeDate;\n\n    date = new AwesomeDate('1/1/2015');\n\n    date.addDays(30);\n\n    assert.equal('1/31/2015', date);\n\n    date = new AwesomeDate('2/1/2016');\n\n    date.addDays(28);\n\n    assert.equal('02/29/2016', date);\n\n    date = new AwesomeDate('2/1/2015');\n\n    date.addDays(28);\n\n    assert.equal('03/01/2015', date);\n\n  });\n\n});\n\n```\n\n**:+1: 正例:**\n\n```ts\n\nimport { assert } from 'chai';\n\ndescribe('AwesomeDate', () =\u003e {\n\n  it('handles 30-day months', () =\u003e {\n\n    const date = new AwesomeDate('1/1/2015');\n\n    date.addDays(30);\n\n    assert.equal('1/31/2015', date);\n\n  });\n\n  it('handles leap year', () =\u003e {\n\n    const date = new AwesomeDate('2/1/2016');\n\n    date.addDays(28);\n\n    assert.equal('02/29/2016', date);\n\n  });\n\n  it('handles non-leap year', () =\u003e {\n\n    const date = new AwesomeDate('2/1/2015');\n\n    date.addDays(28);\n\n    assert.equal('03/01/2015', date);\n\n  });\n\n});\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 测试用例名称应该显示它的意图\n\n当测试失败时，出错的第一个迹象可能就是它的名字。\n\n**:-1: 反例:**\n\n```ts\n\ndescribe('Calendar', () =\u003e {\n\n  it('2/29/2020', () =\u003e {\n\n    // ...\n\n  });\n\n  it('throws', () =\u003e {\n\n    // ...\n\n  });\n\n});\n\n```\n\n**:+1: 正例:**\n\n```ts\n\ndescribe('Calendar', () =\u003e {\n\n  it('should handle leap year', () =\u003e {\n\n    // ...\n\n  });\n\n  it('should throw when format is invalid', () =\u003e {\n\n    // ...\n\n  });\n\n});\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n## 并发\n\n### 用 Promises 替代回调\n\n回调不够整洁而且会导致过多的嵌套*(回调地狱)*。\n\n有些工具使用回调的方式将现有函数转换为 promise 对象：\n- Node.js 参见[`util.promisify`](https://nodejs.org/dist/latest-v8.x/docs/api/util.html#util_util_promisify_original)\n- 通用参见 [pify](https://www.npmjs.com/package/pify), [es6-promisify](https://www.npmjs.com/package/es6-promisify)\n\n**:-1: 反例:**\n\n```ts\n\nimport { get } from 'request';\n\nimport { writeFile } from 'fs';\n\nfunction downloadPage(url: string, saveTo: string, callback: (error: Error, content?: string) =\u003e void){\n\n  get(url, (error, response) =\u003e {\n\n    if (error) {\n\n      callback(error);\n\n    } else {\n\n      writeFile(saveTo, response.body, (error) =\u003e {\n\n        if (error) {\n\n          callback(error);\n\n        } else {\n\n          callback(null, response.body);\n\n        }\n\n      });\n\n    }\n\n  })\n\n}\n\ndownloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html', (error, content) =\u003e {\n\n  if (error) {\n\n    console.error(error);\n\n  } else {\n\n    console.log(content);\n\n  }\n\n});\n\n```\n\n**:+1: 正例:**\n\n```ts\n\nimport { get } from 'request';\n\nimport { writeFile } from 'fs';\n\nimport { promisify } from 'util';\n\nconst write = promisify(writeFile);\n\nfunction downloadPage(url: string, saveTo: string): Promise\u003cstring\u003e {\n\n  return get(url)\n\n    .then(response =\u003e write(saveTo, response))\n\n}\n\ndownloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html')\n\n  .then(content =\u003e console.log(content))\n\n  .catch(error =\u003e console.error(error));  \n\n```\n\nPromise 提供了一些辅助方法，能让代码更简洁：\n\n| 方法                     | 描述                                       |  \n| ------------------------ | -----------------------------------------  |  \n| `Promise.resolve(value)` | 返回一个传入值解析后的 promise 。   |  \n| `Promise.reject(error)`  | 返回一个带有拒绝原因的 promise 。   |  \n| `Promise.all(promises)`  | 返回一个新的 promise，传入数组中的**每个** promise 都执行完成后返回的 promise 才算完成，或第一个 promise 拒绝而拒绝。|\n| `Promise.race(promises)` | 返回一个新的 promise，传入数组中的**某个** promise 解决或拒绝，返回的 promise 就会解决或拒绝。|\n\n\n`Promise.all`在并行运行任务时尤其有用，`Promise.race`让为 Promise 更容易实现超时。\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### `Async/Await` 比 `Promises` 更好\n\n使用`async`/`await`语法，可以编写更简洁、更易理解的链式 promise 的代码。一个函数使用`async`关键字作为前缀，JavaScript 运行时会暂停`await`关键字上的代码执行(当使用 promise 时)。\n\n**:-1: 反例:**\n\n```ts\n\nimport { get } from 'request';\n\nimport { writeFile } from 'fs';\n\nimport { promisify } from 'util';\n\nconst write = util.promisify(writeFile);\n\nfunction downloadPage(url: string, saveTo: string): Promise\u003cstring\u003e {\n\n  return get(url).then(response =\u003e write(saveTo, response))\n\n}\n\ndownloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html')\n\n  .then(content =\u003e console.log(content))\n\n  .catch(error =\u003e console.error(error));  \n\n```\n\n**:+1: 正例:**\n\n```ts\n\nimport { get } from 'request';\n\nimport { writeFile } from 'fs';\n\nimport { promisify } from 'util';\n\nconst write = promisify(writeFile);\n\nasync function downloadPage(url: string, saveTo: string): Promise\u003cstring\u003e {\n\n  const response = await get(url);\n\n  await write(saveTo, response);\n\n  return response;\n\n}\n\n// somewhere in an async function\n\ntry {\n\n  const content = await downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html');\n\n  console.log(content);\n\n} catch (error) {\n\n  console.error(error);\n\n}\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n## 错误处理\n\n抛出错误是件好事！它表示着运行时已经成功识别出程序中的错误，通过停止当前堆栈上的函数执行，终止进程(在Node.js)，以及在控制台中打印堆栈信息来让你知晓。\n\n### 抛出`Error`或 使用`reject`\n\nJavaScript 和 TypeScript 允许你 `throw` 任何对象。Promise 也可以用任何理由对象拒绝。\n\n建议使用 `Error` 类型的 `throw` 语法。因为你的错误可能在写有 `catch`语法的高级代码中被捕获。在那里捕获字符串消息显得非常混乱，并且会使[调试更加痛苦](https://basarat.gitbooks.io/typescript/docs/types/exceptions.html#always-use-error)。出于同样的原因，也应该在拒绝 promise 时使用 `Error `类型。\n\n**:-1: 反例:**\n\n```ts\n\nfunction calculateTotal(items: Item[]): number {\n\n  throw 'Not implemented.';\n\n}\n\nfunction get(): Promise\u003cItem[]\u003e {\n\n  return Promise.reject('Not implemented.');\n\n}\n\n```\n\n**:+1: 正例:**\n\n```ts\n\nfunction calculateTotal(items: Item[]): number {\n\n  throw new Error('Not implemented.');\n\n}\n\nfunction get(): Promise\u003cItem[]\u003e {\n\n  return Promise.reject(new Error('Not implemented.'));\n\n}\n\n// or equivalent to:\n\nasync function get(): Promise\u003cItem[]\u003e {\n\n  throw new Error('Not implemented.');\n\n}\n\n```\n\n使用 `Error` 类型的好处是 `try/catch/finally` 语法支持它，并且隐式地所有错误都具有  `stack` 属性，该属性对于调试非常有用。\n\n另外，即使不用 `throw` 语法而是返回自定义错误对象，TypeScript在这块更容易。考虑下面的例子:\n\n```ts\n\ntype Failable\u003cR, E\u003e = {\n\n  isError: true;\n\n  error: E;\n\n} | {\n\n  isError: false;\n\n  value: R;\n\n}\n\nfunction calculateTotal(items: Item[]): Failable\u003cnumber, 'empty'\u003e {\n\n  if (items.length === 0) {\n\n    return { isError: true, error: 'empty' };\n\n  }\n\n  // ...\n\n  return { isError: false, value: 42 };\n\n}\n\n```\n\n详细解释请参考[原文](https://medium.com/@dhruvrajvanshi/making-exceptions-type-safe-in-typescript-c4d200ee78e9)。\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 别忘了捕获错误\n\n捕获错误而不处理实际上也是没有修复错误，将错误记录到控制台(console.log）也好不到哪里去，因为它常常丢失在控制台大量的日志之中。如果将代码写在`try/catch` 中，说明那里可能会发生错误，因此应该考虑在错误发生时做一些处理。\n\n**:-1: 反例:**\n\n```ts\n\ntry {\n\n  functionThatMightThrow();\n\n} catch (error) {\n\n  console.log(error);\n\n}\n\n// or even worse\n\ntry {\n\n  functionThatMightThrow();\n\n} catch (error) {\n\n  // ignore error\n\n}\n\n```\n\n**:+1: 正例:**\n\n```ts\n\nimport { logger } from './logging'\n\ntry {\n\n  functionThatMightThrow();\n\n} catch (error) {\n\n  logger.log(error);\n\n}\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 不要忽略被拒绝的 promises\n\n理由和不能在`try/catch`中忽略`Error`一样。\n\n**:-1: 反例:**\n\n```ts\n\ngetUser()\n\n  .then((user: User) =\u003e {\n\n    return sendEmail(user.email, 'Welcome!');\n\n  })\n\n  .catch((error) =\u003e {\n\n    console.log(error);\n\n  });\n\n```\n\n**:+1: 正例:**\n\n```ts\n\nimport { logger } from './logging'\n\ngetUser()\n\n  .then((user: User) =\u003e {\n\n    return sendEmail(user.email, 'Welcome!');\n\n  })\n\n  .catch((error) =\u003e {\n\n    logger.log(error);\n\n  });\n\n// or using the async/await syntax:\n\ntry {\n\n  const user = await getUser();\n\n  await sendEmail(user.email, 'Welcome!');\n\n} catch (error) {\n\n  logger.log(error);\n\n}\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n## 格式化\n\n就像这里的许多规则一样，没有什么是硬性规定，格式化也是。重点是**不要争论**格式，使用自动化工具实现格式化。对于工程师来说，争论格式就是浪费时间和金钱。通用的原则是*保持一致的格式规则*。\n\n对于 TypeScript ，有一个强大的工具叫做 TSLint。它是一个静态分析工具，可以帮助您显著提高代码的可读性和可维护性。项目中使用可以参考以下 TSLint 配置:\n\n* [TSLint Config Standard](https://www.npmjs.com/package/tslint-config-standard) - 标准格式规则\n\n* [TSLint Config Airbnb](https://www.npmjs.com/package/tslint-config-airbnb) - Airbnb 格式规则\n\n* [TSLint Clean Code](https://www.npmjs.com/package/tslint-clean-code) - 灵感来自于[Clean Code: A Handbook of Agile Software Craftsmanship](https://www.amazon.ca/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882) 的 TSLint 规则。\n\n* [TSLint react](https://www.npmjs.com/package/tslint-react) - React 相关的Lint规则\n\n* [TSLint + Prettier](https://www.npmjs.com/package/tslint-config-prettier) - [Prettier](https://github.com/prettier/prettier) 代码格式化相关的 lint 规则\n\n* [ESLint rules for TSLint](https://www.npmjs.com/package/tslint-eslint-rules) - TypeScript 的 ESLint\n\n* [Immutable](https://www.npmjs.com/package/tslint-immutable) - 在 TypeScript 中禁用 mutation 的规则\n\n还可以参考[TypeScript 风格指南和编码约定](https://basarat.gitbooks.io/typescript/docs/styleguide/styleguide.html)的源代码。\n\n### 大小写一致\n\n大写可以告诉你很多关于变量、函数等的信息。这些都是主观规则，由你的团队做选择。关键是无论怎么选，都要*一致*。\n\n**:-1: 反例:**\n\n```ts\n\nconst DAYS_IN_WEEK = 7;\n\nconst daysInMonth = 30;\n\nconst songs = ['Back In Black', 'Stairway to Heaven', 'Hey Jude'];\n\nconst Artists = ['ACDC', 'Led Zeppelin', 'The Beatles'];\n\nfunction eraseDatabase() {}\n\nfunction restore_database() {}\n\nclass animal {}\n\nclass Container {}\n\n```\n\n**:+1: 正例:**\n\n```ts\n\nconst DAYS_IN_WEEK = 7;\n\nconst DAYS_IN_MONTH = 30;\n\nconst SONGS = ['Back In Black', 'Stairway to Heaven', 'Hey Jude'];\n\nconst ARTISTS = ['ACDC', 'Led Zeppelin', 'The Beatles'];\n\nfunction eraseDatabase() {}\n\nfunction restoreDatabase() {}\n\nclass Animal {}\n\nclass Container {}\n\n```\n\n类名、接口名、类型名和命名空间名最好使用“帕斯卡命名”。\n\n变量、函数和类成员使用“驼峰式命名”。\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 调用函数的函数和被调函数应靠近放置\n\n当函数间存在相互调用的情况时，应将两者靠近放置。最好是应将调用者写在被调者的上方。这就像读报纸一样，我们都是从上往下读，那么读代码也是。\n\n**:-1: 反例:**\n\n```ts\n\nclass PerformanceReview {\n\n  constructor(private readonly employee: Employee) {\n\n  }\n\n  private lookupPeers() {\n\n    return db.lookup(this.employee.id, 'peers');\n\n  }\n\n  private lookupManager() {\n\n    return db.lookup(this.employee, 'manager');\n\n  }\n\n  private getPeerReviews() {\n\n    const peers = this.lookupPeers();\n\n    // ...\n\n  }\n\n  review() {\n\n    this.getPeerReviews();\n\n    this.getManagerReview();\n\n    this.getSelfReview();\n\n    // ...\n\n  }\n\n  private getManagerReview() {\n\n    const manager = this.lookupManager();\n\n  }\n\n  private getSelfReview() {\n\n    // ...\n\n  }\n\n}\n\nconst review = new PerformanceReview(employee);\n\nreview.review();\n\n```\n\n**:+1: 正例:**\n\n```ts\n\nclass PerformanceReview {\n\n  constructor(private readonly employee: Employee) {\n\n  }\n\n  review() {\n\n    this.getPeerReviews();\n\n    this.getManagerReview();\n\n    this.getSelfReview();\n\n    // ...\n\n  }\n\n  private getPeerReviews() {\n\n    const peers = this.lookupPeers();\n\n    // ...\n\n  }\n\n  private lookupPeers() {\n\n    return db.lookup(this.employee.id, 'peers');\n\n  }\n\n  private getManagerReview() {\n\n    const manager = this.lookupManager();\n\n  }\n\n  private lookupManager() {\n\n    return db.lookup(this.employee, 'manager');\n\n  } \n\n  private getSelfReview() {\n\n    // ...\n\n  }\n\n}\n\nconst review = new PerformanceReview(employee);\n\nreview.review();\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 组织导入\n\n使用整洁且易于阅读的`import`语句，您可以快速查看当前代码的依赖关系。导入语句应遵循以下做法:\n\n- `Import`语句应该按字母顺序排列和分组。\n- 应该删除未使用的导入语句。\n- 命名导入必须按字母顺序(例如：`import {A, B, C} from 'foo';`)。\n- 导入源必须在组中按字母顺序排列。 例如: `import * as foo from 'a'; import * as bar from 'b';`\n- 导入组用空行隔开。\n- 组内按照如下排序:\n  - Polyfills (例如: `import 'reflect-metadata';`)\n  - Node 内置模块 (例如: `import fs from 'fs';`)\n  - 外部模块 (例如: `import { query } from 'itiriri';`)\n  - 内部模块 (例如: `import { UserService } from 'src/services/userService';`)\n  - 父目录中的模块 (例如: `import foo from '../foo'; import qux from '../../foo/qux';`)\n  - 来自相同或兄弟目录的模块 (例如: `import bar from './bar'; import baz from './bar/baz';`)\n\n**:-1: 反例:**\n\n```ts\nimport { TypeDefinition } from '../types/typeDefinition';\nimport { AttributeTypes } from '../model/attribute';\nimport { ApiCredentials, Adapters } from './common/api/authorization';\nimport fs from 'fs';\nimport { ConfigPlugin } from './plugins/config/configPlugin';\nimport { BindingScopeEnum, Container } from 'inversify';\nimport 'reflect-metadata';\n```\n\n**:+1: 正例:**\n\n```ts\nimport 'reflect-metadata';\n\nimport fs from 'fs';\nimport { BindingScopeEnum, Container } from 'inversify';\n\nimport { AttributeTypes } from '../model/attribute';\nimport { TypeDefinition } from '../types/typeDefinition';\n\nimport { ApiCredentials, Adapters } from './common/api/authorization';\nimport { ConfigPlugin } from './plugins/config/configPlugin';\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 使用 typescript 别名\n\n为了创建整洁漂亮的导入语句，可以在`tsconfig.json`中设置编译器选项的`paths`和`baseUrl`属性。\n\n这样可以避免导入时使用较长的相对路径。\n\n**:-1: 反例:**\n\n```ts\nimport { UserService } from '../../../services/UserService';\n```\n\n**:+1: 正例:**\n\n```ts\nimport { UserService } from '@services/UserService';\n```\n\n```js\n// tsconfig.json\n...\n  \"compilerOptions\": {\n    ...\n    \"baseUrl\": \"src\",\n    \"paths\": {\n      \"@services\": [\"services/*\"]\n    }\n    ...\n  }\n...\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n\n## 注释\n\n写注释意味着没有注释就无法表达清楚，而最好用代码去表达。\n\n\u003e 不要注释坏代码，重写吧！— *Brian W. Kernighan and P. J. Plaugher*\n\n### 代码自解释而不是用注释\n\n代码即文档。\n\n**:-1: 反例:**\n\n```ts\n\n// Check if subscription is active.\n\nif (subscription.endDate \u003e Date.now) {  }\n\n```\n\n**:+1: 正例:**\n\n```ts\n\nconst isSubscriptionActive = subscription.endDate \u003e Date.now;\n\nif (isSubscriptionActive) { /* ... */ }\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 不要将注释掉的代码留在代码库中\n\n版本控制存在的一个理由，就是让旧代码成为历史。\n\n**:-1: 反例:**\n\n```ts\n\nclass User {\n\n  name: string;\n\n  email: string;\n\n  // age: number;\n\n  // jobPosition: string;\n\n}\n\n```\n\n**:+1: 正例:**\n\n```ts\n\nclass User {\n\n  name: string;\n\n  email: string;\n\n}\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 不要像写日记一样写注释\n\n记住，使用版本控制！不需要保留无用代码、注释掉的代码，尤其像日记一样的注释。使用`git log`来获取历史。\n\n**:-1: 反例:**\n\n```ts\n\n/**\n\n * 2016-12-20: Removed monads, didn't understand them (RM)\n\n * 2016-10-01: Improved using special monads (JP)\n\n * 2016-02-03: Added type-checking (LI)\n\n * 2015-03-14: Implemented combine (JR)\n\n */\n\nfunction combine(a:number, b:number): number {\n\n  return a + b;\n\n}\n\n```\n\n**:+1: 正例:**\n\n```ts\n\nfunction combine(a:number, b:number): number {\n\n  return a + b;\n\n}\n\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### 避免使用注释标记位置\n\n它们常常扰乱代码。要让代码结构化，函数和变量要有合适的缩进和格式。\n\n另外，你可以使用支持代码折叠的IDE (看下 Visual Studio Code [代码折叠](https://code.visualstudio.com/updates/v1_17#_folding-regions)).\n\n**:-1: 反例:**\n\n```ts\n\n////////////////////////////////////////////////////////////////////////////////\n\n// Client class\n\n////////////////////////////////////////////////////////////////////////////////\n\nclass Client {\n\n  id: number;\n\n  name: string;\n\n  address: Address;\n\n  contact: Contact;\n\n  ////////////////////////////////////////////////////////////////////////////////\n\n  // public methods\n\n  ////////////////////////////////////////////////////////////////////////////////\n\n  public describe(): string {\n\n    // ...\n\n  }\n\n  ////////////////////////////////////////////////////////////////////////////////\n\n  // private methods\n\n  ////////////////////////////////////////////////////////////////////////////////\n\n  private describeAddress(): string {\n\n    // ...\n\n  }\n\n  private describeContact(): string {\n\n    // ...\n\n  }\n\n};\n\n```\n\n**:+1: 正例:**\n\n```ts\n\nclass Client {\n\n  id: number;\n\n  name: string;\n\n  address: Address;\n\n  contact: Contact;\n\n  public describe(): string {\n\n    // ...\n\n  }\n\n  private describeAddress(): string {\n\n    // ...\n\n  }\n\n  private describeContact(): string {\n\n    // ...\n\n  }\n\n};\n\n```\n\n\n**[\u0026uarr; 回到顶部](#目录)**\n\n### TODO 注释 \n\n当发现自己需要在代码中留下注释，以提醒后续改进时，使用`// TODO`注释。大多数IDE都对这类注释提供了特殊的支持，你可以快速浏览整个`TODO`列表。\n\n但是，请记住**TODO**注释并不是坏代码的借口。\n\n**:-1: 反例:**\n\n```ts\nfunction getActiveSubscriptions(): Promise\u003cSubscription[]\u003e {\n  // ensure `dueDate` is indexed.\n  return db.subscriptions.find({ dueDate: { $lte: new Date() } });\n}\n```\n\n**:+1: 正例:**\n\n```ts\nfunction getActiveSubscriptions(): Promise\u003cSubscription[]\u003e {\n  // TODO: ensure `dueDate` is indexed.\n  return db.subscriptions.find({ dueDate: { $lte: new Date() } });\n}\n```\n\n**[\u0026uarr; 回到顶部](#目录)**\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpipiliang%2Fclean-code-typescript","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpipiliang%2Fclean-code-typescript","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpipiliang%2Fclean-code-typescript/lists"}