├── .gitattributes ├── LICENSE ├── README-en.md ├── README.md ├── _config.yml └── clean-code-typescript.png /.gitattributes: -------------------------------------------------------------------------------- 1 | *.md linguist-documentation=false 2 | *.md linguist-language=TypeScript -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Junwoo Ji 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | ------------------------------------------------------------------------------ 24 | 25 | The MIT License (MIT) 26 | 27 | Copyright (c) 2019 Labs42 28 | 29 | Permission is hereby granted, free of charge, to any person obtaining a copy 30 | of this software and associated documentation files (the "Software"), to deal 31 | in the Software without restriction, including without limitation the rights 32 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 33 | copies of the Software, and to permit persons to whom the Software is 34 | furnished to do so, subject to the following conditions: 35 | 36 | The above copyright notice and this permission notice shall be included in all 37 | copies or substantial portions of the Software. 38 | 39 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 40 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 41 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 42 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 43 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 44 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 45 | SOFTWARE. 46 | 47 | -------------------------------------------------------------------------------- /README-en.md: -------------------------------------------------------------------------------- 1 | # clean-code-typescript [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=Clean%20Code%20Typescript&url=https://github.com/labs42io/clean-code-typescript) 2 | 3 | Clean Code concepts adapted for TypeScript. 4 | Inspired from [clean-code-javascript](https://github.com/ryanmcdermott/clean-code-javascript). 5 | 6 | ## Table of Contents 7 | 8 | 1. [Introduction](#introduction) 9 | 2. [Variables](#variables) 10 | 3. [Functions](#functions) 11 | 4. [Objects and Data Structures](#objects-and-data-structures) 12 | 5. [Classes](#classes) 13 | 6. [SOLID](#solid) 14 | 7. [Testing](#testing) 15 | 8. [Concurrency](#concurrency) 16 | 9. [Error Handling](#error-handling) 17 | 10. [Formatting](#formatting) 18 | 11. [Comments](#comments) 19 | 12. [Translations](#translations) 20 | 21 | ## Introduction 22 | 23 | ![Humorous image of software quality estimation as a count of how many expletives 24 | you shout when reading code](https://www.osnews.com/images/comics/wtfm.jpg) 25 | 26 | Software engineering principles, from Robert C. Martin's book 27 | [*Clean Code*](https://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882), 28 | adapted for TypeScript. This is not a style guide. It's a guide to producing 29 | [readable, reusable, and refactorable](https://github.com/ryanmcdermott/3rs-of-software-architecture) software in TypeScript. 30 | 31 | Not every principle herein has to be strictly followed, and even fewer will be 32 | universally agreed upon. These are guidelines and nothing more, but they are 33 | ones codified over many years of collective experience by the authors of 34 | *Clean Code*. 35 | 36 | Our craft of software engineering is just a bit over 50 years old, and we are 37 | still learning a lot. When software architecture is as old as architecture 38 | itself, maybe then we will have harder rules to follow. For now, let these 39 | guidelines serve as a touchstone by which to assess the quality of the 40 | TypeScript code that you and your team produce. 41 | 42 | One more thing: knowing these won't immediately make you a better software 43 | developer, and working with them for many years doesn't mean you won't make 44 | mistakes. Every piece of code starts as a first draft, like wet clay getting 45 | shaped into its final form. Finally, we chisel away the imperfections when 46 | we review it with our peers. Don't beat yourself up for first drafts that need 47 | improvement. Beat up the code instead! 48 | 49 | **[⬆ back to top](#table-of-contents)** 50 | 51 | ## Variables 52 | 53 | ### Use meaningful variable names 54 | 55 | Distinguish names in such a way that the reader knows what the differences offer. 56 | 57 | **Bad:** 58 | 59 | ```ts 60 | function between(a1: T, a2: T, a3: T): boolean { 61 | return a2 <= a1 && a1 <= a3; 62 | } 63 | 64 | ``` 65 | 66 | **Good:** 67 | 68 | ```ts 69 | function between(value: T, left: T, right: T): boolean { 70 | return left <= value && value <= right; 71 | } 72 | ``` 73 | 74 | **[⬆ back to top](#table-of-contents)** 75 | 76 | ### Use pronounceable variable names 77 | 78 | If you can’t pronounce it, you can’t discuss it without sounding like an idiot. 79 | 80 | **Bad:** 81 | 82 | ```ts 83 | type DtaRcrd102 = { 84 | genymdhms: Date; 85 | modymdhms: Date; 86 | pszqint: number; 87 | } 88 | ``` 89 | 90 | **Good:** 91 | 92 | ```ts 93 | type Customer = { 94 | generationTimestamp: Date; 95 | modificationTimestamp: Date; 96 | recordId: number; 97 | } 98 | ``` 99 | 100 | **[⬆ back to top](#table-of-contents)** 101 | 102 | ### Use the same vocabulary for the same type of variable 103 | 104 | **Bad:** 105 | 106 | ```ts 107 | function getUserInfo(): User; 108 | function getUserDetails(): User; 109 | function getUserData(): User; 110 | ``` 111 | 112 | **Good:** 113 | 114 | ```ts 115 | function getUser(): User; 116 | ``` 117 | 118 | **[⬆ back to top](#table-of-contents)** 119 | 120 | ### Use searchable names 121 | 122 | We will read more code than we will ever write. It's important that the code we do write is readable and searchable. By *not* naming variables that end up being meaningful for understanding our program, we hurt our readers. Make your names searchable. Tools like [TSLint](https://palantir.github.io/tslint/rules/no-magic-numbers/) can help identify unnamed constants. 123 | 124 | **Bad:** 125 | 126 | ```ts 127 | // What the heck is 86400000 for? 128 | setTimeout(restart, 86400000); 129 | ``` 130 | 131 | **Good:** 132 | 133 | ```ts 134 | // Declare them as capitalized named constants. 135 | const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000; 136 | 137 | setTimeout(restart, MILLISECONDS_IN_A_DAY); 138 | ``` 139 | 140 | **[⬆ back to top](#table-of-contents)** 141 | 142 | ### Use explanatory variables 143 | 144 | **Bad:** 145 | 146 | ```ts 147 | declare const users: Map; 148 | 149 | for (const keyValue of users) { 150 | // iterate through users map 151 | } 152 | ``` 153 | 154 | **Good:** 155 | 156 | ```ts 157 | declare const users: Map; 158 | 159 | for (const [id, user] of users) { 160 | // iterate through users map 161 | } 162 | ``` 163 | 164 | **[⬆ back to top](#table-of-contents)** 165 | 166 | ### Avoid Mental Mapping 167 | 168 | Explicit is better than implicit. 169 | *Clarity is king.* 170 | 171 | **Bad:** 172 | 173 | ```ts 174 | const u = getUser(); 175 | const s = getSubscription(); 176 | const t = charge(u, s); 177 | ``` 178 | 179 | **Good:** 180 | 181 | ```ts 182 | const user = getUser(); 183 | const subscription = getSubscription(); 184 | const transaction = charge(user, subscription); 185 | ``` 186 | 187 | **[⬆ back to top](#table-of-contents)** 188 | 189 | ### Don't add unneeded context 190 | 191 | If your class/type/object name tells you something, don't repeat that in your variable name. 192 | 193 | **Bad:** 194 | 195 | ```ts 196 | type Car = { 197 | carMake: string; 198 | carModel: string; 199 | carColor: string; 200 | } 201 | 202 | function print(car: Car): void { 203 | console.log(`${car.carMake} ${car.carModel} (${car.carColor})`); 204 | } 205 | ``` 206 | 207 | **Good:** 208 | 209 | ```ts 210 | type Car = { 211 | make: string; 212 | model: string; 213 | color: string; 214 | } 215 | 216 | function print(car: Car): void { 217 | console.log(`${car.make} ${car.model} (${car.color})`); 218 | } 219 | ``` 220 | 221 | **[⬆ back to top](#table-of-contents)** 222 | 223 | ### Use default arguments instead of short circuiting or conditionals 224 | 225 | Default arguments are often cleaner than short circuiting. 226 | 227 | **Bad:** 228 | 229 | ```ts 230 | function loadPages(count?: number) { 231 | const loadCount = count !== undefined ? count : 10; 232 | // ... 233 | } 234 | ``` 235 | 236 | **Good:** 237 | 238 | ```ts 239 | function loadPages(count: number = 10) { 240 | // ... 241 | } 242 | ``` 243 | 244 | **[⬆ back to top](#table-of-contents)** 245 | 246 | ### Use enum to document the intent 247 | 248 | Enums can help you document the intent of the code. For example when we are concerned about values being 249 | different rather than the exact value of those. 250 | 251 | **Bad:** 252 | 253 | ```ts 254 | const GENRE = { 255 | ROMANTIC: 'romantic', 256 | DRAMA: 'drama', 257 | COMEDY: 'comedy', 258 | DOCUMENTARY: 'documentary', 259 | } 260 | 261 | projector.configureFilm(GENRE.COMEDY); 262 | 263 | class Projector { 264 | // delactation of Projector 265 | configureFilm(genre) { 266 | switch (genre) { 267 | case GENRE.ROMANTIC: 268 | // some logic to be executed 269 | } 270 | } 271 | } 272 | ``` 273 | 274 | **Good:** 275 | 276 | ```ts 277 | enum GENRE { 278 | ROMANTIC, 279 | DRAMA, 280 | COMEDY, 281 | DOCUMENTARY, 282 | } 283 | 284 | projector.configureFilm(GENRE.COMEDY); 285 | 286 | class Projector { 287 | // delactation of Projector 288 | configureFilm(genre) { 289 | switch (genre) { 290 | case GENRE.ROMANTIC: 291 | // some logic to be executed 292 | } 293 | } 294 | } 295 | ``` 296 | 297 | **[⬆ back to top](#table-of-contents)** 298 | 299 | ## Functions 300 | 301 | ### Function arguments (2 or fewer ideally) 302 | 303 | Limiting the amount of function parameters is incredibly important because it makes testing your function easier. 304 | Having more than three leads to a combinatorial explosion where you have to test tons of different cases with each separate argument. 305 | 306 | One or two arguments is the ideal case, and three should be avoided if possible. Anything more than that should be consolidated. 307 | Usually, if you have more than two arguments then your function is trying to do too much. 308 | In cases where it's not, most of the time a higher-level object will suffice as an argument. 309 | 310 | Consider using object literals if you are finding yourself needing a lot of arguments. 311 | 312 | To make it obvious what properties the function expects, you can use the [destructuring](https://basarat.gitbook.io/typescript/future-javascript/destructuring) syntax. 313 | This has a few advantages: 314 | 315 | 1. When someone looks at the function signature, it's immediately clear what properties are being used. 316 | 317 | 2. It can be used to simulate named parameters. 318 | 319 | 3. Destructuring also clones the specified primitive values of the argument object passed into the function. This can help prevent side effects. Note: objects and arrays that are destructured from the argument object are NOT cloned. 320 | 321 | 4. TypeScript warns you about unused properties, which would be impossible without destructuring. 322 | 323 | **Bad:** 324 | 325 | ```ts 326 | function createMenu(title: string, body: string, buttonText: string, cancellable: boolean) { 327 | // ... 328 | } 329 | 330 | createMenu('Foo', 'Bar', 'Baz', true); 331 | ``` 332 | 333 | **Good:** 334 | 335 | ```ts 336 | function createMenu(options: { title: string, body: string, buttonText: string, cancellable: boolean }) { 337 | // ... 338 | } 339 | 340 | createMenu({ 341 | title: 'Foo', 342 | body: 'Bar', 343 | buttonText: 'Baz', 344 | cancellable: true 345 | }); 346 | ``` 347 | 348 | You can further improve readability by using [type aliases](https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-aliases): 349 | 350 | ```ts 351 | 352 | type MenuOptions = { title: string, body: string, buttonText: string, cancellable: boolean }; 353 | 354 | function createMenu(options: MenuOptions) { 355 | // ... 356 | } 357 | 358 | createMenu({ 359 | title: 'Foo', 360 | body: 'Bar', 361 | buttonText: 'Baz', 362 | cancellable: true 363 | }); 364 | ``` 365 | 366 | **[⬆ back to top](#table-of-contents)** 367 | 368 | ### Functions should do one thing 369 | 370 | This is by far the most important rule in software engineering. When functions do more than one thing, they are harder to compose, test, and reason about. When you can isolate a function to just one action, it can be refactored easily and your code will read much cleaner. If you take nothing else away from this guide other than this, you'll be ahead of many developers. 371 | 372 | **Bad:** 373 | 374 | ```ts 375 | function emailClients(clients: Client[]) { 376 | clients.forEach((client) => { 377 | const clientRecord = database.lookup(client); 378 | if (clientRecord.isActive()) { 379 | email(client); 380 | } 381 | }); 382 | } 383 | ``` 384 | 385 | **Good:** 386 | 387 | ```ts 388 | function emailClients(clients: Client[]) { 389 | clients.filter(isActiveClient).forEach(email); 390 | } 391 | 392 | function isActiveClient(client: Client) { 393 | const clientRecord = database.lookup(client); 394 | return clientRecord.isActive(); 395 | } 396 | ``` 397 | 398 | **[⬆ back to top](#table-of-contents)** 399 | 400 | ### Function names should say what they do 401 | 402 | **Bad:** 403 | 404 | ```ts 405 | function addToDate(date: Date, month: number): Date { 406 | // ... 407 | } 408 | 409 | const date = new Date(); 410 | 411 | // It's hard to tell from the function name what is added 412 | addToDate(date, 1); 413 | ``` 414 | 415 | **Good:** 416 | 417 | ```ts 418 | function addMonthToDate(date: Date, month: number): Date { 419 | // ... 420 | } 421 | 422 | const date = new Date(); 423 | addMonthToDate(date, 1); 424 | ``` 425 | 426 | **[⬆ back to top](#table-of-contents)** 427 | 428 | ### Functions should only be one level of abstraction 429 | 430 | When you have more than one level of abstraction your function is usually doing too much. Splitting up functions leads to reusability and easier testing. 431 | 432 | **Bad:** 433 | 434 | ```ts 435 | function parseCode(code: string) { 436 | const REGEXES = [ /* ... */ ]; 437 | const statements = code.split(' '); 438 | const tokens = []; 439 | 440 | REGEXES.forEach((regex) => { 441 | statements.forEach((statement) => { 442 | // ... 443 | }); 444 | }); 445 | 446 | const ast = []; 447 | tokens.forEach((token) => { 448 | // lex... 449 | }); 450 | 451 | ast.forEach((node) => { 452 | // parse... 453 | }); 454 | } 455 | ``` 456 | 457 | **Good:** 458 | 459 | ```ts 460 | const REGEXES = [ /* ... */ ]; 461 | 462 | function parseCode(code: string) { 463 | const tokens = tokenize(code); 464 | const syntaxTree = parse(tokens); 465 | 466 | syntaxTree.forEach((node) => { 467 | // parse... 468 | }); 469 | } 470 | 471 | function tokenize(code: string): Token[] { 472 | const statements = code.split(' '); 473 | const tokens: Token[] = []; 474 | 475 | REGEXES.forEach((regex) => { 476 | statements.forEach((statement) => { 477 | tokens.push( /* ... */ ); 478 | }); 479 | }); 480 | 481 | return tokens; 482 | } 483 | 484 | function parse(tokens: Token[]): SyntaxTree { 485 | const syntaxTree: SyntaxTree[] = []; 486 | tokens.forEach((token) => { 487 | syntaxTree.push( /* ... */ ); 488 | }); 489 | 490 | return syntaxTree; 491 | } 492 | ``` 493 | 494 | **[⬆ back to top](#table-of-contents)** 495 | 496 | ### Remove duplicate code 497 | 498 | Do your absolute best to avoid duplicate code. 499 | Duplicate code is bad because it means that there's more than one place to alter something if you need to change some logic. 500 | 501 | Imagine if you run a restaurant and you keep track of your inventory: all your tomatoes, onions, garlic, spices, etc. 502 | If you have multiple lists that you keep this on, then all have to be updated when you serve a dish with tomatoes in them. 503 | If you only have one list, there's only one place to update! 504 | 505 | Oftentimes you have duplicate code because you have two or more slightly different things, that share a lot in common, but their differences force you to have two or more separate functions that do much of the same things. Removing duplicate code means creating an abstraction that can handle this set of different things with just one function/module/class. 506 | 507 | Getting the abstraction right is critical, that's why you should follow the [SOLID](#solid) principles. Bad abstractions can be worse than duplicate code, so be careful! Having said this, if you can make a good abstraction, do it! Don't repeat yourself, otherwise you'll find yourself updating multiple places anytime you want to change one thing. 508 | 509 | **Bad:** 510 | 511 | ```ts 512 | function showDeveloperList(developers: Developer[]) { 513 | developers.forEach((developer) => { 514 | const expectedSalary = developer.calculateExpectedSalary(); 515 | const experience = developer.getExperience(); 516 | const githubLink = developer.getGithubLink(); 517 | 518 | const data = { 519 | expectedSalary, 520 | experience, 521 | githubLink 522 | }; 523 | 524 | render(data); 525 | }); 526 | } 527 | 528 | function showManagerList(managers: Manager[]) { 529 | managers.forEach((manager) => { 530 | const expectedSalary = manager.calculateExpectedSalary(); 531 | const experience = manager.getExperience(); 532 | const portfolio = manager.getMBAProjects(); 533 | 534 | const data = { 535 | expectedSalary, 536 | experience, 537 | portfolio 538 | }; 539 | 540 | render(data); 541 | }); 542 | } 543 | ``` 544 | 545 | **Good:** 546 | 547 | ```ts 548 | class Developer { 549 | // ... 550 | getExtraDetails() { 551 | return { 552 | githubLink: this.githubLink, 553 | } 554 | } 555 | } 556 | 557 | class Manager { 558 | // ... 559 | getExtraDetails() { 560 | return { 561 | portfolio: this.portfolio, 562 | } 563 | } 564 | } 565 | 566 | function showEmployeeList(employee: Developer | Manager) { 567 | employee.forEach((employee) => { 568 | const expectedSalary = employee.calculateExpectedSalary(); 569 | const experience = employee.getExperience(); 570 | const extra = employee.getExtraDetails(); 571 | 572 | const data = { 573 | expectedSalary, 574 | experience, 575 | extra, 576 | }; 577 | 578 | render(data); 579 | }); 580 | } 581 | ``` 582 | 583 | You should be critical about code duplication. Sometimes there is a tradeoff between duplicated code and increased complexity by introducing unnecessary abstraction. When two implementations from two different modules look similar but live in different domains, duplication might be acceptable and preferred over extracting the common code. The extracted common code in this case introduces an indirect dependency between the two modules. 584 | 585 | **[⬆ back to top](#table-of-contents)** 586 | 587 | ### Set default objects with Object.assign or destructuring 588 | 589 | **Bad:** 590 | 591 | ```ts 592 | type MenuConfig = { title?: string, body?: string, buttonText?: string, cancellable?: boolean }; 593 | 594 | function createMenu(config: MenuConfig) { 595 | config.title = config.title || 'Foo'; 596 | config.body = config.body || 'Bar'; 597 | config.buttonText = config.buttonText || 'Baz'; 598 | config.cancellable = config.cancellable !== undefined ? config.cancellable : true; 599 | 600 | // ... 601 | } 602 | 603 | createMenu({ body: 'Bar' }); 604 | ``` 605 | 606 | **Good:** 607 | 608 | ```ts 609 | type MenuConfig = { title?: string, body?: string, buttonText?: string, cancellable?: boolean }; 610 | 611 | function createMenu(config: MenuConfig) { 612 | const menuConfig = Object.assign({ 613 | title: 'Foo', 614 | body: 'Bar', 615 | buttonText: 'Baz', 616 | cancellable: true 617 | }, config); 618 | 619 | // ... 620 | } 621 | 622 | createMenu({ body: 'Bar' }); 623 | ``` 624 | 625 | Alternatively, you can use destructuring with default values: 626 | 627 | ```ts 628 | type MenuConfig = { title?: string, body?: string, buttonText?: string, cancellable?: boolean }; 629 | 630 | function createMenu({ title = 'Foo', body = 'Bar', buttonText = 'Baz', cancellable = true }: MenuConfig) { 631 | // ... 632 | } 633 | 634 | createMenu({ body: 'Bar' }); 635 | ``` 636 | 637 | To avoid any side effects and unexpected behavior by passing in explicitly the `undefined` or `null` value, you can tell the TypeScript compiler to not allow it. 638 | See [`--strictNullChecks`](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#--strictnullchecks) option in TypeScript. 639 | 640 | **[⬆ back to top](#table-of-contents)** 641 | 642 | ### Don't use flags as function parameters 643 | 644 | Flags tell your user that this function does more than one thing. 645 | Functions should do one thing. Split out your functions if they are following different code paths based on a boolean. 646 | 647 | **Bad:** 648 | 649 | ```ts 650 | function createFile(name: string, temp: boolean) { 651 | if (temp) { 652 | fs.create(`./temp/${name}`); 653 | } else { 654 | fs.create(name); 655 | } 656 | } 657 | ``` 658 | 659 | **Good:** 660 | 661 | ```ts 662 | function createTempFile(name: string) { 663 | createFile(`./temp/${name}`); 664 | } 665 | 666 | function createFile(name: string) { 667 | fs.create(name); 668 | } 669 | ``` 670 | 671 | **[⬆ back to top](#table-of-contents)** 672 | 673 | ### Avoid Side Effects (part 1) 674 | 675 | A function produces a side effect if it does anything other than take a value in and return another value or values. 676 | A side effect could be writing to a file, modifying some global variable, or accidentally wiring all your money to a stranger. 677 | 678 | Now, you do need to have side effects in a program on occasion. Like the previous example, you might need to write to a file. 679 | What you want to do is to centralize where you are doing this. Don't have several functions and classes that write to a particular file. 680 | Have one service that does it. One and only one. 681 | 682 | The main point is to avoid common pitfalls like sharing state between objects without any structure, using mutable data types that can be written to by anything, and not centralizing where your side effects occur. If you can do this, you will be happier than the vast majority of other programmers. 683 | 684 | **Bad:** 685 | 686 | ```ts 687 | // Global variable referenced by following function. 688 | let name = 'Robert C. Martin'; 689 | 690 | function toBase64() { 691 | name = btoa(name); 692 | } 693 | 694 | toBase64(); 695 | // If we had another function that used this name, now it'd be a Base64 value 696 | 697 | console.log(name); // expected to print 'Robert C. Martin' but instead 'Um9iZXJ0IEMuIE1hcnRpbg==' 698 | ``` 699 | 700 | **Good:** 701 | 702 | ```ts 703 | const name = 'Robert C. Martin'; 704 | 705 | function toBase64(text: string): string { 706 | return btoa(text); 707 | } 708 | 709 | const encodedName = toBase64(name); 710 | console.log(name); 711 | ``` 712 | 713 | **[⬆ back to top](#table-of-contents)** 714 | 715 | ### Avoid Side Effects (part 2) 716 | 717 | In JavaScript, primitives are passed by value and objects/arrays are passed by reference. In the case of objects and arrays, if your function makes a change in a shopping cart array, for example, by adding an item to purchase, then any other function that uses that `cart` array will be affected by this addition. That may be great, however it can be bad too. Let's imagine a bad situation: 718 | 719 | The user clicks the "Purchase", button which calls a `purchase` function that spawns a network request and sends the `cart` array to the server. Because of a bad network connection, the purchase function has to keep retrying the request. Now, what if in the meantime the user accidentally clicks "Add to Cart" button on an item they don't actually want before the network request begins? If that happens and the network request begins, then that purchase function will send the accidentally added item because it has a reference to a shopping cart array that the `addItemToCart` function modified by adding an unwanted item. 720 | 721 | A great solution would be for the `addItemToCart` to always clone the `cart`, edit it, and return the clone. This ensures that no other functions that are holding onto a reference of the shopping cart will be affected by any changes. 722 | 723 | Two caveats to mention to this approach: 724 | 725 | 1. There might be cases where you actually want to modify the input object, but when you adopt this programming practice you will find that those cases are pretty rare. Most things can be refactored to have no side effects! (see [pure function](https://en.wikipedia.org/wiki/Pure_function)) 726 | 727 | 2. Cloning big objects can be very expensive in terms of performance. Luckily, this isn't a big issue in practice because there are great libraries that allow this kind of programming approach to be fast and not as memory intensive as it would be for you to manually clone objects and arrays. 728 | 729 | **Bad:** 730 | 731 | ```ts 732 | function addItemToCart(cart: CartItem[], item: Item): void { 733 | cart.push({ item, date: Date.now() }); 734 | }; 735 | ``` 736 | 737 | **Good:** 738 | 739 | ```ts 740 | function addItemToCart(cart: CartItem[], item: Item): CartItem[] { 741 | return [...cart, { item, date: Date.now() }]; 742 | }; 743 | ``` 744 | 745 | **[⬆ back to top](#table-of-contents)** 746 | 747 | ### Don't write to global functions 748 | 749 | Polluting globals is a bad practice in JavaScript because you could clash with another library and the user of your API would be none-the-wiser until they get an exception in production. Let's think about an example: what if you wanted to extend JavaScript's native Array method to have a `diff` method that could show the difference between two arrays? You could write your new function to the `Array.prototype`, but it could clash with another library that tried to do the same thing. What if that other library was just using `diff` to find the difference between the first and last elements of an array? This is why it would be much better to just use classes and simply extend the `Array` global. 750 | 751 | **Bad:** 752 | 753 | ```ts 754 | declare global { 755 | interface Array { 756 | diff(other: T[]): Array; 757 | } 758 | } 759 | 760 | if (!Array.prototype.diff) { 761 | Array.prototype.diff = function (other: T[]): T[] { 762 | const hash = new Set(other); 763 | return this.filter(elem => !hash.has(elem)); 764 | }; 765 | } 766 | ``` 767 | 768 | **Good:** 769 | 770 | ```ts 771 | class MyArray extends Array { 772 | diff(other: T[]): T[] { 773 | const hash = new Set(other); 774 | return this.filter(elem => !hash.has(elem)); 775 | }; 776 | } 777 | ``` 778 | 779 | **[⬆ back to top](#table-of-contents)** 780 | 781 | ### Favor functional programming over imperative programming 782 | 783 | Favor this style of programming when you can. 784 | 785 | **Bad:** 786 | 787 | ```ts 788 | const contributions = [ 789 | { 790 | name: 'Uncle Bobby', 791 | linesOfCode: 500 792 | }, { 793 | name: 'Suzie Q', 794 | linesOfCode: 1500 795 | }, { 796 | name: 'Jimmy Gosling', 797 | linesOfCode: 150 798 | }, { 799 | name: 'Gracie Hopper', 800 | linesOfCode: 1000 801 | } 802 | ]; 803 | 804 | let totalOutput = 0; 805 | 806 | for (let i = 0; i < contributions.length; i++) { 807 | totalOutput += contributions[i].linesOfCode; 808 | } 809 | ``` 810 | 811 | **Good:** 812 | 813 | ```ts 814 | const contributions = [ 815 | { 816 | name: 'Uncle Bobby', 817 | linesOfCode: 500 818 | }, { 819 | name: 'Suzie Q', 820 | linesOfCode: 1500 821 | }, { 822 | name: 'Jimmy Gosling', 823 | linesOfCode: 150 824 | }, { 825 | name: 'Gracie Hopper', 826 | linesOfCode: 1000 827 | } 828 | ]; 829 | 830 | const totalOutput = contributions 831 | .reduce((totalLines, output) => totalLines + output.linesOfCode, 0); 832 | ``` 833 | 834 | **[⬆ back to top](#table-of-contents)** 835 | 836 | ### Encapsulate conditionals 837 | 838 | **Bad:** 839 | 840 | ```ts 841 | if (subscription.isTrial || account.balance > 0) { 842 | // ... 843 | } 844 | ``` 845 | 846 | **Good:** 847 | 848 | ```ts 849 | function canActivateService(subscription: Subscription, account: Account) { 850 | return subscription.isTrial || account.balance > 0; 851 | } 852 | 853 | if (canActivateService(subscription, account)) { 854 | // ... 855 | } 856 | ``` 857 | 858 | **[⬆ back to top](#table-of-contents)** 859 | 860 | ### Avoid negative conditionals 861 | 862 | **Bad:** 863 | 864 | ```ts 865 | function isEmailNotUsed(email: string): boolean { 866 | // ... 867 | } 868 | 869 | if (isEmailNotUsed(email)) { 870 | // ... 871 | } 872 | ``` 873 | 874 | **Good:** 875 | 876 | ```ts 877 | function isEmailUsed(email): boolean { 878 | // ... 879 | } 880 | 881 | if (!isEmailUsed(node)) { 882 | // ... 883 | } 884 | ``` 885 | 886 | **[⬆ back to top](#table-of-contents)** 887 | 888 | ### Avoid conditionals 889 | 890 | This seems like an impossible task. Upon first hearing this, most people say, "how am I supposed to do anything without an `if` statement?" The answer is that you can use polymorphism to achieve the same task in many cases. The second question is usually, "well that's great but why would I want to do that?" The answer is a previous clean code concept we learned: a function should only do one thing. When you have classes and functions that have `if` statements, you are telling your user that your function does more than one thing. Remember, just do one thing. 891 | 892 | **Bad:** 893 | 894 | ```ts 895 | class Airplane { 896 | private type: string; 897 | // ... 898 | 899 | getCruisingAltitude() { 900 | switch (this.type) { 901 | case '777': 902 | return this.getMaxAltitude() - this.getPassengerCount(); 903 | case 'Air Force One': 904 | return this.getMaxAltitude(); 905 | case 'Cessna': 906 | return this.getMaxAltitude() - this.getFuelExpenditure(); 907 | default: 908 | throw new Error('Unknown airplane type.'); 909 | } 910 | } 911 | 912 | private getMaxAltitude(): number { 913 | // ... 914 | } 915 | } 916 | ``` 917 | 918 | **Good:** 919 | 920 | ```ts 921 | abstract class Airplane { 922 | protected getMaxAltitude(): number { 923 | // shared logic with subclasses ... 924 | } 925 | 926 | // ... 927 | } 928 | 929 | class Boeing777 extends Airplane { 930 | // ... 931 | getCruisingAltitude() { 932 | return this.getMaxAltitude() - this.getPassengerCount(); 933 | } 934 | } 935 | 936 | class AirForceOne extends Airplane { 937 | // ... 938 | getCruisingAltitude() { 939 | return this.getMaxAltitude(); 940 | } 941 | } 942 | 943 | class Cessna extends Airplane { 944 | // ... 945 | getCruisingAltitude() { 946 | return this.getMaxAltitude() - this.getFuelExpenditure(); 947 | } 948 | } 949 | ``` 950 | 951 | **[⬆ back to top](#table-of-contents)** 952 | 953 | ### Avoid type checking 954 | 955 | TypeScript is a strict syntactical superset of JavaScript and adds optional static type checking to the language. 956 | Always prefer to specify types of variables, parameters and return values to leverage the full power of TypeScript features. 957 | It makes refactoring more easier. 958 | 959 | **Bad:** 960 | 961 | ```ts 962 | function travelToTexas(vehicle: Bicycle | Car) { 963 | if (vehicle instanceof Bicycle) { 964 | vehicle.pedal(currentLocation, new Location('texas')); 965 | } else if (vehicle instanceof Car) { 966 | vehicle.drive(currentLocation, new Location('texas')); 967 | } 968 | } 969 | ``` 970 | 971 | **Good:** 972 | 973 | ```ts 974 | type Vehicle = Bicycle | Car; 975 | 976 | function travelToTexas(vehicle: Vehicle) { 977 | vehicle.move(currentLocation, new Location('texas')); 978 | } 979 | ``` 980 | 981 | **[⬆ back to top](#table-of-contents)** 982 | 983 | ### Don't over-optimize 984 | 985 | Modern browsers do a lot of optimization under-the-hood at runtime. A lot of times, if you are optimizing then you are just wasting your time. There are good [resources](https://github.com/petkaantonov/bluebird/wiki/Optimization-killers) for seeing where optimization is lacking. Target those in the meantime, until they are fixed if they can be. 986 | 987 | **Bad:** 988 | 989 | ```ts 990 | // On old browsers, each iteration with uncached `list.length` would be costly 991 | // because of `list.length` recomputation. In modern browsers, this is optimized. 992 | for (let i = 0, len = list.length; i < len; i++) { 993 | // ... 994 | } 995 | ``` 996 | 997 | **Good:** 998 | 999 | ```ts 1000 | for (let i = 0; i < list.length; i++) { 1001 | // ... 1002 | } 1003 | ``` 1004 | 1005 | **[⬆ back to top](#table-of-contents)** 1006 | 1007 | ### Remove dead code 1008 | 1009 | Dead code is just as bad as duplicate code. There's no reason to keep it in your codebase. 1010 | If it's not being called, get rid of it! It will still be safe in your version history if you still need it. 1011 | 1012 | **Bad:** 1013 | 1014 | ```ts 1015 | function oldRequestModule(url: string) { 1016 | // ... 1017 | } 1018 | 1019 | function requestModule(url: string) { 1020 | // ... 1021 | } 1022 | 1023 | const req = requestModule; 1024 | inventoryTracker('apples', req, 'www.inventory-awesome.io'); 1025 | ``` 1026 | 1027 | **Good:** 1028 | 1029 | ```ts 1030 | function requestModule(url: string) { 1031 | // ... 1032 | } 1033 | 1034 | const req = requestModule; 1035 | inventoryTracker('apples', req, 'www.inventory-awesome.io'); 1036 | ``` 1037 | 1038 | **[⬆ back to top](#table-of-contents)** 1039 | 1040 | ### Use iterators and generators 1041 | 1042 | Use generators and iterables when working with collections of data used like a stream. 1043 | There are some good reasons: 1044 | 1045 | - decouples the callee from the generator implementation in a sense that callee decides how many 1046 | items to access 1047 | - lazy execution, items are streamed on demand 1048 | - built-in support for iterating items using the `for-of` syntax 1049 | - iterables allow to implement optimized iterator patterns 1050 | 1051 | **Bad:** 1052 | 1053 | ```ts 1054 | function fibonacci(n: number): number[] { 1055 | if (n === 1) return [0]; 1056 | if (n === 2) return [0, 1]; 1057 | 1058 | const items: number[] = [0, 1]; 1059 | while (items.length < n) { 1060 | items.push(items[items.length - 2] + items[items.length - 1]); 1061 | } 1062 | 1063 | return items; 1064 | } 1065 | 1066 | function print(n: number) { 1067 | fibonacci(n).forEach(fib => console.log(fib)); 1068 | } 1069 | 1070 | // Print first 10 Fibonacci numbers. 1071 | print(10); 1072 | ``` 1073 | 1074 | **Good:** 1075 | 1076 | ```ts 1077 | // Generates an infinite stream of Fibonacci numbers. 1078 | // The generator doesn't keep the array of all numbers. 1079 | function* fibonacci(): IterableIterator { 1080 | let [a, b] = [0, 1]; 1081 | 1082 | while (true) { 1083 | yield a; 1084 | [a, b] = [b, a + b]; 1085 | } 1086 | } 1087 | 1088 | function print(n: number) { 1089 | let i = 0; 1090 | for (const fib of fibonacci()) { 1091 | if (i++ === n) break; 1092 | console.log(fib); 1093 | } 1094 | } 1095 | 1096 | // Print first 10 Fibonacci numbers. 1097 | print(10); 1098 | ``` 1099 | 1100 | There are libraries that allow working with iterables in a similar way as with native arrays, by 1101 | chaining methods like `map`, `slice`, `forEach` etc. See [itiriri](https://www.npmjs.com/package/itiriri) for 1102 | an example of advanced manipulation with iterables (or [itiriri-async](https://www.npmjs.com/package/itiriri-async) for manipulation of async iterables). 1103 | 1104 | ```ts 1105 | import itiriri from 'itiriri'; 1106 | 1107 | function* fibonacci(): IterableIterator { 1108 | let [a, b] = [0, 1]; 1109 | 1110 | while (true) { 1111 | yield a; 1112 | [a, b] = [b, a + b]; 1113 | } 1114 | } 1115 | 1116 | itiriri(fibonacci()) 1117 | .take(10) 1118 | .forEach(fib => console.log(fib)); 1119 | ``` 1120 | 1121 | **[⬆ back to top](#table-of-contents)** 1122 | 1123 | ## Objects and Data Structures 1124 | 1125 | ### Use getters and setters 1126 | 1127 | TypeScript supports getter/setter syntax. 1128 | Using getters and setters to access data from objects that encapsulate behavior could be better than simply looking for a property on an object. 1129 | "Why?" you might ask. Well, here's a list of reasons: 1130 | 1131 | - When you want to do more beyond getting an object property, you don't have to look up and change every accessor in your codebase. 1132 | - Makes adding validation simple when doing a `set`. 1133 | - Encapsulates the internal representation. 1134 | - Easy to add logging and error handling when getting and setting. 1135 | - You can lazy load your object's properties, let's say getting it from a server. 1136 | 1137 | **Bad:** 1138 | 1139 | ```ts 1140 | type BankAccount = { 1141 | balance: number; 1142 | // ... 1143 | } 1144 | 1145 | const value = 100; 1146 | const account: BankAccount = { 1147 | balance: 0, 1148 | // ... 1149 | }; 1150 | 1151 | if (value < 0) { 1152 | throw new Error('Cannot set negative balance.'); 1153 | } 1154 | 1155 | account.balance = value; 1156 | ``` 1157 | 1158 | **Good:** 1159 | 1160 | ```ts 1161 | class BankAccount { 1162 | private accountBalance: number = 0; 1163 | 1164 | get balance(): number { 1165 | return this.accountBalance; 1166 | } 1167 | 1168 | set balance(value: number) { 1169 | if (value < 0) { 1170 | throw new Error('Cannot set negative balance.'); 1171 | } 1172 | 1173 | this.accountBalance = value; 1174 | } 1175 | 1176 | // ... 1177 | } 1178 | 1179 | // Now `BankAccount` encapsulates the validation logic. 1180 | // If one day the specifications change, and we need extra validation rule, 1181 | // we would have to alter only the `setter` implementation, 1182 | // leaving all dependent code unchanged. 1183 | const account = new BankAccount(); 1184 | account.balance = 100; 1185 | ``` 1186 | 1187 | **[⬆ back to top](#table-of-contents)** 1188 | 1189 | ### Make objects have private/protected members 1190 | 1191 | TypeScript supports `public` *(default)*, `protected` and `private` accessors on class members. 1192 | 1193 | **Bad:** 1194 | 1195 | ```ts 1196 | class Circle { 1197 | radius: number; 1198 | 1199 | constructor(radius: number) { 1200 | this.radius = radius; 1201 | } 1202 | 1203 | perimeter() { 1204 | return 2 * Math.PI * this.radius; 1205 | } 1206 | 1207 | surface() { 1208 | return Math.PI * this.radius * this.radius; 1209 | } 1210 | } 1211 | ``` 1212 | 1213 | **Good:** 1214 | 1215 | ```ts 1216 | class Circle { 1217 | constructor(private readonly radius: number) { 1218 | } 1219 | 1220 | perimeter() { 1221 | return 2 * Math.PI * this.radius; 1222 | } 1223 | 1224 | surface() { 1225 | return Math.PI * this.radius * this.radius; 1226 | } 1227 | } 1228 | ``` 1229 | 1230 | **[⬆ back to top](#table-of-contents)** 1231 | 1232 | ### Prefer immutability 1233 | 1234 | TypeScript's type system allows you to mark individual properties on an interface / class as *readonly*. This allows you to work in a functional way (unexpected mutation is bad). 1235 | For more advanced scenarios there is a built-in type `Readonly` that takes a type `T` and marks all of its properties as readonly using mapped types (see [mapped types](https://www.typescriptlang.org/docs/handbook/advanced-types.html#mapped-types)). 1236 | 1237 | **Bad:** 1238 | 1239 | ```ts 1240 | interface Config { 1241 | host: string; 1242 | port: string; 1243 | db: string; 1244 | } 1245 | ``` 1246 | 1247 | **Good:** 1248 | 1249 | ```ts 1250 | interface Config { 1251 | readonly host: string; 1252 | readonly port: string; 1253 | readonly db: string; 1254 | } 1255 | ``` 1256 | 1257 | Case of Array, you can create a read-only array by using `ReadonlyArray`. 1258 | do not allow changes such as `push()` and `fill()`, but can use features such as `concat()` and `slice()` that do not change the value. 1259 | 1260 | **Bad:** 1261 | 1262 | ```ts 1263 | const array: number[] = [ 1, 3, 5 ]; 1264 | array = []; // error 1265 | array.push(100); // array will updated 1266 | ``` 1267 | 1268 | **Good:** 1269 | 1270 | ```ts 1271 | const array: ReadonlyArray = [ 1, 3, 5 ]; 1272 | array = []; // error 1273 | array.push(100); // error 1274 | ``` 1275 | 1276 | Declaring read-only arguments in [TypeScript 3.4 is a bit easier](https://github.com/microsoft/TypeScript/wiki/What's-new-in-TypeScript#improvements-for-readonlyarray-and-readonly-tuples). 1277 | 1278 | ```ts 1279 | function hoge(args: readonly string[]) { 1280 | args.push(1); // error 1281 | } 1282 | ``` 1283 | 1284 | Prefer [const assertions](https://github.com/microsoft/TypeScript/wiki/What's-new-in-TypeScript#const-assertions) for literal values. 1285 | 1286 | **Bad:** 1287 | 1288 | ```ts 1289 | const config = { 1290 | hello: 'world' 1291 | }; 1292 | config.hello = 'world'; // value is changed 1293 | 1294 | const array = [ 1, 3, 5 ]; 1295 | array[0] = 10; // value is changed 1296 | 1297 | // writable objects is returned 1298 | function readonlyData(value: number) { 1299 | return { value }; 1300 | } 1301 | 1302 | const result = readonlyData(100); 1303 | result.value = 200; // value is changed 1304 | ``` 1305 | 1306 | **Good:** 1307 | 1308 | ```ts 1309 | // read-only object 1310 | const config = { 1311 | hello: 'world' 1312 | } as const; 1313 | config.hello = 'world'; // error 1314 | 1315 | // read-only array 1316 | const array = [ 1, 3, 5 ] as const; 1317 | array[0] = 10; // error 1318 | 1319 | // You can return read-only objects 1320 | function readonlyData(value: number) { 1321 | return { value } as const; 1322 | } 1323 | 1324 | const result = readonlyData(100); 1325 | result.value = 200; // error 1326 | ``` 1327 | 1328 | **[⬆ back to top](#table-of-contents)** 1329 | 1330 | ### type vs. interface 1331 | 1332 | Use type when you might need a union or intersection. Use interface when you want `extends` or `implements`. There is no strict rule however, use the one that works for you. 1333 | For a more detailed explanation refer to this [answer](https://stackoverflow.com/questions/37233735/typescript-interfaces-vs-types/54101543#54101543) about the differences between `type` and `interface` in TypeScript. 1334 | 1335 | **Bad:** 1336 | 1337 | ```ts 1338 | interface EmailConfig { 1339 | // ... 1340 | } 1341 | 1342 | interface DbConfig { 1343 | // ... 1344 | } 1345 | 1346 | interface Config { 1347 | // ... 1348 | } 1349 | 1350 | //... 1351 | 1352 | type Shape = { 1353 | // ... 1354 | } 1355 | ``` 1356 | 1357 | **Good:** 1358 | 1359 | ```ts 1360 | 1361 | type EmailConfig = { 1362 | // ... 1363 | } 1364 | 1365 | type DbConfig = { 1366 | // ... 1367 | } 1368 | 1369 | type Config = EmailConfig | DbConfig; 1370 | 1371 | // ... 1372 | 1373 | interface Shape { 1374 | // ... 1375 | } 1376 | 1377 | class Circle implements Shape { 1378 | // ... 1379 | } 1380 | 1381 | class Square implements Shape { 1382 | // ... 1383 | } 1384 | ``` 1385 | 1386 | **[⬆ back to top](#table-of-contents)** 1387 | 1388 | ## Classes 1389 | 1390 | ### Classes should be small 1391 | 1392 | The class' size is measured by its responsibility. Following the *Single Responsibility principle* a class should be small. 1393 | 1394 | **Bad:** 1395 | 1396 | ```ts 1397 | class Dashboard { 1398 | getLanguage(): string { /* ... */ } 1399 | setLanguage(language: string): void { /* ... */ } 1400 | showProgress(): void { /* ... */ } 1401 | hideProgress(): void { /* ... */ } 1402 | isDirty(): boolean { /* ... */ } 1403 | disable(): void { /* ... */ } 1404 | enable(): void { /* ... */ } 1405 | addSubscription(subscription: Subscription): void { /* ... */ } 1406 | removeSubscription(subscription: Subscription): void { /* ... */ } 1407 | addUser(user: User): void { /* ... */ } 1408 | removeUser(user: User): void { /* ... */ } 1409 | goToHomePage(): void { /* ... */ } 1410 | updateProfile(details: UserDetails): void { /* ... */ } 1411 | getVersion(): string { /* ... */ } 1412 | // ... 1413 | } 1414 | 1415 | ``` 1416 | 1417 | **Good:** 1418 | 1419 | ```ts 1420 | class Dashboard { 1421 | disable(): void { /* ... */ } 1422 | enable(): void { /* ... */ } 1423 | getVersion(): string { /* ... */ } 1424 | } 1425 | 1426 | // split the responsibilities by moving the remaining methods to other classes 1427 | // ... 1428 | ``` 1429 | 1430 | **[⬆ back to top](#table-of-contents)** 1431 | 1432 | ### High cohesion and low coupling 1433 | 1434 | Cohesion defines the degree to which class members are related to each other. Ideally, all fields within a class should be used by each method. 1435 | We then say that the class is *maximally cohesive*. In practice, this however is not always possible, nor even advisable. You should however prefer cohesion to be high. 1436 | 1437 | Coupling refers to how related or dependent are two classes toward each other. Classes are said to be low coupled if changes in one of them doesn't affect the other one. 1438 | 1439 | Good software design has **high cohesion** and **low coupling**. 1440 | 1441 | **Bad:** 1442 | 1443 | ```ts 1444 | class UserManager { 1445 | // Bad: each private variable is used by one or another group of methods. 1446 | // It makes clear evidence that the class is holding more than a single responsibility. 1447 | // If I need only to create the service to get the transactions for a user, 1448 | // I'm still forced to pass and instance of `emailSender`. 1449 | constructor( 1450 | private readonly db: Database, 1451 | private readonly emailSender: EmailSender) { 1452 | } 1453 | 1454 | async getUser(id: number): Promise { 1455 | return await db.users.findOne({ id }); 1456 | } 1457 | 1458 | async getTransactions(userId: number): Promise { 1459 | return await db.transactions.find({ userId }); 1460 | } 1461 | 1462 | async sendGreeting(): Promise { 1463 | await emailSender.send('Welcome!'); 1464 | } 1465 | 1466 | async sendNotification(text: string): Promise { 1467 | await emailSender.send(text); 1468 | } 1469 | 1470 | async sendNewsletter(): Promise { 1471 | // ... 1472 | } 1473 | } 1474 | ``` 1475 | 1476 | **Good:** 1477 | 1478 | ```ts 1479 | class UserService { 1480 | constructor(private readonly db: Database) { 1481 | } 1482 | 1483 | async getUser(id: number): Promise { 1484 | return await this.db.users.findOne({ id }); 1485 | } 1486 | 1487 | async getTransactions(userId: number): Promise { 1488 | return await this.db.transactions.find({ userId }); 1489 | } 1490 | } 1491 | 1492 | class UserNotifier { 1493 | constructor(private readonly emailSender: EmailSender) { 1494 | } 1495 | 1496 | async sendGreeting(): Promise { 1497 | await this.emailSender.send('Welcome!'); 1498 | } 1499 | 1500 | async sendNotification(text: string): Promise { 1501 | await this.emailSender.send(text); 1502 | } 1503 | 1504 | async sendNewsletter(): Promise { 1505 | // ... 1506 | } 1507 | } 1508 | ``` 1509 | 1510 | **[⬆ back to top](#table-of-contents)** 1511 | 1512 | ### Prefer composition over inheritance 1513 | 1514 | As stated famously in [Design Patterns](https://en.wikipedia.org/wiki/Design_Patterns) by the Gang of Four, you should *prefer composition over inheritance* where you can. There are lots of good reasons to use inheritance and lots of good reasons to use composition. The main point for this maxim is that if your mind instinctively goes for inheritance, try to think if composition could model your problem better. In some cases it can. 1515 | 1516 | You might be wondering then, "when should I use inheritance?" It depends on your problem at hand, but this is a decent list of when inheritance makes more sense than composition: 1517 | 1518 | 1. Your inheritance represents an "is-a" relationship and not a "has-a" relationship (Human->Animal vs. User->UserDetails). 1519 | 1520 | 2. You can reuse code from the base classes (Humans can move like all animals). 1521 | 1522 | 3. You want to make global changes to derived classes by changing a base class. (Change the caloric expenditure of all animals when they move). 1523 | 1524 | **Bad:** 1525 | 1526 | ```ts 1527 | class Employee { 1528 | constructor( 1529 | private readonly name: string, 1530 | private readonly email: string) { 1531 | } 1532 | 1533 | // ... 1534 | } 1535 | 1536 | // Bad because Employees "have" tax data. EmployeeTaxData is not a type of Employee 1537 | class EmployeeTaxData extends Employee { 1538 | constructor( 1539 | name: string, 1540 | email: string, 1541 | private readonly ssn: string, 1542 | private readonly salary: number) { 1543 | super(name, email); 1544 | } 1545 | 1546 | // ... 1547 | } 1548 | ``` 1549 | 1550 | **Good:** 1551 | 1552 | ```ts 1553 | class Employee { 1554 | private taxData: EmployeeTaxData; 1555 | 1556 | constructor( 1557 | private readonly name: string, 1558 | private readonly email: string) { 1559 | } 1560 | 1561 | setTaxData(ssn: string, salary: number): Employee { 1562 | this.taxData = new EmployeeTaxData(ssn, salary); 1563 | return this; 1564 | } 1565 | 1566 | // ... 1567 | } 1568 | 1569 | class EmployeeTaxData { 1570 | constructor( 1571 | public readonly ssn: string, 1572 | public readonly salary: number) { 1573 | } 1574 | 1575 | // ... 1576 | } 1577 | ``` 1578 | 1579 | **[⬆ back to top](#table-of-contents)** 1580 | 1581 | ### Use method chaining 1582 | 1583 | This pattern is very useful and commonly used in many libraries. It allows your code to be expressive, and less verbose. For that reason, use method chaining and take a look at how clean your code will be. 1584 | 1585 | **Bad:** 1586 | 1587 | ```ts 1588 | class QueryBuilder { 1589 | private collection: string; 1590 | private pageNumber: number = 1; 1591 | private itemsPerPage: number = 100; 1592 | private orderByFields: string[] = []; 1593 | 1594 | from(collection: string): void { 1595 | this.collection = collection; 1596 | } 1597 | 1598 | page(number: number, itemsPerPage: number = 100): void { 1599 | this.pageNumber = number; 1600 | this.itemsPerPage = itemsPerPage; 1601 | } 1602 | 1603 | orderBy(...fields: string[]): void { 1604 | this.orderByFields = fields; 1605 | } 1606 | 1607 | build(): Query { 1608 | // ... 1609 | } 1610 | } 1611 | 1612 | // ... 1613 | 1614 | const queryBuilder = new QueryBuilder(); 1615 | queryBuilder.from('users'); 1616 | queryBuilder.page(1, 100); 1617 | queryBuilder.orderBy('firstName', 'lastName'); 1618 | 1619 | const query = queryBuilder.build(); 1620 | ``` 1621 | 1622 | **Good:** 1623 | 1624 | ```ts 1625 | class QueryBuilder { 1626 | private collection: string; 1627 | private pageNumber: number = 1; 1628 | private itemsPerPage: number = 100; 1629 | private orderByFields: string[] = []; 1630 | 1631 | from(collection: string): this { 1632 | this.collection = collection; 1633 | return this; 1634 | } 1635 | 1636 | page(number: number, itemsPerPage: number = 100): this { 1637 | this.pageNumber = number; 1638 | this.itemsPerPage = itemsPerPage; 1639 | return this; 1640 | } 1641 | 1642 | orderBy(...fields: string[]): this { 1643 | this.orderByFields = fields; 1644 | return this; 1645 | } 1646 | 1647 | build(): Query { 1648 | // ... 1649 | } 1650 | } 1651 | 1652 | // ... 1653 | 1654 | const query = new QueryBuilder() 1655 | .from('users') 1656 | .page(1, 100) 1657 | .orderBy('firstName', 'lastName') 1658 | .build(); 1659 | ``` 1660 | 1661 | **[⬆ back to top](#table-of-contents)** 1662 | 1663 | ## SOLID 1664 | 1665 | ### Single Responsibility Principle (SRP) 1666 | 1667 | As stated in Clean Code, "There should never be more than one reason for a class to change". It's tempting to jam-pack a class with a lot of functionality, like when you can only take one suitcase on your flight. The issue with this is that your class won't be conceptually cohesive and it will give it many reasons to change. Minimizing the amount of times you need to change a class is important. It's important because if too much functionality is in one class and you modify a piece of it, it can be difficult to understand how that will affect other dependent modules in your codebase. 1668 | 1669 | **Bad:** 1670 | 1671 | ```ts 1672 | class UserSettings { 1673 | constructor(private readonly user: User) { 1674 | } 1675 | 1676 | changeSettings(settings: UserSettings) { 1677 | if (this.verifyCredentials()) { 1678 | // ... 1679 | } 1680 | } 1681 | 1682 | verifyCredentials() { 1683 | // ... 1684 | } 1685 | } 1686 | ``` 1687 | 1688 | **Good:** 1689 | 1690 | ```ts 1691 | class UserAuth { 1692 | constructor(private readonly user: User) { 1693 | } 1694 | 1695 | verifyCredentials() { 1696 | // ... 1697 | } 1698 | } 1699 | 1700 | 1701 | class UserSettings { 1702 | private readonly auth: UserAuth; 1703 | 1704 | constructor(private readonly user: User) { 1705 | this.auth = new UserAuth(user); 1706 | } 1707 | 1708 | changeSettings(settings: UserSettings) { 1709 | if (this.auth.verifyCredentials()) { 1710 | // ... 1711 | } 1712 | } 1713 | } 1714 | ``` 1715 | 1716 | **[⬆ back to top](#table-of-contents)** 1717 | 1718 | ### Open/Closed Principle (OCP) 1719 | 1720 | As stated by Bertrand Meyer, "software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification." What does that mean though? This principle basically states that you should allow users to add new functionalities without changing existing code. 1721 | 1722 | **Bad:** 1723 | 1724 | ```ts 1725 | class AjaxAdapter extends Adapter { 1726 | constructor() { 1727 | super(); 1728 | } 1729 | 1730 | // ... 1731 | } 1732 | 1733 | class NodeAdapter extends Adapter { 1734 | constructor() { 1735 | super(); 1736 | } 1737 | 1738 | // ... 1739 | } 1740 | 1741 | class HttpRequester { 1742 | constructor(private readonly adapter: Adapter) { 1743 | } 1744 | 1745 | async fetch(url: string): Promise { 1746 | if (this.adapter instanceof AjaxAdapter) { 1747 | const response = await makeAjaxCall(url); 1748 | // transform response and return 1749 | } else if (this.adapter instanceof NodeAdapter) { 1750 | const response = await makeHttpCall(url); 1751 | // transform response and return 1752 | } 1753 | } 1754 | } 1755 | 1756 | function makeAjaxCall(url: string): Promise { 1757 | // request and return promise 1758 | } 1759 | 1760 | function makeHttpCall(url: string): Promise { 1761 | // request and return promise 1762 | } 1763 | ``` 1764 | 1765 | **Good:** 1766 | 1767 | ```ts 1768 | abstract class Adapter { 1769 | abstract async request(url: string): Promise; 1770 | 1771 | // code shared to subclasses ... 1772 | } 1773 | 1774 | class AjaxAdapter extends Adapter { 1775 | constructor() { 1776 | super(); 1777 | } 1778 | 1779 | async request(url: string): Promise{ 1780 | // request and return promise 1781 | } 1782 | 1783 | // ... 1784 | } 1785 | 1786 | class NodeAdapter extends Adapter { 1787 | constructor() { 1788 | super(); 1789 | } 1790 | 1791 | async request(url: string): Promise{ 1792 | // request and return promise 1793 | } 1794 | 1795 | // ... 1796 | } 1797 | 1798 | class HttpRequester { 1799 | constructor(private readonly adapter: Adapter) { 1800 | } 1801 | 1802 | async fetch(url: string): Promise { 1803 | const response = await this.adapter.request(url); 1804 | // transform response and return 1805 | } 1806 | } 1807 | ``` 1808 | 1809 | **[⬆ back to top](#table-of-contents)** 1810 | 1811 | ### Liskov Substitution Principle (LSP) 1812 | 1813 | This is a scary term for a very simple concept. It's formally defined as "If S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e., objects of type S may substitute objects of type T) without altering any of the desirable properties of that program (correctness, task performed, etc.)." That's an even scarier definition. 1814 | 1815 | The best explanation for this is if you have a parent class and a child class, then the parent class and child class can be used interchangeably without getting incorrect results. This might still be confusing, so let's take a look at the classic Square-Rectangle example. Mathematically, a square is a rectangle, but if you model it using the "is-a" relationship via inheritance, you quickly get into trouble. 1816 | 1817 | **Bad:** 1818 | 1819 | ```ts 1820 | class Rectangle { 1821 | constructor( 1822 | protected width: number = 0, 1823 | protected height: number = 0) { 1824 | 1825 | } 1826 | 1827 | setColor(color: string): this { 1828 | // ... 1829 | } 1830 | 1831 | render(area: number) { 1832 | // ... 1833 | } 1834 | 1835 | setWidth(width: number): this { 1836 | this.width = width; 1837 | return this; 1838 | } 1839 | 1840 | setHeight(height: number): this { 1841 | this.height = height; 1842 | return this; 1843 | } 1844 | 1845 | getArea(): number { 1846 | return this.width * this.height; 1847 | } 1848 | } 1849 | 1850 | class Square extends Rectangle { 1851 | setWidth(width: number): this { 1852 | this.width = width; 1853 | this.height = width; 1854 | return this; 1855 | } 1856 | 1857 | setHeight(height: number): this { 1858 | this.width = height; 1859 | this.height = height; 1860 | return this; 1861 | } 1862 | } 1863 | 1864 | function renderLargeRectangles(rectangles: Rectangle[]) { 1865 | rectangles.forEach((rectangle) => { 1866 | const area = rectangle 1867 | .setWidth(4) 1868 | .setHeight(5) 1869 | .getArea(); // BAD: Returns 25 for Square. Should be 20. 1870 | rectangle.render(area); 1871 | }); 1872 | } 1873 | 1874 | const rectangles = [new Rectangle(), new Rectangle(), new Square()]; 1875 | renderLargeRectangles(rectangles); 1876 | ``` 1877 | 1878 | **Good:** 1879 | 1880 | ```ts 1881 | abstract class Shape { 1882 | setColor(color: string): this { 1883 | // ... 1884 | } 1885 | 1886 | render(area: number) { 1887 | // ... 1888 | } 1889 | 1890 | abstract getArea(): number; 1891 | } 1892 | 1893 | class Rectangle extends Shape { 1894 | constructor( 1895 | private readonly width = 0, 1896 | private readonly height = 0) { 1897 | super(); 1898 | } 1899 | 1900 | getArea(): number { 1901 | return this.width * this.height; 1902 | } 1903 | } 1904 | 1905 | class Square extends Shape { 1906 | constructor(private readonly length: number) { 1907 | super(); 1908 | } 1909 | 1910 | getArea(): number { 1911 | return this.length * this.length; 1912 | } 1913 | } 1914 | 1915 | function renderLargeShapes(shapes: Shape[]) { 1916 | shapes.forEach((shape) => { 1917 | const area = shape.getArea(); 1918 | shape.render(area); 1919 | }); 1920 | } 1921 | 1922 | const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)]; 1923 | renderLargeShapes(shapes); 1924 | ``` 1925 | 1926 | **[⬆ back to top](#table-of-contents)** 1927 | 1928 | ### Interface Segregation Principle (ISP) 1929 | 1930 | ISP states that "Clients should not be forced to depend upon interfaces that they do not use.". This principle is very much related to the Single Responsibility Principle. 1931 | What it really means is that you should always design your abstractions in a way that the clients that are using the exposed methods do not get the whole pie instead. That also include imposing the clients with the burden of implementing methods that they don’t actually need. 1932 | 1933 | **Bad:** 1934 | 1935 | ```ts 1936 | interface SmartPrinter { 1937 | print(); 1938 | fax(); 1939 | scan(); 1940 | } 1941 | 1942 | class AllInOnePrinter implements SmartPrinter { 1943 | print() { 1944 | // ... 1945 | } 1946 | 1947 | fax() { 1948 | // ... 1949 | } 1950 | 1951 | scan() { 1952 | // ... 1953 | } 1954 | } 1955 | 1956 | class EconomicPrinter implements SmartPrinter { 1957 | print() { 1958 | // ... 1959 | } 1960 | 1961 | fax() { 1962 | throw new Error('Fax not supported.'); 1963 | } 1964 | 1965 | scan() { 1966 | throw new Error('Scan not supported.'); 1967 | } 1968 | } 1969 | ``` 1970 | 1971 | **Good:** 1972 | 1973 | ```ts 1974 | interface Printer { 1975 | print(); 1976 | } 1977 | 1978 | interface Fax { 1979 | fax(); 1980 | } 1981 | 1982 | interface Scanner { 1983 | scan(); 1984 | } 1985 | 1986 | class AllInOnePrinter implements Printer, Fax, Scanner { 1987 | print() { 1988 | // ... 1989 | } 1990 | 1991 | fax() { 1992 | // ... 1993 | } 1994 | 1995 | scan() { 1996 | // ... 1997 | } 1998 | } 1999 | 2000 | class EconomicPrinter implements Printer { 2001 | print() { 2002 | // ... 2003 | } 2004 | } 2005 | ``` 2006 | 2007 | **[⬆ back to top](#table-of-contents)** 2008 | 2009 | ### Dependency Inversion Principle (DIP) 2010 | 2011 | This principle states two essential things: 2012 | 2013 | 1. High-level modules should not depend on low-level modules. Both should depend on abstractions. 2014 | 2015 | 2. Abstractions should not depend upon details. Details should depend on abstractions. 2016 | 2017 | This can be hard to understand at first, but if you've worked with Angular, you've seen an implementation of this principle in the form of Dependency Injection (DI). While they are not identical concepts, DIP keeps high-level modules from knowing the details of its low-level modules and setting them up. It can accomplish this through DI. A huge benefit of this is that it reduces the coupling between modules. Coupling is a very bad development pattern because it makes your code hard to refactor. 2018 | 2019 | DIP is usually achieved by a using an inversion of control (IoC) container. An example of a powerful IoC container for TypeScript is [InversifyJs](https://www.npmjs.com/package/inversify) 2020 | 2021 | **Bad:** 2022 | 2023 | ```ts 2024 | import { readFile as readFileCb } from 'fs'; 2025 | import { promisify } from 'util'; 2026 | 2027 | const readFile = promisify(readFileCb); 2028 | 2029 | type ReportData = { 2030 | // .. 2031 | } 2032 | 2033 | class XmlFormatter { 2034 | parse(content: string): T { 2035 | // Converts an XML string to an object T 2036 | } 2037 | } 2038 | 2039 | class ReportReader { 2040 | 2041 | // BAD: We have created a dependency on a specific request implementation. 2042 | // We should just have ReportReader depend on a parse method: `parse` 2043 | private readonly formatter = new XmlFormatter(); 2044 | 2045 | async read(path: string): Promise { 2046 | const text = await readFile(path, 'UTF8'); 2047 | return this.formatter.parse(text); 2048 | } 2049 | } 2050 | 2051 | // ... 2052 | const reader = new ReportReader(); 2053 | await report = await reader.read('report.xml'); 2054 | ``` 2055 | 2056 | **Good:** 2057 | 2058 | ```ts 2059 | import { readFile as readFileCb } from 'fs'; 2060 | import { promisify } from 'util'; 2061 | 2062 | const readFile = promisify(readFileCb); 2063 | 2064 | type ReportData = { 2065 | // .. 2066 | } 2067 | 2068 | interface Formatter { 2069 | parse(content: string): T; 2070 | } 2071 | 2072 | class XmlFormatter implements Formatter { 2073 | parse(content: string): T { 2074 | // Converts an XML string to an object T 2075 | } 2076 | } 2077 | 2078 | 2079 | class JsonFormatter implements Formatter { 2080 | parse(content: string): T { 2081 | // Converts a JSON string to an object T 2082 | } 2083 | } 2084 | 2085 | class ReportReader { 2086 | constructor(private readonly formatter: Formatter) { 2087 | } 2088 | 2089 | async read(path: string): Promise { 2090 | const text = await readFile(path, 'UTF8'); 2091 | return this.formatter.parse(text); 2092 | } 2093 | } 2094 | 2095 | // ... 2096 | const reader = new ReportReader(new XmlFormatter()); 2097 | await report = await reader.read('report.xml'); 2098 | 2099 | // or if we had to read a json report 2100 | const reader = new ReportReader(new JsonFormatter()); 2101 | await report = await reader.read('report.json'); 2102 | ``` 2103 | 2104 | **[⬆ back to top](#table-of-contents)** 2105 | 2106 | ## Testing 2107 | 2108 | Testing is more important than shipping. If you have no tests or an inadequate amount, then every time you ship code you won't be sure that you didn't break anything. 2109 | Deciding on what constitutes an adequate amount is up to your team, but having 100% coverage (all statements and branches) 2110 | is how you achieve very high confidence and developer peace of mind. This means that in addition to having a great testing framework, you also need to use a good [coverage tool](https://github.com/gotwarlost/istanbul). 2111 | 2112 | There's no excuse to not write tests. There are [plenty of good JS test frameworks](http://jstherightway.org/#testing-tools) with typings support for TypeScript, so find one that your team prefers. When you find one that works for your team, then aim to always write tests for every new feature/module you introduce. If your preferred method is Test Driven Development (TDD), that is great, but the main point is to just make sure you are reaching your coverage goals before launching any feature, or refactoring an existing one. 2113 | 2114 | ### The three laws of TDD 2115 | 2116 | 1. You are not allowed to write any production code unless it is to make a failing unit test pass. 2117 | 2118 | 2. You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures. 2119 | 2120 | 3. You are not allowed to write any more production code than is sufficient to pass the one failing unit test. 2121 | 2122 | **[⬆ back to top](#table-of-contents)** 2123 | 2124 | ### F.I.R.S.T. rules 2125 | 2126 | Clean tests should follow the rules: 2127 | 2128 | - **Fast** tests should be fast because we want to run them frequently. 2129 | 2130 | - **Independent** tests should not depend on each other. They should provide same output whether run independently or all together in any order. 2131 | 2132 | - **Repeatable** tests should be repeatable in any environment and there should be no excuse for why they fail. 2133 | 2134 | - **Self-Validating** a test should answer with either *Passed* or *Failed*. You don't need to compare log files to answer if a test passed. 2135 | 2136 | - **Timely** unit tests should be written before the production code. If you write tests after the production code, you might find writing tests too hard. 2137 | 2138 | **[⬆ back to top](#table-of-contents)** 2139 | 2140 | ### Single concept per test 2141 | 2142 | Tests should also follow the *Single Responsibility Principle*. Make only one assert per unit test. 2143 | 2144 | **Bad:** 2145 | 2146 | ```ts 2147 | import { assert } from 'chai'; 2148 | 2149 | describe('AwesomeDate', () => { 2150 | it('handles date boundaries', () => { 2151 | let date: AwesomeDate; 2152 | 2153 | date = new AwesomeDate('1/1/2015'); 2154 | assert.equal('1/31/2015', date.addDays(30)); 2155 | 2156 | date = new AwesomeDate('2/1/2016'); 2157 | assert.equal('2/29/2016', date.addDays(28)); 2158 | 2159 | date = new AwesomeDate('2/1/2015'); 2160 | assert.equal('3/1/2015', date.addDays(28)); 2161 | }); 2162 | }); 2163 | ``` 2164 | 2165 | **Good:** 2166 | 2167 | ```ts 2168 | import { assert } from 'chai'; 2169 | 2170 | describe('AwesomeDate', () => { 2171 | it('handles 30-day months', () => { 2172 | const date = new AwesomeDate('1/1/2015'); 2173 | assert.equal('1/31/2015', date.addDays(30)); 2174 | }); 2175 | 2176 | it('handles leap year', () => { 2177 | const date = new AwesomeDate('2/1/2016'); 2178 | assert.equal('2/29/2016', date.addDays(28)); 2179 | }); 2180 | 2181 | it('handles non-leap year', () => { 2182 | const date = new AwesomeDate('2/1/2015'); 2183 | assert.equal('3/1/2015', date.addDays(28)); 2184 | }); 2185 | }); 2186 | ``` 2187 | 2188 | **[⬆ back to top](#table-of-contents)** 2189 | 2190 | ### The name of the test should reveal its intention 2191 | 2192 | When a test fail, its name is the first indication of what may have gone wrong. 2193 | 2194 | **Bad:** 2195 | 2196 | ```ts 2197 | describe('Calendar', () => { 2198 | it('2/29/2020', () => { 2199 | // ... 2200 | }); 2201 | 2202 | it('throws', () => { 2203 | // ... 2204 | }); 2205 | }); 2206 | ``` 2207 | 2208 | **Good:** 2209 | 2210 | ```ts 2211 | describe('Calendar', () => { 2212 | it('should handle leap year', () => { 2213 | // ... 2214 | }); 2215 | 2216 | it('should throw when format is invalid', () => { 2217 | // ... 2218 | }); 2219 | }); 2220 | ``` 2221 | 2222 | **[⬆ back to top](#table-of-contents)** 2223 | 2224 | ## Concurrency 2225 | 2226 | ### Prefer promises vs callbacks 2227 | 2228 | Callbacks aren't clean, and they cause excessive amounts of nesting *(the callback hell)*. 2229 | There are utilities that transform existing functions using the callback style to a version that returns promises 2230 | (for Node.js see [`util.promisify`](https://nodejs.org/dist/latest-v8.x/docs/api/util.html#util_util_promisify_original), for general purpose see [pify](https://www.npmjs.com/package/pify), [es6-promisify](https://www.npmjs.com/package/es6-promisify)) 2231 | 2232 | **Bad:** 2233 | 2234 | ```ts 2235 | import { get } from 'request'; 2236 | import { writeFile } from 'fs'; 2237 | 2238 | function downloadPage(url: string, saveTo: string, callback: (error: Error, content?: string) => void) { 2239 | get(url, (error, response) => { 2240 | if (error) { 2241 | callback(error); 2242 | } else { 2243 | writeFile(saveTo, response.body, (error) => { 2244 | if (error) { 2245 | callback(error); 2246 | } else { 2247 | callback(null, response.body); 2248 | } 2249 | }); 2250 | } 2251 | }); 2252 | } 2253 | 2254 | downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html', (error, content) => { 2255 | if (error) { 2256 | console.error(error); 2257 | } else { 2258 | console.log(content); 2259 | } 2260 | }); 2261 | ``` 2262 | 2263 | **Good:** 2264 | 2265 | ```ts 2266 | import { get } from 'request'; 2267 | import { writeFile } from 'fs'; 2268 | import { promisify } from 'util'; 2269 | 2270 | const write = promisify(writeFile); 2271 | 2272 | function downloadPage(url: string, saveTo: string): Promise { 2273 | return get(url) 2274 | .then(response => write(saveTo, response)); 2275 | } 2276 | 2277 | downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html') 2278 | .then(content => console.log(content)) 2279 | .catch(error => console.error(error)); 2280 | ``` 2281 | 2282 | Promises supports a few helper methods that help make code more conscise: 2283 | 2284 | | Pattern | Description | 2285 | | ------------------------ | ----------------------------------------- | 2286 | | `Promise.resolve(value)` | Convert a value into a resolved promise. | 2287 | | `Promise.reject(error)` | Convert an error into a rejected promise. | 2288 | | `Promise.all(promises)` |Returns a new promise which is fulfilled with an array of fulfillment values for the passed promises or rejects with the reason of the first promise that rejects. | 2289 | | `Promise.race(promises)`|Returns a new promise which is fulfilled/rejected with the result/error of the first settled promise from the array of passed promises. | 2290 | 2291 | `Promise.all` is especially useful when there is a need to run tasks in parallel. `Promise.race` makes it easier to implement things like timeouts for promises. 2292 | 2293 | **[⬆ back to top](#table-of-contents)** 2294 | 2295 | ### Async/Await are even cleaner than Promises 2296 | 2297 | With `async`/`await` syntax you can write code that is far cleaner and more understandable than chained promises. Within a function prefixed with `async` keyword you have a way to tell the JavaScript runtime to pause the execution of code on the `await` keyword (when used on a promise). 2298 | 2299 | **Bad:** 2300 | 2301 | ```ts 2302 | import { get } from 'request'; 2303 | import { writeFile } from 'fs'; 2304 | import { promisify } from 'util'; 2305 | 2306 | const write = util.promisify(writeFile); 2307 | 2308 | function downloadPage(url: string, saveTo: string): Promise { 2309 | return get(url).then(response => write(saveTo, response)); 2310 | } 2311 | 2312 | downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html') 2313 | .then(content => console.log(content)) 2314 | .catch(error => console.error(error)); 2315 | ``` 2316 | 2317 | **Good:** 2318 | 2319 | ```ts 2320 | import { get } from 'request'; 2321 | import { writeFile } from 'fs'; 2322 | import { promisify } from 'util'; 2323 | 2324 | const write = promisify(writeFile); 2325 | 2326 | async function downloadPage(url: string, saveTo: string): Promise { 2327 | const response = await get(url); 2328 | await write(saveTo, response); 2329 | return response; 2330 | } 2331 | 2332 | // somewhere in an async function 2333 | try { 2334 | const content = await downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html'); 2335 | console.log(content); 2336 | } catch (error) { 2337 | console.error(error); 2338 | } 2339 | ``` 2340 | 2341 | **[⬆ back to top](#table-of-contents)** 2342 | 2343 | ## Error Handling 2344 | 2345 | Thrown errors are a good thing! They mean the runtime has successfully identified when something in your program has gone wrong and it's letting you know by stopping function 2346 | execution on the current stack, killing the process (in Node), and notifying you in the console with a stack trace. 2347 | 2348 | ### Always use Error for throwing or rejecting 2349 | 2350 | JavaScript as well as TypeScript allow you to `throw` any object. A Promise can also be rejected with any reason object. 2351 | It is advisable to use the `throw` syntax with an `Error` type. This is because your error might be caught in higher level code with a `catch` syntax. 2352 | It would be very confusing to catch a string message there and would make 2353 | [debugging more painful](https://basarat.gitbook.io/typescript/type-system/exceptions#always-use-error). 2354 | For the same reason you should reject promises with `Error` types. 2355 | 2356 | **Bad:** 2357 | 2358 | ```ts 2359 | function calculateTotal(items: Item[]): number { 2360 | throw 'Not implemented.'; 2361 | } 2362 | 2363 | function get(): Promise { 2364 | return Promise.reject('Not implemented.'); 2365 | } 2366 | ``` 2367 | 2368 | **Good:** 2369 | 2370 | ```ts 2371 | function calculateTotal(items: Item[]): number { 2372 | throw new Error('Not implemented.'); 2373 | } 2374 | 2375 | function get(): Promise { 2376 | return Promise.reject(new Error('Not implemented.')); 2377 | } 2378 | 2379 | // or equivalent to: 2380 | 2381 | async function get(): Promise { 2382 | throw new Error('Not implemented.'); 2383 | } 2384 | ``` 2385 | 2386 | The benefit of using `Error` types is that it is supported by the syntax `try/catch/finally` and implicitly all errors have the `stack` property which 2387 | is very powerful for debugging. 2388 | There are also another alternatives, not to use the `throw` syntax and instead always return custom error objects. TypeScript makes this even easier. 2389 | Consider following example: 2390 | 2391 | ```ts 2392 | type Result = { isError: false, value: R }; 2393 | type Failure = { isError: true, error: E }; 2394 | type Failable = Result | Failure; 2395 | 2396 | function calculateTotal(items: Item[]): Failable { 2397 | if (items.length === 0) { 2398 | return { isError: true, error: 'empty' }; 2399 | } 2400 | 2401 | // ... 2402 | return { isError: false, value: 42 }; 2403 | } 2404 | ``` 2405 | 2406 | For the detailed explanation of this idea refer to the [original post](https://medium.com/@dhruvrajvanshi/making-exceptions-type-safe-in-typescript-c4d200ee78e9). 2407 | 2408 | **[⬆ back to top](#table-of-contents)** 2409 | 2410 | ### Don't ignore caught errors 2411 | 2412 | Doing nothing with a caught error doesn't give you the ability to ever fix or react to said error. Logging the error to the console (`console.log`) isn't much better as often times it can get lost in a sea of things printed to the console. If you wrap any bit of code in a `try/catch` it means you think an error may occur there and therefore you should have a plan, or create a code path, for when it occurs. 2413 | 2414 | **Bad:** 2415 | 2416 | ```ts 2417 | try { 2418 | functionThatMightThrow(); 2419 | } catch (error) { 2420 | console.log(error); 2421 | } 2422 | 2423 | // or even worse 2424 | 2425 | try { 2426 | functionThatMightThrow(); 2427 | } catch (error) { 2428 | // ignore error 2429 | } 2430 | ``` 2431 | 2432 | **Good:** 2433 | 2434 | ```ts 2435 | import { logger } from './logging' 2436 | 2437 | try { 2438 | functionThatMightThrow(); 2439 | } catch (error) { 2440 | logger.log(error); 2441 | } 2442 | ``` 2443 | 2444 | **[⬆ back to top](#table-of-contents)** 2445 | 2446 | ### Don't ignore rejected promises 2447 | 2448 | For the same reason you shouldn't ignore caught errors from `try/catch`. 2449 | 2450 | **Bad:** 2451 | 2452 | ```ts 2453 | getUser() 2454 | .then((user: User) => { 2455 | return sendEmail(user.email, 'Welcome!'); 2456 | }) 2457 | .catch((error) => { 2458 | console.log(error); 2459 | }); 2460 | ``` 2461 | 2462 | **Good:** 2463 | 2464 | ```ts 2465 | import { logger } from './logging' 2466 | 2467 | getUser() 2468 | .then((user: User) => { 2469 | return sendEmail(user.email, 'Welcome!'); 2470 | }) 2471 | .catch((error) => { 2472 | logger.log(error); 2473 | }); 2474 | 2475 | // or using the async/await syntax: 2476 | 2477 | try { 2478 | const user = await getUser(); 2479 | await sendEmail(user.email, 'Welcome!'); 2480 | } catch (error) { 2481 | logger.log(error); 2482 | } 2483 | ``` 2484 | 2485 | **[⬆ back to top](#table-of-contents)** 2486 | 2487 | ## Formatting 2488 | 2489 | Formatting is subjective. Like many rules herein, there is no hard and fast rule that you must follow. The main point is *DO NOT ARGUE* over formatting. There are tons of tools to automate this. Use one! It's a waste of time and money for engineers to argue over formatting. The general rule to follow is *keep consistent formatting rules*. 2490 | 2491 | For TypeScript there is a powerful tool called [TSLint](https://palantir.github.io/tslint/). It's a static analysis tool that can help you improve dramatically the readability and maintainability of your code. There are ready to use TSLint configurations that you can reference in your projects: 2492 | 2493 | - [TSLint Config Standard](https://www.npmjs.com/package/tslint-config-standard) - standard style rules 2494 | 2495 | - [TSLint Config Airbnb](https://www.npmjs.com/package/tslint-config-airbnb) - Airbnb style guide 2496 | 2497 | - [TSLint Clean Code](https://www.npmjs.com/package/tslint-clean-code) - TSLint rules inspired by the [Clean Code: A Handbook of Agile Software Craftsmanship](https://www.amazon.ca/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882) 2498 | 2499 | - [TSLint react](https://www.npmjs.com/package/tslint-react) - lint rules related to React & JSX 2500 | 2501 | - [TSLint + Prettier](https://www.npmjs.com/package/tslint-config-prettier) - lint rules for [Prettier](https://github.com/prettier/prettier) code formatter 2502 | 2503 | - [ESLint rules for TSLint](https://www.npmjs.com/package/tslint-eslint-rules) - ESLint rules for TypeScript 2504 | 2505 | - [Immutable](https://www.npmjs.com/package/tslint-immutable) - rules to disable mutation in TypeScript 2506 | 2507 | Refer also to this great [TypeScript StyleGuide and Coding Conventions](https://basarat.gitbook.io/typescript/styleguide) source. 2508 | 2509 | ### Use consistent capitalization 2510 | 2511 | Capitalization tells you a lot about your variables, functions, etc. These rules are subjective, so your team can choose whatever they want. The point is, no matter what you all choose, just *be consistent*. 2512 | 2513 | **Bad:** 2514 | 2515 | ```ts 2516 | const DAYS_IN_WEEK = 7; 2517 | const daysInMonth = 30; 2518 | 2519 | const songs = ['Back In Black', 'Stairway to Heaven', 'Hey Jude']; 2520 | const Artists = ['ACDC', 'Led Zeppelin', 'The Beatles']; 2521 | 2522 | function eraseDatabase() {} 2523 | function restore_database() {} 2524 | 2525 | type animal = { /* ... */ } 2526 | type Container = { /* ... */ } 2527 | ``` 2528 | 2529 | **Good:** 2530 | 2531 | ```ts 2532 | const DAYS_IN_WEEK = 7; 2533 | const DAYS_IN_MONTH = 30; 2534 | 2535 | const SONGS = ['Back In Black', 'Stairway to Heaven', 'Hey Jude']; 2536 | const ARTISTS = ['ACDC', 'Led Zeppelin', 'The Beatles']; 2537 | 2538 | function eraseDatabase() {} 2539 | function restoreDatabase() {} 2540 | 2541 | type Animal = { /* ... */ } 2542 | type Container = { /* ... */ } 2543 | ``` 2544 | 2545 | Prefer using `PascalCase` for class, interface, type and namespace names. 2546 | Prefer using `camelCase` for variables, functions and class members. 2547 | 2548 | **[⬆ back to top](#table-of-contents)** 2549 | 2550 | ### Function callers and callees should be close 2551 | 2552 | If a function calls another, keep those functions vertically close in the source file. Ideally, keep the caller right above the callee. 2553 | We tend to read code from top-to-bottom, like a newspaper. Because of this, make your code read that way. 2554 | 2555 | **Bad:** 2556 | 2557 | ```ts 2558 | class PerformanceReview { 2559 | constructor(private readonly employee: Employee) { 2560 | } 2561 | 2562 | private lookupPeers() { 2563 | return db.lookup(this.employee.id, 'peers'); 2564 | } 2565 | 2566 | private lookupManager() { 2567 | return db.lookup(this.employee, 'manager'); 2568 | } 2569 | 2570 | private getPeerReviews() { 2571 | const peers = this.lookupPeers(); 2572 | // ... 2573 | } 2574 | 2575 | review() { 2576 | this.getPeerReviews(); 2577 | this.getManagerReview(); 2578 | this.getSelfReview(); 2579 | 2580 | // ... 2581 | } 2582 | 2583 | private getManagerReview() { 2584 | const manager = this.lookupManager(); 2585 | } 2586 | 2587 | private getSelfReview() { 2588 | // ... 2589 | } 2590 | } 2591 | 2592 | const review = new PerformanceReview(employee); 2593 | review.review(); 2594 | ``` 2595 | 2596 | **Good:** 2597 | 2598 | ```ts 2599 | class PerformanceReview { 2600 | constructor(private readonly employee: Employee) { 2601 | } 2602 | 2603 | review() { 2604 | this.getPeerReviews(); 2605 | this.getManagerReview(); 2606 | this.getSelfReview(); 2607 | 2608 | // ... 2609 | } 2610 | 2611 | private getPeerReviews() { 2612 | const peers = this.lookupPeers(); 2613 | // ... 2614 | } 2615 | 2616 | private lookupPeers() { 2617 | return db.lookup(this.employee.id, 'peers'); 2618 | } 2619 | 2620 | private getManagerReview() { 2621 | const manager = this.lookupManager(); 2622 | } 2623 | 2624 | private lookupManager() { 2625 | return db.lookup(this.employee, 'manager'); 2626 | } 2627 | 2628 | private getSelfReview() { 2629 | // ... 2630 | } 2631 | } 2632 | 2633 | const review = new PerformanceReview(employee); 2634 | review.review(); 2635 | ``` 2636 | 2637 | **[⬆ back to top](#table-of-contents)** 2638 | 2639 | ### Organize imports 2640 | 2641 | With clean and easy to read import statements you can quickly see the dependencies of current code. Make sure you apply following good practices for `import` statements: 2642 | 2643 | - Import statements should be alphabetized and grouped. 2644 | - Unused imports should be removed. 2645 | - Named imports must be alphabetized (i.e. `import {A, B, C} from 'foo';`) 2646 | - Import sources must be alphabetized within groups, i.e.: `import * as foo from 'a'; import * as bar from 'b';` 2647 | - Groups of imports are delineated by blank lines. 2648 | - Groups must respect following order: 2649 | - Polyfills (i.e. `import 'reflect-metadata';`) 2650 | - Node builtin modules (i.e. `import fs from 'fs';`) 2651 | - external modules (i.e. `import { query } from 'itiriri';`) 2652 | - internal modules (i.e `import { UserService } from 'src/services/userService';`) 2653 | - modules from a parent directory (i.e. `import foo from '../foo'; import qux from '../../foo/qux';`) 2654 | - modules from the same or a sibling's directory (i.e. `import bar from './bar'; import baz from './bar/baz';`) 2655 | 2656 | **Bad:** 2657 | 2658 | ```ts 2659 | import { TypeDefinition } from '../types/typeDefinition'; 2660 | import { AttributeTypes } from '../model/attribute'; 2661 | import { ApiCredentials, Adapters } from './common/api/authorization'; 2662 | import fs from 'fs'; 2663 | import { ConfigPlugin } from './plugins/config/configPlugin'; 2664 | import { BindingScopeEnum, Container } from 'inversify'; 2665 | import 'reflect-metadata'; 2666 | ``` 2667 | 2668 | **Good:** 2669 | 2670 | ```ts 2671 | import 'reflect-metadata'; 2672 | 2673 | import fs from 'fs'; 2674 | import { BindingScopeEnum, Container } from 'inversify'; 2675 | 2676 | import { AttributeTypes } from '../model/attribute'; 2677 | import { TypeDefinition } from '../types/typeDefinition'; 2678 | 2679 | import { ApiCredentials, Adapters } from './common/api/authorization'; 2680 | import { ConfigPlugin } from './plugins/config/configPlugin'; 2681 | ``` 2682 | 2683 | **[⬆ back to top](#table-of-contents)** 2684 | 2685 | ### Use typescript aliases 2686 | 2687 | Create prettier imports by defining the paths and baseUrl properties in the compilerOptions section in the `tsconfig.json` 2688 | 2689 | This will avoid long relative paths when doing imports. 2690 | 2691 | **Bad:** 2692 | 2693 | ```ts 2694 | import { UserService } from '../../../services/UserService'; 2695 | ``` 2696 | 2697 | **Good:** 2698 | 2699 | ```ts 2700 | import { UserService } from '@services/UserService'; 2701 | ``` 2702 | 2703 | ```js 2704 | // tsconfig.json 2705 | ... 2706 | "compilerOptions": { 2707 | ... 2708 | "baseUrl": "src", 2709 | "paths": { 2710 | "@services": ["services/*"] 2711 | } 2712 | ... 2713 | } 2714 | ... 2715 | ``` 2716 | 2717 | **[⬆ back to top](#table-of-contents)** 2718 | 2719 | ## Comments 2720 | 2721 | The use of a comments is an indication of failure to express without them. Code should be the only source of truth. 2722 | 2723 | > Don’t comment bad code—rewrite it. 2724 | > — *Brian W. Kernighan and P. J. Plaugher* 2725 | 2726 | ### Prefer self explanatory code instead of comments 2727 | 2728 | Comments are an apology, not a requirement. Good code *mostly* documents itself. 2729 | 2730 | **Bad:** 2731 | 2732 | ```ts 2733 | // Check if subscription is active. 2734 | if (subscription.endDate > Date.now) { } 2735 | ``` 2736 | 2737 | **Good:** 2738 | 2739 | ```ts 2740 | const isSubscriptionActive = subscription.endDate > Date.now; 2741 | if (isSubscriptionActive) { /* ... */ } 2742 | ``` 2743 | 2744 | **[⬆ back to top](#table-of-contents)** 2745 | 2746 | ### Don't leave commented out code in your codebase 2747 | 2748 | Version control exists for a reason. Leave old code in your history. 2749 | 2750 | **Bad:** 2751 | 2752 | ```ts 2753 | type User = { 2754 | name: string; 2755 | email: string; 2756 | // age: number; 2757 | // jobPosition: string; 2758 | } 2759 | ``` 2760 | 2761 | **Good:** 2762 | 2763 | ```ts 2764 | type User = { 2765 | name: string; 2766 | email: string; 2767 | } 2768 | ``` 2769 | 2770 | **[⬆ back to top](#table-of-contents)** 2771 | 2772 | ### Don't have journal comments 2773 | 2774 | Remember, use version control! There's no need for dead code, commented code, and especially journal comments. Use `git log` to get history! 2775 | 2776 | **Bad:** 2777 | 2778 | ```ts 2779 | /** 2780 | * 2016-12-20: Removed monads, didn't understand them (RM) 2781 | * 2016-10-01: Improved using special monads (JP) 2782 | * 2016-02-03: Added type-checking (LI) 2783 | * 2015-03-14: Implemented combine (JR) 2784 | */ 2785 | function combine(a: number, b: number): number { 2786 | return a + b; 2787 | } 2788 | ``` 2789 | 2790 | **Good:** 2791 | 2792 | ```ts 2793 | function combine(a: number, b: number): number { 2794 | return a + b; 2795 | } 2796 | ``` 2797 | 2798 | **[⬆ back to top](#table-of-contents)** 2799 | 2800 | ### Avoid positional markers 2801 | 2802 | They usually just add noise. Let the functions and variable names along with the proper indentation and formatting give the visual structure to your code. 2803 | Most IDE support code folding feature that allows you to collapse/expand blocks of code (see Visual Studio Code [folding regions](https://code.visualstudio.com/updates/v1_17#_folding-regions)). 2804 | 2805 | **Bad:** 2806 | 2807 | ```ts 2808 | //////////////////////////////////////////////////////////////////////////////// 2809 | // Client class 2810 | //////////////////////////////////////////////////////////////////////////////// 2811 | class Client { 2812 | id: number; 2813 | name: string; 2814 | address: Address; 2815 | contact: Contact; 2816 | 2817 | //////////////////////////////////////////////////////////////////////////////// 2818 | // public methods 2819 | //////////////////////////////////////////////////////////////////////////////// 2820 | public describe(): string { 2821 | // ... 2822 | } 2823 | 2824 | //////////////////////////////////////////////////////////////////////////////// 2825 | // private methods 2826 | //////////////////////////////////////////////////////////////////////////////// 2827 | private describeAddress(): string { 2828 | // ... 2829 | } 2830 | 2831 | private describeContact(): string { 2832 | // ... 2833 | } 2834 | }; 2835 | ``` 2836 | 2837 | **Good:** 2838 | 2839 | ```ts 2840 | class Client { 2841 | id: number; 2842 | name: string; 2843 | address: Address; 2844 | contact: Contact; 2845 | 2846 | public describe(): string { 2847 | // ... 2848 | } 2849 | 2850 | private describeAddress(): string { 2851 | // ... 2852 | } 2853 | 2854 | private describeContact(): string { 2855 | // ... 2856 | } 2857 | }; 2858 | ``` 2859 | 2860 | **[⬆ back to top](#table-of-contents)** 2861 | 2862 | ### TODO comments 2863 | 2864 | When you find yourself that you need to leave notes in the code for some later improvements, 2865 | do that using `// TODO` comments. Most IDE have special support for those kind of comments so that 2866 | you can quickly go over the entire list of todos. 2867 | 2868 | Keep in mind however that a *TODO* comment is not an excuse for bad code. 2869 | 2870 | **Bad:** 2871 | 2872 | ```ts 2873 | function getActiveSubscriptions(): Promise { 2874 | // ensure `dueDate` is indexed. 2875 | return db.subscriptions.find({ dueDate: { $lte: new Date() } }); 2876 | } 2877 | ``` 2878 | 2879 | **Good:** 2880 | 2881 | ```ts 2882 | function getActiveSubscriptions(): Promise { 2883 | // TODO: ensure `dueDate` is indexed. 2884 | return db.subscriptions.find({ dueDate: { $lte: new Date() } }); 2885 | } 2886 | ``` 2887 | 2888 | **[⬆ back to top](#table-of-contents)** 2889 | 2890 | ## Translations 2891 | 2892 | This is also available in other languages: 2893 | - ![br](https://raw.githubusercontent.com/gosquared/flags/master/flags/flags/shiny/24/Brazil.png) **Brazilian Portuguese**: [vitorfreitas/clean-code-typescript](https://github.com/vitorfreitas/clean-code-typescript) 2894 | - ![cn](https://raw.githubusercontent.com/gosquared/flags/master/flags/flags/shiny/24/China.png) **Chinese**: 2895 | - [beginor/clean-code-typescript](https://github.com/beginor/clean-code-typescript) 2896 | - [pipiliang/clean-code-typescript](https://github.com/pipiliang/clean-code-typescript) 2897 | - ![ja](https://raw.githubusercontent.com/gosquared/flags/master/flags/flags/shiny/24/Japan.png) **Japanese**: [MSakamaki/clean-code-typescript](https://github.com/MSakamaki/clean-code-typescript) 2898 | - ![ru](https://raw.githubusercontent.com/gosquared/flags/master/flags/flags/shiny/24/Russia.png) **Russian**: [Real001/clean-code-typescript](https://github.com/Real001/clean-code-typescript) 2899 | - ![tr](https://raw.githubusercontent.com/gosquared/flags/master/flags/flags/shiny/24/Turkey.png) **Turkish**: [ozanhonamlioglu/clean-code-typescript](https://github.com/ozanhonamlioglu/clean-code-typescript) 2900 | 2901 | 2902 | There is work in progress for translating this to other languages: 2903 | 2904 | - ![kr](https://raw.githubusercontent.com/gosquared/flags/master/flags/flags/shiny/24/South-Korea.png) Korean 2905 | 2906 | References will be added once translations are completed. 2907 | Check this [discussion](https://github.com/labs42io/clean-code-typescript/issues/15) for more details and progress. 2908 | You can make an indispensable contribution to *Clean Code* community by translating this to your language. 2909 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

5 | clean-code-typescript 6 |

7 | 8 |

9 | 10 | Tweet 11 | 12 | 13 | Tweet 14 | 15 |

16 | 17 |

18 | 타입스크립트를 위한 클린코드 19 |

20 |

21 | clean-code-javascript에서 영감을 받았습니다. 22 |

23 | 24 | ## 목차 25 | 26 | 1. [소개](#소개) 27 | 2. [변수](#변수) 28 | 3. [함수](#함수) 29 | 4. [객체와 자료구조](#객체와-자료구조) 30 | 5. [클래스](#클래스) 31 | 6. [SOLID](#solid) 32 | 7. [테스트](#테스트) 33 | 8. [동시성](#동시성) 34 | 9. [에러 처리](#에러-처리) 35 | 10. [서식](#서식) 36 | 11. [주석](#주석) 37 | 12. [번역](#번역) 38 | 13. [번역에 도움을 주신 분들](#번역에-도움을-주신-분들) 39 | 40 | ## 소개 41 | 42 | ![Humorous image of software quality estimation as a count of how many expletives 43 | you shout when reading code](https://www.osnews.com/images/comics/wtfm.jpg) 44 | 45 | Robert C. Martin의 책인 [*클린 코드*](http://www.yes24.com/Product/Goods/11681152)에 있는 소프트웨어 공학 방법론을 타입스크립트에 적용한 글입니다. 이 글은 스타일 가이드가 아닙니다. 이 글은 타입스크립트에서 [읽기 쉽고, 재사용 가능하며, 리팩토링 가능한](https://github.com/ryanmcdermott/3rs-of-software-architecture) 소프트웨어를 작성하기 위한 가이드입니다. 46 | 47 | 여기 있는 모든 규칙을 엄격하게 따를 필요는 없으며, 보편적으로 통용되는 규칙은 아닙니다. 이 글은 하나의 지침일 뿐이며, *클린 코드*의 저자가 수년간 경험한 내용을 바탕으로 정리한 것입니다. 48 | 49 | 소프트웨어 공학 기술의 역사는 50년이 조금 넘었고, 배워야 할 것이 여전히 많습니다. 소프트웨어 설계가 건축 설계만큼 오래되었을 때는 아마도 아래 규칙들보다 엄격한 규칙을 따라야 할 것입니다. 하지만 지금은 이 지침을 당신과 당신 팀이 작성하는 타입스크립트 코드의 품질을 평가하는 기준으로 삼으세요. 50 | 51 | 한 가지 더 말씀드리자면, 이 규칙들을 알게 된다 해서 당장 더 나은 개발자가 되는 것은 아니며 코드를 작성할 때 실수를 하지 않게 해주는 것은 아닙니다. 젖은 점토가 최종의 결과물로 빚어지는 것처럼 모든 코드들도 처음 작성한 코드로 시작됩니다. 결국은 동료들과 리뷰하면서 결점이 제거됩니다. 당신이 처음 작성한 코드에 개선이 필요할 때 자책하지 마세요. 대신 코드가 더 나아지도록 두들기세요! 52 | 53 | **[⬆ 맨 위로 이동](#목차)** 54 | 55 | ## 변수 56 | 57 | ### 의미있는 변수 이름을 사용하세요 58 | 59 | 읽는 사람으로 하여금 변수마다 어떤 점이 다른지 알 수 있도록 이름을 구별하세요. 60 | 61 | **Bad:** 62 | 63 | ```ts 64 | function between(a1: T, a2: T, a3: T): boolean { 65 | return a2 <= a1 && a1 <= a3; 66 | } 67 | 68 | ``` 69 | 70 | **Good:** 71 | 72 | ```ts 73 | function between(value: T, left: T, right: T): boolean { 74 | return left <= value && value <= right; 75 | } 76 | ``` 77 | 78 | **[⬆ 맨 위로 이동](#목차)** 79 | 80 | ### 발음할 수 있는 변수 이름을 사용하세요 81 | 82 | 발음할 수 없는 이름은 그 변수에 대해서 바보 같이 소리를 내 토론할 수밖에 없습니다. 83 | 84 | **Bad:** 85 | 86 | ```ts 87 | type DtaRcrd102 = { 88 | genymdhms: Date; 89 | modymdhms: Date; 90 | pszqint: number; 91 | } 92 | ``` 93 | 94 | **Good:** 95 | 96 | ```ts 97 | type Customer = { 98 | generationTimestamp: Date; 99 | modificationTimestamp: Date; 100 | recordId: number; 101 | } 102 | ``` 103 | 104 | **[⬆ 맨 위로 이동](#목차)** 105 | 106 | ### 동일한 유형의 변수는 동일한 단어를 사용하세요 107 | 108 | **Bad:** 109 | 110 | ```ts 111 | function getUserInfo(): User; 112 | function getUserDetails(): User; 113 | function getUserData(): User; 114 | ``` 115 | 116 | **Good:** 117 | 118 | ```ts 119 | function getUser(): User; 120 | ``` 121 | 122 | **[⬆ 맨 위로 이동](#목차)** 123 | 124 | ### 검색할 수 있는 이름을 사용하세요 125 | 126 | 코드를 쓸 때보다 읽을 때가 더 많기 때문에 우리가 쓰는 코드는 읽을 수 있고 검색이 가능해야 합니다. 프로그램을 이해할 때 의미있는 변수 이름을 짓지 않으면 읽는 사람으로 하여금 어려움을 줄 수 있습니다. 검색 가능한 이름을 지으세요. [TSLint](https://palantir.github.io/tslint/rules/no-magic-numbers/)와 같은 도구는 이름이 없는 상수를 식별할 수 있도록 도와줍니다. 127 | 128 | **Bad:** 129 | 130 | ```ts 131 | // 86400000이 도대체 뭐지? 132 | setTimeout(restart, 86400000); 133 | ``` 134 | 135 | **Good:** 136 | 137 | ```ts 138 | // 대문자로 이루어진 상수로 선언하세요. 139 | const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000; 140 | 141 | setTimeout(restart, MILLISECONDS_IN_A_DAY); 142 | ``` 143 | 144 | **[⬆ 맨 위로 이동](#목차)** 145 | 146 | ### 의도를 나타내는 변수를 사용하세요 147 | 148 | **Bad:** 149 | 150 | ```ts 151 | declare const users: Map; 152 | 153 | for (const keyValue of users) { 154 | // users 맵을 순회 155 | } 156 | ``` 157 | 158 | **Good:** 159 | 160 | ```ts 161 | declare const users: Map; 162 | 163 | for (const [id, user] of users) { 164 | // users 맵을 순회 165 | } 166 | ``` 167 | 168 | **[⬆ 맨 위로 이동](#목차)** 169 | 170 | ### 암시하는 이름은 사용하지 마세요 171 | 172 | 명시적인 것이 암시적인 것보다 좋습니다. 173 | *명료함은 최고입니다.* 174 | 175 | **Bad:** 176 | 177 | ```ts 178 | const u = getUser(); 179 | const s = getSubscription(); 180 | const t = charge(u, s); 181 | ``` 182 | 183 | **Good:** 184 | 185 | ```ts 186 | const user = getUser(); 187 | const subscription = getSubscription(); 188 | const transaction = charge(user, subscription); 189 | ``` 190 | 191 | **[⬆ 맨 위로 이동](#목차)** 192 | 193 | ### 불필요한 문맥은 추가하지 마세요 194 | 195 | 클래스/타입/객체의 이름에 의미가 담겨있다면, 변수 이름에서 반복하지 마세요. 196 | 197 | **Bad:** 198 | 199 | ```ts 200 | type Car = { 201 | carMake: string; 202 | carModel: string; 203 | carColor: string; 204 | } 205 | 206 | function print(car: Car): void { 207 | console.log(`${car.carMake} ${car.carModel} (${car.carColor})`); 208 | } 209 | ``` 210 | 211 | **Good:** 212 | 213 | ```ts 214 | type Car = { 215 | make: string; 216 | model: string; 217 | color: string; 218 | } 219 | 220 | function print(car: Car): void { 221 | console.log(`${car.make} ${car.model} (${car.color})`); 222 | } 223 | ``` 224 | 225 | **[⬆ 맨 위로 이동](#목차)** 226 | 227 | ### short circuiting이나 조건문 대신 기본 매개변수를 사용하세요 228 | 229 | 기본 매개변수는 short circuiting보다 보통 명료합니다. 230 | 231 | **Bad:** 232 | 233 | ```ts 234 | function loadPages(count?: number) { 235 | const loadCount = count !== undefined ? count : 10; 236 | // ... 237 | } 238 | ``` 239 | 240 | **Good:** 241 | 242 | ```ts 243 | function loadPages(count: number = 10) { 244 | // ... 245 | } 246 | ``` 247 | 248 | **[⬆ 맨 위로 이동](#목차)** 249 | 250 | ### 의도를 알려주기 위해 `enum`을 사용하세요 251 | 252 | 예를 들어 그것들의 값 자체보다 값이 구별되어야 할 때와 같이 코드의 의도를 알려주는데에 `enum`은 도움을 줄 수 있습니다. 253 | 254 | **Bad:** 255 | 256 | ```ts 257 | const GENRE = { 258 | ROMANTIC: 'romantic', 259 | DRAMA: 'drama', 260 | COMEDY: 'comedy', 261 | DOCUMENTARY: 'documentary', 262 | } 263 | 264 | projector.configureFilm(GENRE.COMEDY); 265 | 266 | class Projector { 267 | // Projector의 선언 268 | configureFilm(genre) { 269 | switch (genre) { 270 | case GENRE.ROMANTIC: 271 | // 실행되어야 하는 로직 272 | } 273 | } 274 | } 275 | ``` 276 | 277 | **Good:** 278 | 279 | ```ts 280 | enum GENRE { 281 | ROMANTIC, 282 | DRAMA, 283 | COMEDY, 284 | DOCUMENTARY, 285 | } 286 | 287 | projector.configureFilm(GENRE.COMEDY); 288 | 289 | class Projector { 290 | // Projector의 선언 291 | configureFilm(genre) { 292 | switch (genre) { 293 | case GENRE.ROMANTIC: 294 | // 실행되어야 하는 로직 295 | } 296 | } 297 | } 298 | ``` 299 | 300 | **[⬆ 맨 위로 이동](#목차)** 301 | 302 | ## 함수 303 | 304 | ### 함수의 매개변수는 2개 혹은 그 이하가 이상적입니다 305 | 306 | 함수 매개변수의 개수를 제한하는 것은 함수를 테스트하기 쉽게 만들어주기 때문에 놀라울 정도로 중요합니다. 307 | 함수 매개변수가 3개 이상인 경우, 각기 다른 인수로 여러 다른 케이스를 테스트해야 하므로 경우의 수가 매우 많아집니다. 308 | 309 | 한 개 혹은 두 개의 매개변수가 이상적인 경우고, 가능하다면 세 개는 피해야 합니다. 그 이상의 경우에는 합쳐야 합니다. 310 | 두 개 이상의 매개변수를 가질 경우, 함수가 많은 것을 할 가능성이 높아집니다. 311 | 그렇지 않은 경우, 대부분 상위 객체는 하나의 매개변수로 충분할 것입니다. 312 | 313 | 많은 매개변수를 사용해야 한다면 객체 리터럴을 사용하는 것을 고려해보세요. 314 | 315 | 함수가 기대하는 속성을 명확하게 하기 위해, [구조 분해](https://basarat.gitbook.io/typescript/future-javascript/destructuring) 구문을 사용할 수 있습니다. 316 | 이 구문은 몇 개의 장점을 가지고 있습니다: 317 | 318 | 1. 어떤 사람이 함수 시그니쳐(매개변수의 타입, 반환값의 타입 등)를 볼 때, 어떤 속성이 사용되는지 즉시 알 수 있습니다. 319 | 320 | 2. 명명된 매개변수처럼 보이게 할 때 사용할 수 있습니다. 321 | 322 | 3. 또한 구조 분해는 함수로 전달된 매개변수 객체의 특정한 원시 값을 복제하며 이것은 사이드 이펙트를 방지하는데 도움을 줍니다. 유의사항: 매개변수 객체로부터 구조 분해된 객체와 배열은 **복제되지 않습니다.** 323 | 324 | 4. 타입스크립트는 사용하지 않은 속성에 대해서 경고를 주며, 구조 분해를 사용하면 경고를 받지 않을 수 있습니다. 325 | 326 | **Bad:** 327 | 328 | ```ts 329 | function createMenu(title: string, body: string, buttonText: string, cancellable: boolean) { 330 | // ... 331 | } 332 | 333 | createMenu('Foo', 'Bar', 'Baz', true); 334 | ``` 335 | 336 | **Good:** 337 | 338 | ```ts 339 | function createMenu(options: { title: string, body: string, buttonText: string, cancellable: boolean }) { 340 | // ... 341 | } 342 | 343 | createMenu({ 344 | title: 'Foo', 345 | body: 'Bar', 346 | buttonText: 'Baz', 347 | cancellable: true 348 | }); 349 | ``` 350 | 351 | [타입 앨리어스](https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-aliases)를 사용해서 가독성을 더 높일 수 있습니다: 352 | 353 | ```ts 354 | 355 | type MenuOptions = { title: string, body: string, buttonText: string, cancellable: boolean }; 356 | 357 | function createMenu(options: MenuOptions) { 358 | // ... 359 | } 360 | 361 | createMenu({ 362 | title: 'Foo', 363 | body: 'Bar', 364 | buttonText: 'Baz', 365 | cancellable: true 366 | }); 367 | ``` 368 | 369 | **[⬆ 맨 위로 이동](#목차)** 370 | 371 | ### 함수는 한 가지만 해야합니다 372 | 373 | 이것은 소프트웨어 공학에서 단연코 가장 중요한 규칙입니다. 함수가 한 가지 이상의 역할을 수행할 때 작성하고 테스트하고 추론하기 어려워집니다. 함수를 하나의 행동으로 정의할 수 있을 때, 쉽게 리팩토링할 수 있으며 코드를 더욱 명료하게 읽을 수 있습니다. 374 | 이 가이드에서 이 부분만 자기것으로 만들어도 당신은 많은 개발자보다 앞설 수 있습니다. 375 | 376 | **Bad:** 377 | 378 | ```ts 379 | function emailClients(clients: Client[]) { 380 | clients.forEach((client) => { 381 | const clientRecord = database.lookup(client); 382 | if (clientRecord.isActive()) { 383 | email(client); 384 | } 385 | }); 386 | } 387 | ``` 388 | 389 | **Good:** 390 | 391 | ```ts 392 | function emailClients(clients: Client[]) { 393 | clients.filter(isActiveClient).forEach(email); 394 | } 395 | 396 | function isActiveClient(client: Client) { 397 | const clientRecord = database.lookup(client); 398 | return clientRecord.isActive(); 399 | } 400 | ``` 401 | 402 | **[⬆ 맨 위로 이동](#목차)** 403 | 404 | ### 함수가 무엇을 하는지 알 수 있도록 함수 이름을 지으세요 405 | 406 | **Bad:** 407 | 408 | ```ts 409 | function addToDate(date: Date, month: number): Date { 410 | // ... 411 | } 412 | 413 | const date = new Date(); 414 | 415 | // 무엇이 추가되는지 함수 이름만으로 유추하기 어렵습니다 416 | addToDate(date, 1); 417 | ``` 418 | 419 | **Good:** 420 | 421 | ```ts 422 | function addMonthToDate(date: Date, month: number): Date { 423 | // ... 424 | } 425 | 426 | const date = new Date(); 427 | addMonthToDate(date, 1); 428 | ``` 429 | 430 | **[⬆ 맨 위로 이동](#목차)** 431 | 432 | ### 함수는 단일 행동을 추상화해야 합니다 433 | 434 | 함수가 한 가지 이상을 추상화한다면 그 함수는 너무 많은 일을 하게 됩니다. 재사용성과 쉬운 테스트를 위해서 함수를 쪼개세요. 435 | 436 | **Bad:** 437 | 438 | ```ts 439 | function parseCode(code: string) { 440 | const REGEXES = [ /* ... */ ]; 441 | const statements = code.split(' '); 442 | const tokens = []; 443 | 444 | REGEXES.forEach((regex) => { 445 | statements.forEach((statement) => { 446 | // ... 447 | }); 448 | }); 449 | 450 | const ast = []; 451 | tokens.forEach((token) => { 452 | // lex... 453 | }); 454 | 455 | ast.forEach((node) => { 456 | // parse... 457 | }); 458 | } 459 | ``` 460 | 461 | **Good:** 462 | 463 | ```ts 464 | const REGEXES = [ /* ... */ ]; 465 | 466 | function parseCode(code: string) { 467 | const tokens = tokenize(code); 468 | const syntaxTree = parse(tokens); 469 | 470 | syntaxTree.forEach((node) => { 471 | // parse... 472 | }); 473 | } 474 | 475 | function tokenize(code: string): Token[] { 476 | const statements = code.split(' '); 477 | const tokens: Token[] = []; 478 | 479 | REGEXES.forEach((regex) => { 480 | statements.forEach((statement) => { 481 | tokens.push( /* ... */ ); 482 | }); 483 | }); 484 | 485 | return tokens; 486 | } 487 | 488 | function parse(tokens: Token[]): SyntaxTree { 489 | const syntaxTree: SyntaxTree[] = []; 490 | tokens.forEach((token) => { 491 | syntaxTree.push( /* ... */ ); 492 | }); 493 | 494 | return syntaxTree; 495 | } 496 | ``` 497 | 498 | **[⬆ 맨 위로 이동](#목차)** 499 | 500 | ### 중복된 코드를 제거해주세요 501 | 502 | 코드가 중복되지 않도록 최선을 다하세요. 503 | 중복된 코드는 어떤 로직을 변경할 때 한 곳 이상을 변경해야 하기 때문에 좋지 않습니다. 504 | 505 | 당신이 레스토랑을 운영하면서 재고를 추적한다고 상상해보세요: 모든 토마토, 양파, 마늘, 양념 등. 506 | 관리하는 목록이 여러개일 때 토마토를 넣은 요리를 제공할 때마다 모든 목록을 수정해야 합니다. 507 | 관리하는 목록이 단 하나일 때는 한 곳만 수정하면 됩니다! 508 | 509 | 당신은 종종 두 개 이상의 사소한 차이점이 존재한다고 생각해서 거의 비슷한 코드를 중복 작성합니다. 하지만 그 몇가지 다른 것으로 인해 같은 역할을 하는 두 개 이상의 함수를 만들게 됩니다. 중복된 코드를 제거하는 것은 조금씩 다른 역할을 하는 것을 묶음으로써 하나의 함수/모듈/클래스로 처리하는 추상화를 만드는 것을 의미합니다. 510 | 511 | 추상화를 올바르게 하는 것은 중요하며, 이것은 [SOLID](#solid) 원칙을 따르는 이유이기도 합니다. 올바르지 않은 추상화는 중복된 코드보다 나쁘므로 주의하세요! 좋은 추상화를 할 수 있다면 그렇게 하라는 말입니다! 반복하지 마세요. 그렇지 않으면 하나를 변경할 때마다 여러 곳을 변경하게 될 것입니다. 512 | 513 | **Bad:** 514 | 515 | ```ts 516 | function showDeveloperList(developers: Developer[]) { 517 | developers.forEach((developer) => { 518 | const expectedSalary = developer.calculateExpectedSalary(); 519 | const experience = developer.getExperience(); 520 | const githubLink = developer.getGithubLink(); 521 | 522 | const data = { 523 | expectedSalary, 524 | experience, 525 | githubLink 526 | }; 527 | 528 | render(data); 529 | }); 530 | } 531 | 532 | function showManagerList(managers: Manager[]) { 533 | managers.forEach((manager) => { 534 | const expectedSalary = manager.calculateExpectedSalary(); 535 | const experience = manager.getExperience(); 536 | const portfolio = manager.getMBAProjects(); 537 | 538 | const data = { 539 | expectedSalary, 540 | experience, 541 | portfolio 542 | }; 543 | 544 | render(data); 545 | }); 546 | } 547 | ``` 548 | 549 | **Good:** 550 | 551 | ```ts 552 | class Developer { 553 | // ... 554 | getExtraDetails() { 555 | return { 556 | githubLink: this.githubLink, 557 | } 558 | } 559 | } 560 | 561 | class Manager { 562 | // ... 563 | getExtraDetails() { 564 | return { 565 | portfolio: this.portfolio, 566 | } 567 | } 568 | } 569 | 570 | function showEmployeeList(employee: Developer | Manager) { 571 | employee.forEach((employee) => { 572 | const expectedSalary = employee.calculateExpectedSalary(); 573 | const experience = employee.getExperience(); 574 | const extra = employee.getExtraDetails(); 575 | 576 | const data = { 577 | expectedSalary, 578 | experience, 579 | extra, 580 | }; 581 | 582 | render(data); 583 | }); 584 | } 585 | ``` 586 | 587 | 당신은 중복된 코드에 대해서 비판적으로 생각해야 합니다. 가끔은 중복된 코드와 불필요한 추상화로 인한 복잡성 간의 맞바꿈이 있을 수 있습니다. 서로 다른 두 개의 모듈의 구현이 유사해 보이지만 서로 다른 도메인에 존재하는 경우, 코드 중복은 공통된 코드에서 추출해서 중복을 줄이는 것보다 나은 선택일 수 있습니다. 이 경우에 추출된 공통의 코드는 두 모듈 사이에서 간접적인 의존성이 나타나게 됩니다. 588 | 589 | **[⬆ 맨 위로 이동](#목차)** 590 | 591 | ### `Object.assign` 혹은 구조 분해를 사용해서 기본 객체를 만드세요 592 | 593 | **Bad:** 594 | 595 | ```ts 596 | type MenuConfig = { title?: string, body?: string, buttonText?: string, cancellable?: boolean }; 597 | 598 | function createMenu(config: MenuConfig) { 599 | config.title = config.title || 'Foo'; 600 | config.body = config.body || 'Bar'; 601 | config.buttonText = config.buttonText || 'Baz'; 602 | config.cancellable = config.cancellable !== undefined ? config.cancellable : true; 603 | 604 | // ... 605 | } 606 | 607 | createMenu({ body: 'Bar' }); 608 | ``` 609 | 610 | **Good:** 611 | 612 | ```ts 613 | type MenuConfig = { title?: string, body?: string, buttonText?: string, cancellable?: boolean }; 614 | 615 | function createMenu(config: MenuConfig) { 616 | const menuConfig = Object.assign({ 617 | title: 'Foo', 618 | body: 'Bar', 619 | buttonText: 'Baz', 620 | cancellable: true 621 | }, config); 622 | 623 | // ... 624 | } 625 | 626 | createMenu({ body: 'Bar' }); 627 | ``` 628 | 629 | 대안으로, 기본 값을 구조 분해를 사용해서 해결할 수 있습니다: 630 | 631 | ```ts 632 | type MenuConfig = { title?: string, body?: string, buttonText?: string, cancellable?: boolean }; 633 | 634 | function createMenu({ title = 'Foo', body = 'Bar', buttonText = 'Baz', cancellable = true }: MenuConfig) { 635 | // ... 636 | } 637 | 638 | createMenu({ body: 'Bar' }); 639 | ``` 640 | 641 | 사이드 이펙트와 `undefined` 혹은 `null` 값을 명시적으로 넘기는 예상치 못한 행동을 피하기 위해서 타입스크립트 컴파일러에게 그것을 허락하지 않도록 설정할 수 있습니다. 타입스크립트에서 [`--strictNullChecks`](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#--strictnullchecks) 옵션을 확인하세요. 642 | 643 | **[⬆ 맨 위로 이동](#목차)** 644 | 645 | ### 함수 매개변수로 플래그를 사용하지 마세요 646 | 647 | 플래그를 사용하는 것은 해당 함수가 한 가지 이상의 일을 한다는 것을 뜻합니다. 648 | 함수는 한 가지의 일을 해야합니다. boolean 변수로 인해 다른 코드가 실행된다면 그 함수를 쪼개도록 하세요. 649 | 650 | **Bad:** 651 | 652 | ```ts 653 | function createFile(name: string, temp: boolean) { 654 | if (temp) { 655 | fs.create(`./temp/${name}`); 656 | } else { 657 | fs.create(name); 658 | } 659 | } 660 | ``` 661 | 662 | **Good:** 663 | 664 | ```ts 665 | function createTempFile(name: string) { 666 | createFile(`./temp/${name}`); 667 | } 668 | 669 | function createFile(name: string) { 670 | fs.create(name); 671 | } 672 | ``` 673 | 674 | **[⬆ 맨 위로 이동](#목차)** 675 | 676 | ### 사이드 이펙트를 피하세요 (파트 1) 677 | 678 | 함수는 값을 가져와서 다른 값을 반환하는 것 이외에 다른 것을 할 경우 사이드 이펙트를 발생시킬 수 있습니다. 사이드 이펙트는 파일을 쓴다거나, 전역 변수를 조작한다거나, 뜻하지 않게 낯선 사람에게 당신의 전재산을 송금할 수 있습니다. 679 | 680 | 당신은 가끔 프로그램에서 사이드 이펙트를 가질 필요가 있습니다. 이전의 사례에서와 같이 당신은 파일을 써야할 때가 있습니다. 681 | 당신이 하고 싶은 것은 이것을 중앙화하는 것입니다. 특정 파일을 쓰기 위해 몇 개의 함수와 클래스를 만들지 마세요. 682 | 그것을 행하는 서비스를 단 하나만 만드세요. 683 | 684 | 중요한 것은 어떠한 구조도 없이 객체 사이에 상태를 공유하거나 어떤 것에 의해서든지 변경될 수 있는 데이터 타입을 사용하거나 사이드 이펙트가 일어나는 곳을 중앙화 하지 않는 것과 같은 위험 요소를 피하는 것입니다. 만약 그렇게 할 수 있다면, 당신은 대부분의 다른 프로그래머들보다 더욱 행복할 것입니다. 685 | 686 | **Bad:** 687 | 688 | ```ts 689 | // 아래의 함수에서 참조하는 전역 변수입니다. 690 | let name = 'Robert C. Martin'; 691 | 692 | function toBase64() { 693 | name = btoa(name); 694 | } 695 | 696 | toBase64(); 697 | // 이 이름을 사용하는 다른 함수가 있다면, 그것은 Base64 값을 반환할 것입니다 698 | 699 | console.log(name); // 'Robert C. Martin'이 출력되는 것을 예상했지만 'Um9iZXJ0IEMuIE1hcnRpbg=='가 출력됨 700 | ``` 701 | 702 | **Good:** 703 | 704 | ```ts 705 | const name = 'Robert C. Martin'; 706 | 707 | function toBase64(text: string): string { 708 | return btoa(text); 709 | } 710 | 711 | const encodedName = toBase64(name); 712 | console.log(name); 713 | ``` 714 | 715 | **[⬆ 맨 위로 이동](#목차)** 716 | 717 | ### 사이드 이펙트를 피하세요 (파트 2) 718 | 719 | 자바스크립트에서 원시값은 값에 의해 전달되고 객체/배열은 참조에 의해 전달됩니다. 예를 들어, 객체와 배열의 경우 어떤 함수가 쇼핑 장바구니 배열을 변경하는 기능을 가지고 있다면, 구매하려는 아이템이 추가됨으로써 `cart` 배열을 사용하는 다른 함수는 이 추가의 영향을 받을 수 있습니다. 이것은 장점이 될 수도 있지만 단점이 될 수도 있습니다. 최악의 상황을 상상해보겠습니다: 720 | 721 | 사용자는 네트워크 요청을 생성하고 서버에 `cart` 배열을 전송하는 `purchase` 함수를 호출하는 "구매" 버튼을 클릭합니다. 네트워크 연결 불량 때문에 `purchase` 함수는 요청을 재시도해야 합니다. 네트워크 요청이 시작되기 전에 사용자가 원하지 않은 아이템을 실수로 "장바구니에 추가하기" 버튼을 누르면 어떻게 될까요? 네트워크 요청이 시작되면, `purchase` 함수는 `addItemToCart` 함수가 변경한 쇼핑 장바구니 배열을 참조하고 있기 때문에 `purchase` 함수는 실수로 추가된 아이템을 보낼 것입니다. 722 | 723 | 훌륭한 해법은 `addItemToCart` 함수에서 `cart` 배열을 복제하고 그것을 수정하고 그 복제한 값을 반환하는 것입니다. 이는 쇼핑 장바구니 배열을 참조하고 있는 값을 들고 있는 어떤 다른 함수도 다른 변경에 의해 영향을 받지 않는 것을 보장합니다. 724 | 725 | 이 접근법에 대한 두 가지 주의사항: 726 | 727 | 1. 실제로는 입력된 객체값을 변경하기를 원하는 경우가 있을 수 있습니다. 하지만 이러한 프로그래밍 관례를 선택할 때 당신은 이러한 경우가 매우 드물다는 것을 알게 될 것입니다. 대부분은 사이드 이펙트가 없도록 리팩토링될 수 있습니다! ([순수 함수](https://en.wikipedia.org/wiki/Pure_function)를 확인해주세요) 728 | 729 | 3. 큰 객체를 복제하는 것은 성능 관점에서 비용이 높을 수 있습니다. 다행히도 이러한 프로그래밍 접근법을 가능하게 해주는 훌륭한 라이브러리가 있기 때문에 큰 문제는 아닙니다. 이는 수동으로 객체와 배열을 복제해주는 것만큼 메모리 집약적이지 않게 해주고 빠르게 복제해줍니다. 730 | 731 | **Bad:** 732 | 733 | ```ts 734 | function addItemToCart(cart: CartItem[], item: Item): void { 735 | cart.push({ item, date: Date.now() }); 736 | }; 737 | ``` 738 | 739 | **Good:** 740 | 741 | ```ts 742 | function addItemToCart(cart: CartItem[], item: Item): CartItem[] { 743 | return [...cart, { item, date: Date.now() }]; 744 | }; 745 | ``` 746 | 747 | **[⬆ 맨 위로 이동](#목차)** 748 | 749 | ### 전역 함수를 작성하지 마세요 750 | 751 | 전역을 더럽히는 것은 자바스크립트에서 나쁜 관습입니다. 왜냐하면 다른 라이브러리와 충돌날 수 있고 당신의 API의 사용자는 상용에서 예외가 발생할 때까지 전혀 모를 것이기 때문입니다. 한 예제를 생각해보겠습니다: 당신이 자바스크립트 네이티브 배열 메소드를 확장해서 두 배열 사이의 다른 점을 보여주는 `diff` 메소드를 추가하고 싶다면 어떨까요? `Array.prototype`에 당신의 새로운 함수를 작성할 것입니다. 하지만 동일한 기능을 수행하고 있는 다른 라이브러리와 충돌날 수 있습니다. 다른 라이브러리에서는 배열에서 첫 번째 요소와 마지막 요소 사이의 다름만 찾기 위해 `diff` 함수를 사용한다면 어떨까요? 이것이 단지 클래스를 사용해서 전역 `Array`를 상속하는 것이 더 좋은 이유입니다. 752 | 753 | **Bad:** 754 | 755 | ```ts 756 | declare global { 757 | interface Array { 758 | diff(other: T[]): Array; 759 | } 760 | } 761 | 762 | if (!Array.prototype.diff) { 763 | Array.prototype.diff = function (other: T[]): T[] { 764 | const hash = new Set(other); 765 | return this.filter(elem => !hash.has(elem)); 766 | }; 767 | } 768 | ``` 769 | 770 | **Good:** 771 | 772 | ```ts 773 | class MyArray extends Array { 774 | diff(other: T[]): T[] { 775 | const hash = new Set(other); 776 | return this.filter(elem => !hash.has(elem)); 777 | }; 778 | } 779 | ``` 780 | 781 | **[⬆ 맨 위로 이동](#목차)** 782 | 783 | ### 명령형 프로그래밍보다 함수형 프로그래밍을 지향하세요 784 | 785 | 가능하다면 이런 방식의 프로그래밍을 지향하세요. 786 | 787 | **Bad:** 788 | 789 | ```ts 790 | const contributions = [ 791 | { 792 | name: 'Uncle Bobby', 793 | linesOfCode: 500 794 | }, { 795 | name: 'Suzie Q', 796 | linesOfCode: 1500 797 | }, { 798 | name: 'Jimmy Gosling', 799 | linesOfCode: 150 800 | }, { 801 | name: 'Gracie Hopper', 802 | linesOfCode: 1000 803 | } 804 | ]; 805 | 806 | let totalOutput = 0; 807 | 808 | for (let i = 0; i < contributions.length; i++) { 809 | totalOutput += contributions[i].linesOfCode; 810 | } 811 | ``` 812 | 813 | **Good:** 814 | 815 | ```ts 816 | const contributions = [ 817 | { 818 | name: 'Uncle Bobby', 819 | linesOfCode: 500 820 | }, { 821 | name: 'Suzie Q', 822 | linesOfCode: 1500 823 | }, { 824 | name: 'Jimmy Gosling', 825 | linesOfCode: 150 826 | }, { 827 | name: 'Gracie Hopper', 828 | linesOfCode: 1000 829 | } 830 | ]; 831 | 832 | const totalOutput = contributions 833 | .reduce((totalLines, output) => totalLines + output.linesOfCode, 0); 834 | ``` 835 | 836 | **[⬆ 맨 위로 이동](#목차)** 837 | 838 | ### 조건문을 캡슐화하세요 839 | 840 | **Bad:** 841 | 842 | ```ts 843 | if (subscription.isTrial || account.balance > 0) { 844 | // ... 845 | } 846 | ``` 847 | 848 | **Good:** 849 | 850 | ```ts 851 | function canActivateService(subscription: Subscription, account: Account) { 852 | return subscription.isTrial || account.balance > 0; 853 | } 854 | 855 | if (canActivateService(subscription, account)) { 856 | // ... 857 | } 858 | ``` 859 | 860 | **[⬆ 맨 위로 이동](#목차)** 861 | 862 | ### 부정 조건문을 피하세요 863 | 864 | **Bad:** 865 | 866 | ```ts 867 | function isEmailNotUsed(email: string): boolean { 868 | // ... 869 | } 870 | 871 | if (isEmailNotUsed(email)) { 872 | // ... 873 | } 874 | ``` 875 | 876 | **Good:** 877 | 878 | ```ts 879 | function isEmailUsed(email): boolean { 880 | // ... 881 | } 882 | 883 | if (!isEmailUsed(node)) { 884 | // ... 885 | } 886 | ``` 887 | 888 | **[⬆ 맨 위로 이동](#목차)** 889 | 890 | ### 조건문을 피하세요 891 | 892 | 불가능해보일 수 있습니다. 처음 이를 본 대부분의 사람들은 "대체 `if`문 없이 뭘 할 수 있나요?" 라고 반응합니다. 하지만 많은 경우에 다형성을 사용한다면 해결할 수 있습니다. 그 다음 반응으로는 "좋아요. 하지만 왜 그래야하죠?" 입니다. 이에 대한 해답은 우리가 이전에 배운 클린 코드 컨셉 중 "함수는 한 가지 일만 해야합니다" 입니다. `if`문이 있는 클래스와 함수가 있다면, 그 함수는 한 가지 이상의 일을 하고 있다는 것입니다. 함수는 한 가지 일만 해야한다는 것을 기억하세요. 893 | 894 | **Bad:** 895 | 896 | ```ts 897 | class Airplane { 898 | private type: string; 899 | // ... 900 | 901 | getCruisingAltitude() { 902 | switch (this.type) { 903 | case '777': 904 | return this.getMaxAltitude() - this.getPassengerCount(); 905 | case 'Air Force One': 906 | return this.getMaxAltitude(); 907 | case 'Cessna': 908 | return this.getMaxAltitude() - this.getFuelExpenditure(); 909 | default: 910 | throw new Error('Unknown airplane type.'); 911 | } 912 | } 913 | 914 | private getMaxAltitude(): number { 915 | // ... 916 | } 917 | } 918 | ``` 919 | 920 | **Good:** 921 | 922 | ```ts 923 | abstract class Airplane { 924 | protected getMaxAltitude(): number { 925 | // shared logic with subclasses ... 926 | } 927 | 928 | // ... 929 | } 930 | 931 | class Boeing777 extends Airplane { 932 | // ... 933 | getCruisingAltitude() { 934 | return this.getMaxAltitude() - this.getPassengerCount(); 935 | } 936 | } 937 | 938 | class AirForceOne extends Airplane { 939 | // ... 940 | getCruisingAltitude() { 941 | return this.getMaxAltitude(); 942 | } 943 | } 944 | 945 | class Cessna extends Airplane { 946 | // ... 947 | getCruisingAltitude() { 948 | return this.getMaxAltitude() - this.getFuelExpenditure(); 949 | } 950 | } 951 | ``` 952 | 953 | **[⬆ 맨 위로 이동](#목차)** 954 | 955 | ### 타입 체킹을 피하세요 956 | 957 | 타입스크립트는 자바스크립트의 엄격한 구문적 상위 집합이며 언어에 선택적인 정적 타입 검사 기능을 추가합니다. 958 | 타입스크립트의 기능을 최대한 활용하기 위해 항상 변수의 타입, 매개변수, 반환값의 타입을 지정하도록 하세요. 959 | 그렇게 하면 리팩토링이 매우 쉬워집니다. 960 | 961 | **Bad:** 962 | 963 | ```ts 964 | function travelToTexas(vehicle: Bicycle | Car) { 965 | if (vehicle instanceof Bicycle) { 966 | vehicle.pedal(currentLocation, new Location('texas')); 967 | } else if (vehicle instanceof Car) { 968 | vehicle.drive(currentLocation, new Location('texas')); 969 | } 970 | } 971 | ``` 972 | 973 | **Good:** 974 | 975 | ```ts 976 | type Vehicle = Bicycle | Car; 977 | 978 | function travelToTexas(vehicle: Vehicle) { 979 | vehicle.move(currentLocation, new Location('texas')); 980 | } 981 | ``` 982 | 983 | **[⬆ 맨 위로 이동](#목차)** 984 | 985 | ### 필요 이상으로 최적화하지 마세요 986 | 987 | 현대 브라우저는 런타임에서 많은 최적화를 수행합니다. 많은 시간을 최적화하는 데에 사용하고 있다면 시간 낭비입니다. 최적화가 부족한 부분을 확인할 수 있는 좋은 [자료](https://github.com/petkaantonov/bluebird/wiki/Optimization-killers)가 있습니다. 이것을 참조하여 최적화가 부족한 부분만 최적화해줄 수 있습니다. 988 | 989 | **Bad:** 990 | 991 | ```ts 992 | // 예전 브라우저에서는 캐시되지 않은 `list.length`를 사용한 각 순회는 비용이 많이 들 것입니다. 993 | // `list.length`의 재계산 때문입니다. 현대 브라우저에서는 이 부분이 최적화됩니다. 994 | for (let i = 0, len = list.length; i < len; i++) { 995 | // ... 996 | } 997 | ``` 998 | 999 | **Good:** 1000 | 1001 | ```ts 1002 | for (let i = 0; i < list.length; i++) { 1003 | // ... 1004 | } 1005 | ``` 1006 | 1007 | **[⬆ 맨 위로 이동](#목차)** 1008 | 1009 | ### 필요하지 않는 코드는 제거하세요 1010 | 1011 | 사용하지 않은 코드는 중복된 코드만큼 나쁩니다. 당신의 코드에서 이것을 유지할 이유는 없습니다. 1012 | 호출되지 않은 코드가 있다면 제거하세요! 지운 코드를 다시 확인할 필요가 있다면 버전 기록에서 볼 수 있습니다. 1013 | 1014 | **Bad:** 1015 | 1016 | ```ts 1017 | function oldRequestModule(url: string) { 1018 | // ... 1019 | } 1020 | 1021 | function requestModule(url: string) { 1022 | // ... 1023 | } 1024 | 1025 | const req = requestModule; 1026 | inventoryTracker('apples', req, 'www.inventory-awesome.io'); 1027 | ``` 1028 | 1029 | **Good:** 1030 | 1031 | ```ts 1032 | function requestModule(url: string) { 1033 | // ... 1034 | } 1035 | 1036 | const req = requestModule; 1037 | inventoryTracker('apples', req, 'www.inventory-awesome.io'); 1038 | ``` 1039 | 1040 | **[⬆ 맨 위로 이동](#목차)** 1041 | 1042 | ### `iterator`와 `generator`를 사용하세요 1043 | 1044 | 스트림과 같이 사용되는 데이터 콜렉션을 사용할 때는 `generator`와 `iterable`을 사용하세요. 1045 | 몇 가지의 좋은 이유가 있습니다: 1046 | 1047 | - 피호출자가 접근할 아이템 수를 결정한다는 의미에서 피호출자를 `generator` 구현으로부터 분리할 수 있습니다. 1048 | - 지연 실행, 아이템은 요구에 의해 스트림 처리될 수 있습니다. 1049 | - `for-of` 구문을 사용해 아이템을 순회하는 내장 지원이 있습니다. 1050 | - `iterable`은 최적화된 `iterator` 패턴을 구현할 수 있습니다. 1051 | 1052 | **Bad:** 1053 | 1054 | ```ts 1055 | function fibonacci(n: number): number[] { 1056 | if (n === 1) return [0]; 1057 | if (n === 2) return [0, 1]; 1058 | 1059 | const items: number[] = [0, 1]; 1060 | while (items.length < n) { 1061 | items.push(items[items.length - 2] + items[items.length - 1]); 1062 | } 1063 | 1064 | return items; 1065 | } 1066 | 1067 | function print(n: number) { 1068 | fibonacci(n).forEach(fib => console.log(fib)); 1069 | } 1070 | 1071 | // 피보나치 숫자의 첫 번째 10개 숫자를 출력합니다. 1072 | print(10); 1073 | ``` 1074 | 1075 | **Good:** 1076 | 1077 | ```ts 1078 | // 피보나치 숫자의 무한 스트림을 생성합니다. 1079 | // `generator`는 모든 숫자의 배열을 유지하고 있지 않습니다. 1080 | function* fibonacci(): IterableIterator { 1081 | let [a, b] = [0, 1]; 1082 | 1083 | while (true) { 1084 | yield a; 1085 | [a, b] = [b, a + b]; 1086 | } 1087 | } 1088 | 1089 | function print(n: number) { 1090 | let i = 0; 1091 | for (const fib of fibonacci()) { 1092 | if (i++ === n) break; 1093 | console.log(fib); 1094 | } 1095 | } 1096 | 1097 | // 피보나치 숫자의 첫 번째 10개 숫자를 출력합니다. 1098 | print(10); 1099 | ``` 1100 | 1101 | `map`, `slice`, `forEach` 등과 같은 메소드를 연결함으로써 네이티브 배열을 비슷한 방법으로 `iterable`로 작업할 수 있게 하는 라이브러리가 있습니다. 1102 | `iterable`의 발전된 조작의 사례를 위해 [itiriri](https://www.npmjs.com/package/itiriri)를 확인해주세요. (또는 비동기 `iterable`의 조작을 위해서 [itiriri-async](https://www.npmjs.com/package/itiriri-async)를 확인해주세요.) 1103 | 1104 | ```ts 1105 | import itiriri from 'itiriri'; 1106 | 1107 | function* fibonacci(): IterableIterator { 1108 | let [a, b] = [0, 1]; 1109 | 1110 | while (true) { 1111 | yield a; 1112 | [a, b] = [b, a + b]; 1113 | } 1114 | } 1115 | 1116 | itiriri(fibonacci()) 1117 | .take(10) 1118 | .forEach(fib => console.log(fib)); 1119 | ``` 1120 | 1121 | **[⬆ 맨 위로 이동](#목차)** 1122 | 1123 | ## 객체와 자료구조 1124 | 1125 | ### `getter`와 `setter`를 사용하세요 1126 | 1127 | 타입스크립트는 `getter`/`setter` 구문을 지원합니다. 1128 | 행동을 캡슐화한 객체에서 데이터를 접근하기 위해 `getter`와 `setter`를 사용하는 것은 객체에서 속성을 단순히 찾는 것보다 낫습니다. 1129 | "왜 그렇습니까?" 라고 물을 수 있습니다. 다음과 같은 이유가 있습니다: 1130 | 1131 | - 객체 속성을 얻는 것 이상으로 무언가를 더 하고 싶을 때, 코드 안에서 관련된 모든 접근자를 찾고 변경하지 않아도 됩니다. 1132 | - `set`을 사용할 때 검증 로직을 추가하는 것이 간단합니다. 1133 | - 내부의 API를 캡슐화할 수 있습니다. 1134 | - 값을 조회하고 설정할 때 로그를 기록하고 에러를 처리하는 것이 쉽습니다. 1135 | - 서버에서 객체 속성을 불러올 때 지연 로딩할 수 있습니다. 1136 | 1137 | **Bad:** 1138 | 1139 | ```ts 1140 | type BankAccount = { 1141 | balance: number; 1142 | // ... 1143 | } 1144 | 1145 | const value = 100; 1146 | const account: BankAccount = { 1147 | balance: 0, 1148 | // ... 1149 | }; 1150 | 1151 | if (value < 0) { 1152 | throw new Error('Cannot set negative balance.'); 1153 | } 1154 | 1155 | account.balance = value; 1156 | ``` 1157 | 1158 | **Good:** 1159 | 1160 | ```ts 1161 | class BankAccount { 1162 | private accountBalance: number = 0; 1163 | 1164 | get balance(): number { 1165 | return this.accountBalance; 1166 | } 1167 | 1168 | set balance(value: number) { 1169 | if (value < 0) { 1170 | throw new Error('Cannot set negative balance.'); 1171 | } 1172 | 1173 | this.accountBalance = value; 1174 | } 1175 | 1176 | // ... 1177 | } 1178 | 1179 | // 이제 `BankAccount`는 검증 로직을 캡슐화합니다. 1180 | // 명세가 바뀐다면, 추가적인 검증 규칙을 추가할 필요가 있습니다. 1181 | // 그 때, `setter` 구현부만 수정하면 됩니다. 1182 | // 관련있는 다른 코드는 변경할 필요가 없습니다. 1183 | const account = new BankAccount(); 1184 | account.balance = 100; 1185 | ``` 1186 | 1187 | **[⬆ 맨 위로 이동](#목차)** 1188 | 1189 | ### private/protected 멤버를 갖는 객체를 생성하세요 1190 | 1191 | 타입스크립트는 클래스 멤버를 위해 `public` *기본*, `protected`, `private` 접근자를 지원합니다. 1192 | 1193 | **Bad:** 1194 | 1195 | ```ts 1196 | class Circle { 1197 | radius: number; 1198 | 1199 | constructor(radius: number) { 1200 | this.radius = radius; 1201 | } 1202 | 1203 | perimeter() { 1204 | return 2 * Math.PI * this.radius; 1205 | } 1206 | 1207 | surface() { 1208 | return Math.PI * this.radius * this.radius; 1209 | } 1210 | } 1211 | ``` 1212 | 1213 | **Good:** 1214 | 1215 | ```ts 1216 | class Circle { 1217 | constructor(private readonly radius: number) { 1218 | } 1219 | 1220 | perimeter() { 1221 | return 2 * Math.PI * this.radius; 1222 | } 1223 | 1224 | surface() { 1225 | return Math.PI * this.radius * this.radius; 1226 | } 1227 | } 1228 | ``` 1229 | 1230 | **[⬆ 맨 위로 이동](#목차)** 1231 | 1232 | ### 불변성을 선호하세요 1233 | 1234 | 타입스크립트의 타입 시스템은 `interface`/`class`의 개별 속성을 *readonly*로 표시할 수 있습니다. 이를 통해 기능적인 방식으로 작업할 수 있습니다. (예상하지 않은 변조는 위험합니다.) 1235 | 더욱 나은 방법으로는 타입 `T`를 갖고 mapped types를 사용하여 모든 각 속성을 읽기 전용으로 표시하는 `Readonly` 내장 타입이 존재합니다. ([mapped types](https://www.typescriptlang.org/docs/handbook/advanced-types.html#mapped-types)를 확인하세요.) 1236 | 1237 | **Bad:** 1238 | 1239 | ```ts 1240 | interface Config { 1241 | host: string; 1242 | port: string; 1243 | db: string; 1244 | } 1245 | ``` 1246 | 1247 | **Good:** 1248 | 1249 | ```ts 1250 | interface Config { 1251 | readonly host: string; 1252 | readonly port: string; 1253 | readonly db: string; 1254 | } 1255 | ``` 1256 | 1257 | 배열의 경우, `ReadonlyArray`를 사용해서 읽기 전용의 배열을 생성할 수 있습니다. 1258 | 이것은 `push()`와 `fill()`과 같은 변경을 막습니다. 하지만 값 자체를 변경하지 않는 `concat()`, `slice()`과 같은 기능은 사용할 수 있습니다. 1259 | 1260 | **Bad:** 1261 | 1262 | ```ts 1263 | const array: number[] = [ 1, 3, 5 ]; 1264 | array = []; // 에러 1265 | array.push(100); // 배열은 변경될 것입니다. 1266 | ``` 1267 | 1268 | **Good:** 1269 | 1270 | ```ts 1271 | const array: ReadonlyArray = [ 1, 3, 5 ]; 1272 | array = []; // 에러 1273 | array.push(100); // 에러 1274 | ``` 1275 | 1276 | [TypeScript 3.4 is a bit easier](https://github.com/microsoft/TypeScript/wiki/What's-new-in-TypeScript#improvements-for-readonlyarray-and-readonly-tuples)에서 읽기 전용의 매개변수를 선언할 수 있습니다. 1277 | 1278 | ```ts 1279 | function hoge(args: readonly string[]) { 1280 | args.push(1); // 에러 1281 | } 1282 | ``` 1283 | 1284 | 리터럴 값을 위해 [const assertions](https://github.com/microsoft/TypeScript/wiki/What's-new-in-TypeScript#const-assertions)를 사용하세요. 1285 | 1286 | **Bad:** 1287 | 1288 | ```ts 1289 | const config = { 1290 | hello: 'world' 1291 | }; 1292 | config.hello = 'world'; // 값이 바뀝니다 1293 | 1294 | const array = [ 1, 3, 5 ]; 1295 | array[0] = 10; // 값이 바뀝니다 1296 | 1297 | // 쓸 수 있는 객체가 반환됩니다 1298 | function readonlyData(value: number) { 1299 | return { value }; 1300 | } 1301 | 1302 | const result = readonlyData(100); 1303 | result.value = 200; // 값이 바뀝니다 1304 | ``` 1305 | 1306 | **Good:** 1307 | 1308 | ```ts 1309 | // 읽기 전용 객체 1310 | const config = { 1311 | hello: 'world' 1312 | } as const; 1313 | config.hello = 'world'; // 에러 1314 | 1315 | // 읽기 전용 배열 1316 | const array = [ 1, 3, 5 ] as const; 1317 | array[0] = 10; // 에러 1318 | 1319 | // 읽기 전용 객체를 반활할 수 있습니다 1320 | function readonlyData(value: number) { 1321 | return { value } as const; 1322 | } 1323 | 1324 | const result = readonlyData(100); 1325 | result.value = 200; // 에러 1326 | ``` 1327 | 1328 | **[⬆ 맨 위로 이동](#목차)** 1329 | 1330 | ### 타입 vs 인터페이스 1331 | 1332 | 합집합 또는 교집합이 필요할 때 타입을 사용하세요. `extends` 또는 `implements`가 필요할 때 인터페이스를 사용하세요. 엄격한 규칙은 없지만 당신에게 맞는 방법을 사용하세요. 1333 | 타입스크립트에서 `type`과 `interface` 사이의 다른 점에 대해서 더 상세한 설명을 원한다면 이 [답변](https://stackoverflow.com/questions/37233735/typescript-interfaces-vs-types/54101543#54101543)을 참고하세요. 1334 | 1335 | **Bad:** 1336 | 1337 | ```ts 1338 | interface EmailConfig { 1339 | // ... 1340 | } 1341 | 1342 | interface DbConfig { 1343 | // ... 1344 | } 1345 | 1346 | interface Config { 1347 | // ... 1348 | } 1349 | 1350 | //... 1351 | 1352 | type Shape = { 1353 | // ... 1354 | } 1355 | ``` 1356 | 1357 | **Good:** 1358 | 1359 | ```ts 1360 | 1361 | type EmailConfig = { 1362 | // ... 1363 | } 1364 | 1365 | type DbConfig = { 1366 | // ... 1367 | } 1368 | 1369 | type Config = EmailConfig | DbConfig; 1370 | 1371 | // ... 1372 | 1373 | interface Shape { 1374 | // ... 1375 | } 1376 | 1377 | class Circle implements Shape { 1378 | // ... 1379 | } 1380 | 1381 | class Square implements Shape { 1382 | // ... 1383 | } 1384 | ``` 1385 | 1386 | **[⬆ 맨 위로 이동](#목차)** 1387 | 1388 | ## 클래스 1389 | 1390 | ### 클래스는 작아야 합니다 1391 | 1392 | 클래스의 크기는 책임에 의해 측정됩니다. *단일 책임 원칙*에 따르면 클래스는 작아야 합니다. 1393 | 1394 | **Bad:** 1395 | 1396 | ```ts 1397 | class Dashboard { 1398 | getLanguage(): string { /* ... */ } 1399 | setLanguage(language: string): void { /* ... */ } 1400 | showProgress(): void { /* ... */ } 1401 | hideProgress(): void { /* ... */ } 1402 | isDirty(): boolean { /* ... */ } 1403 | disable(): void { /* ... */ } 1404 | enable(): void { /* ... */ } 1405 | addSubscription(subscription: Subscription): void { /* ... */ } 1406 | removeSubscription(subscription: Subscription): void { /* ... */ } 1407 | addUser(user: User): void { /* ... */ } 1408 | removeUser(user: User): void { /* ... */ } 1409 | goToHomePage(): void { /* ... */ } 1410 | updateProfile(details: UserDetails): void { /* ... */ } 1411 | getVersion(): string { /* ... */ } 1412 | // ... 1413 | } 1414 | 1415 | ``` 1416 | 1417 | **Good:** 1418 | 1419 | ```ts 1420 | class Dashboard { 1421 | disable(): void { /* ... */ } 1422 | enable(): void { /* ... */ } 1423 | getVersion(): string { /* ... */ } 1424 | } 1425 | 1426 | // 다른 클래스에 남은 메소드를 이동시킴으로써 책임을 분산시키세요 1427 | // ... 1428 | ``` 1429 | 1430 | **[⬆ 맨 위로 이동](#목차)** 1431 | 1432 | ### 높은 응집도와 낮은 결합도 1433 | 1434 | 응집도는 클래스 멤버가 서로에게 연관되어 있는 정도를 정의합니다. 이상적으로, 클래스 안의 모든 필드는 각 메소드에 의해 사용되어야 합니다. 1435 | 그 때 우리는 클래스가 *최대한으로 응집되어있다*라고 말합니다. 이것은 항상 가능하지도 않고 권장하지 않습니다. 그러나 응집도를 높이는 것을 선호해야 합니다. 1436 | 1437 | 결합도는 두 클래스가 얼마나 서로에게 관련되어있거나 종속적인 정도를 뜻합니다. 하나의 클래스의 변경이 다른 클래스에게 영향을 주지 않는다면 그 클래스들의 결합도는 낮다고 말합니다. 1438 | 1439 | 좋은 소프트웨어 설계는 **높은 응집도**와 **낮은 결합도**를 가집니다. 1440 | 1441 | **Bad:** 1442 | 1443 | ```ts 1444 | class UserManager { 1445 | // Bad: 각 private 변수는 메소드의 하나 혹은 또 다른 그룹에 의해 사용됩니다. 1446 | // 클래스가 단일 책임 이상의 책임을 가지고 있다는 명백한 증거입니다. 1447 | // 사용자의 트랜잭션을 얻기 위해 서비스를 생성하기만 하면 되는 경우, 1448 | // 여전히 `emailSender` 인스턴스를 전달해야 합니다. 1449 | constructor( 1450 | private readonly db: Database, 1451 | private readonly emailSender: EmailSender) { 1452 | } 1453 | 1454 | async getUser(id: number): Promise { 1455 | return await db.users.findOne({ id }); 1456 | } 1457 | 1458 | async getTransactions(userId: number): Promise { 1459 | return await db.transactions.find({ userId }); 1460 | } 1461 | 1462 | async sendGreeting(): Promise { 1463 | await emailSender.send('Welcome!'); 1464 | } 1465 | 1466 | async sendNotification(text: string): Promise { 1467 | await emailSender.send(text); 1468 | } 1469 | 1470 | async sendNewsletter(): Promise { 1471 | // ... 1472 | } 1473 | } 1474 | ``` 1475 | 1476 | **Good:** 1477 | 1478 | ```ts 1479 | class UserService { 1480 | constructor(private readonly db: Database) { 1481 | } 1482 | 1483 | async getUser(id: number): Promise { 1484 | return await this.db.users.findOne({ id }); 1485 | } 1486 | 1487 | async getTransactions(userId: number): Promise { 1488 | return await this.db.transactions.find({ userId }); 1489 | } 1490 | } 1491 | 1492 | class UserNotifier { 1493 | constructor(private readonly emailSender: EmailSender) { 1494 | } 1495 | 1496 | async sendGreeting(): Promise { 1497 | await this.emailSender.send('Welcome!'); 1498 | } 1499 | 1500 | async sendNotification(text: string): Promise { 1501 | await this.emailSender.send(text); 1502 | } 1503 | 1504 | async sendNewsletter(): Promise { 1505 | // ... 1506 | } 1507 | } 1508 | ``` 1509 | 1510 | **[⬆ 맨 위로 이동](#목차)** 1511 | 1512 | ### 상속(inheritance)보다 조합(composition)을 사용하세요 1513 | 1514 | Gang of Four의 [디자인 패턴](https://en.wikipedia.org/wiki/Design_Patterns)에 나와있듯이 할 수 있는 대로 *상속보다 조합을 사용해야 합니다.* 상속과 조합을 사용해야 하는 좋은 이유들은 각각 많습니다. 이 교훈에서 중요한 점은 당신의 마음이 본능적으로 상속을 추구한다면, 조합이 당신의 문제를 더 좋게 해결할 수 있을지 고민해보라는 것입니다. 어떤 경우에는 더 좋을 수 있습니다. 1515 | 1516 | 당신은 "언제 상속을 사용해야 할까요?" 라고 의문점을 가질 것입니다. 그것은 당면한 문제에 달려 있습니다. 조합보다 상속이 더 좋은 경우가 아래에 있습니다: 1517 | 1518 | 1. "has-a" 관계가 아닌 "is-a" 관계일 때 (사람->동물 vs 사용자->사용자 정보) 1519 | 1520 | 2. 기반이 되는 클래스로부터 코드를 재사용할 수 있을 때 (사람은 모든 동물처럼 움직일 수 있습니다.) 1521 | 1522 | 3. 기반이 되는 클래스를 변경하여 파생된 클래스를 전체적으로 변경하려는 경우 (모든 동물은 움직일 때 칼로리가 소비됩니다.) 1523 | 1524 | **Bad:** 1525 | 1526 | ```ts 1527 | class Employee { 1528 | constructor( 1529 | private readonly name: string, 1530 | private readonly email: string) { 1531 | } 1532 | 1533 | // ... 1534 | } 1535 | 1536 | // `Employee`가 세금 데이터를 가지기 때문에 나쁜 에입니다. `EmployeeTaxData`는 `Employee`의 타입이 아닙니다. 1537 | class EmployeeTaxData extends Employee { 1538 | constructor( 1539 | name: string, 1540 | email: string, 1541 | private readonly ssn: string, 1542 | private readonly salary: number) { 1543 | super(name, email); 1544 | } 1545 | 1546 | // ... 1547 | } 1548 | ``` 1549 | 1550 | **Good:** 1551 | 1552 | ```ts 1553 | class Employee { 1554 | private taxData: EmployeeTaxData; 1555 | 1556 | constructor( 1557 | private readonly name: string, 1558 | private readonly email: string) { 1559 | } 1560 | 1561 | setTaxData(ssn: string, salary: number): Employee { 1562 | this.taxData = new EmployeeTaxData(ssn, salary); 1563 | return this; 1564 | } 1565 | 1566 | // ... 1567 | } 1568 | 1569 | class EmployeeTaxData { 1570 | constructor( 1571 | public readonly ssn: string, 1572 | public readonly salary: number) { 1573 | } 1574 | 1575 | // ... 1576 | } 1577 | ``` 1578 | 1579 | **[⬆ 맨 위로 이동](#목차)** 1580 | 1581 | ### 메소드 체이닝을 사용하세요 1582 | 1583 | 이 패턴은 매우 유용하고 많은 라이브러리에서 공통적으로 사용하고 있습니다. 이것은 당신의 코드를 표현력이 있게 해주고 덜 장황하게 해줍니다. 이러한 이유로 메소드 체이닝을 사용해서 당신의 코드가 얼마나 명료해지는지 살펴보시길 바랍니다. 1584 | 1585 | **Bad:** 1586 | 1587 | ```ts 1588 | class QueryBuilder { 1589 | private collection: string; 1590 | private pageNumber: number = 1; 1591 | private itemsPerPage: number = 100; 1592 | private orderByFields: string[] = []; 1593 | 1594 | from(collection: string): void { 1595 | this.collection = collection; 1596 | } 1597 | 1598 | page(number: number, itemsPerPage: number = 100): void { 1599 | this.pageNumber = number; 1600 | this.itemsPerPage = itemsPerPage; 1601 | } 1602 | 1603 | orderBy(...fields: string[]): void { 1604 | this.orderByFields = fields; 1605 | } 1606 | 1607 | build(): Query { 1608 | // ... 1609 | } 1610 | } 1611 | 1612 | // ... 1613 | 1614 | const queryBuilder = new QueryBuilder(); 1615 | queryBuilder.from('users'); 1616 | queryBuilder.page(1, 100); 1617 | queryBuilder.orderBy('firstName', 'lastName'); 1618 | 1619 | const query = queryBuilder.build(); 1620 | ``` 1621 | 1622 | **Good:** 1623 | 1624 | ```ts 1625 | class QueryBuilder { 1626 | private collection: string; 1627 | private pageNumber: number = 1; 1628 | private itemsPerPage: number = 100; 1629 | private orderByFields: string[] = []; 1630 | 1631 | from(collection: string): this { 1632 | this.collection = collection; 1633 | return this; 1634 | } 1635 | 1636 | page(number: number, itemsPerPage: number = 100): this { 1637 | this.pageNumber = number; 1638 | this.itemsPerPage = itemsPerPage; 1639 | return this; 1640 | } 1641 | 1642 | orderBy(...fields: string[]): this { 1643 | this.orderByFields = fields; 1644 | return this; 1645 | } 1646 | 1647 | build(): Query { 1648 | // ... 1649 | } 1650 | } 1651 | 1652 | // ... 1653 | 1654 | const query = new QueryBuilder() 1655 | .from('users') 1656 | .page(1, 100) 1657 | .orderBy('firstName', 'lastName') 1658 | .build(); 1659 | ``` 1660 | 1661 | **[⬆ 맨 위로 이동](#목차)** 1662 | 1663 | ## SOLID 1664 | 1665 | ### 단일 책임 원칙 (SRP) 1666 | 1667 | 클린 코드에서 말하듯이, "클래스를 변경할 때는 단 한 가지 이유만 존재해야 합니다". 여행갈 때 가방 하나에 많은 것을 챙기는 것과 같이, 클래스를 많은 기능으로 꽉 채우고 싶은 유혹이 있습니다. 이러한 문제는 당신의 클래스가 개념적으로 응집력이 있지 않으며 변경될 많은 이유가 존재한다는 것을 말합니다. 클래스를 변경하는 많은 시간을 줄이는 것은 중요합니다. 왜냐하면 너무 많은 기능이 한 클래스에 있고 그 안에서 하나의 기능을 수정한다면, 다른 종속된 모듈에 어떻게 영향을 줄지 이해하는 것이 어렵기 때문입니다. 1668 | 1669 | **Bad:** 1670 | 1671 | ```ts 1672 | class UserSettings { 1673 | constructor(private readonly user: User) { 1674 | } 1675 | 1676 | changeSettings(settings: UserSettings) { 1677 | if (this.verifyCredentials()) { 1678 | // ... 1679 | } 1680 | } 1681 | 1682 | verifyCredentials() { 1683 | // ... 1684 | } 1685 | } 1686 | ``` 1687 | 1688 | **Good:** 1689 | 1690 | ```ts 1691 | class UserAuth { 1692 | constructor(private readonly user: User) { 1693 | } 1694 | 1695 | verifyCredentials() { 1696 | // ... 1697 | } 1698 | } 1699 | 1700 | 1701 | class UserSettings { 1702 | private readonly auth: UserAuth; 1703 | 1704 | constructor(private readonly user: User) { 1705 | this.auth = new UserAuth(user); 1706 | } 1707 | 1708 | changeSettings(settings: UserSettings) { 1709 | if (this.auth.verifyCredentials()) { 1710 | // ... 1711 | } 1712 | } 1713 | } 1714 | ``` 1715 | 1716 | **[⬆ 맨 위로 이동](#목차)** 1717 | 1718 | ### 개방 폐쇄 원칙 (OCP) 1719 | 1720 | Bertrand Meyer가 말했듯이, "소프트웨어 엔터티(클래스, 모듈, 함수 등)는 상속에 개방되어 있습니다. 하지만 수정에는 폐쇄되어 있습니다." 이것이 무엇을 의미할까요? 이 원칙은 기본적으로 기존의 코드를 변경하지 않고 새로운 기능을 추가할 수 있도록 하는 것을 명시합니다. 1721 | 1722 | **Bad:** 1723 | 1724 | ```ts 1725 | class AjaxAdapter extends Adapter { 1726 | constructor() { 1727 | super(); 1728 | } 1729 | 1730 | // ... 1731 | } 1732 | 1733 | class NodeAdapter extends Adapter { 1734 | constructor() { 1735 | super(); 1736 | } 1737 | 1738 | // ... 1739 | } 1740 | 1741 | class HttpRequester { 1742 | constructor(private readonly adapter: Adapter) { 1743 | } 1744 | 1745 | async fetch(url: string): Promise { 1746 | if (this.adapter instanceof AjaxAdapter) { 1747 | const response = await makeAjaxCall(url); 1748 | // response 값을 변경하고 반환합니다. 1749 | } else if (this.adapter instanceof NodeAdapter) { 1750 | const response = await makeHttpCall(url); 1751 | // response 값을 변경하고 반환합니다. 1752 | } 1753 | } 1754 | } 1755 | 1756 | function makeAjaxCall(url: string): Promise { 1757 | // 서버에 요청하고 프로미스를 반환합니다. 1758 | } 1759 | 1760 | function makeHttpCall(url: string): Promise { 1761 | // 서버에 요청하고 프로미스를 반환합니다. 1762 | } 1763 | ``` 1764 | 1765 | **Good:** 1766 | 1767 | ```ts 1768 | abstract class Adapter { 1769 | abstract async request(url: string): Promise; 1770 | 1771 | // 하위 클래스와 공유하는 코드 ... 1772 | } 1773 | 1774 | class AjaxAdapter extends Adapter { 1775 | constructor() { 1776 | super(); 1777 | } 1778 | 1779 | async request(url: string): Promise{ 1780 | // 서버에 요청하고 프로미스를 반환합니다. 1781 | } 1782 | 1783 | // ... 1784 | } 1785 | 1786 | class NodeAdapter extends Adapter { 1787 | constructor() { 1788 | super(); 1789 | } 1790 | 1791 | async request(url: string): Promise{ 1792 | // 서버에 요청하고 프로미스를 반환합니다. 1793 | } 1794 | 1795 | // ... 1796 | } 1797 | 1798 | class HttpRequester { 1799 | constructor(private readonly adapter: Adapter) { 1800 | } 1801 | 1802 | async fetch(url: string): Promise { 1803 | const response = await this.adapter.request(url); 1804 | // response 값을 변경하고 반환합니다. 1805 | } 1806 | } 1807 | ``` 1808 | 1809 | **[⬆ 맨 위로 이동](#목차)** 1810 | 1811 | ### 리스코프 치환 원칙 (LSP) 1812 | 1813 | 매우 단순한 개념을 뜻하는 어려워보이는 용어입니다. "만약 S가 T의 하위 타입이라면, T 타입의 객체는 S 타입의 객체로 대체될 수 있습니다. (예: S 타입 객체는 T 타입 객체로 치환될 수도 있습니다.) 이는 프로그램이 갖추어야할 속성(정확성, 수행되는 작업 등)을 변경하지 않아도 대체될 수 있습니다." 더욱 어려워보이는 정의입니다. 1814 | 1815 | 이를 위한 최고의 설명은 다음과 같습니다. 만약 부모 클래스와 자식 클래스가 있다면, 부모 클래스와 자식 클래스는 잘못된 결과 없이 서로 교환하여 사용될 수 있습니다. 여전히 혼란스러울 수 있습니다. 고전적인 정사각형-직사각형 예제를 살펴보세요. 수학적으로, 정사각형은 직사각형입니다. 그러나 상속을 통해 "is-a" 관계로 설계한다면, 당신은 곤경에 빠질 수 있습니다. 1816 | 1817 | **Bad:** 1818 | 1819 | ```ts 1820 | class Rectangle { 1821 | constructor( 1822 | protected width: number = 0, 1823 | protected height: number = 0) { 1824 | 1825 | } 1826 | 1827 | setColor(color: string): this { 1828 | // ... 1829 | } 1830 | 1831 | render(area: number) { 1832 | // ... 1833 | } 1834 | 1835 | setWidth(width: number): this { 1836 | this.width = width; 1837 | return this; 1838 | } 1839 | 1840 | setHeight(height: number): this { 1841 | this.height = height; 1842 | return this; 1843 | } 1844 | 1845 | getArea(): number { 1846 | return this.width * this.height; 1847 | } 1848 | } 1849 | 1850 | class Square extends Rectangle { 1851 | setWidth(width: number): this { 1852 | this.width = width; 1853 | this.height = width; 1854 | return this; 1855 | } 1856 | 1857 | setHeight(height: number): this { 1858 | this.width = height; 1859 | this.height = height; 1860 | return this; 1861 | } 1862 | } 1863 | 1864 | function renderLargeRectangles(rectangles: Rectangle[]) { 1865 | rectangles.forEach((rectangle) => { 1866 | const area = rectangle 1867 | .setWidth(4) 1868 | .setHeight(5) 1869 | .getArea(); // BAD: `Square` 클래스에서는 25를 반환합니다. 20이 반환되어야 합니다. 1870 | rectangle.render(area); 1871 | }); 1872 | } 1873 | 1874 | const rectangles = [new Rectangle(), new Rectangle(), new Square()]; 1875 | renderLargeRectangles(rectangles); 1876 | ``` 1877 | 1878 | **Good:** 1879 | 1880 | ```ts 1881 | abstract class Shape { 1882 | setColor(color: string): this { 1883 | // ... 1884 | } 1885 | 1886 | render(area: number) { 1887 | // ... 1888 | } 1889 | 1890 | abstract getArea(): number; 1891 | } 1892 | 1893 | class Rectangle extends Shape { 1894 | constructor( 1895 | private readonly width = 0, 1896 | private readonly height = 0) { 1897 | super(); 1898 | } 1899 | 1900 | getArea(): number { 1901 | return this.width * this.height; 1902 | } 1903 | } 1904 | 1905 | class Square extends Shape { 1906 | constructor(private readonly length: number) { 1907 | super(); 1908 | } 1909 | 1910 | getArea(): number { 1911 | return this.length * this.length; 1912 | } 1913 | } 1914 | 1915 | function renderLargeShapes(shapes: Shape[]) { 1916 | shapes.forEach((shape) => { 1917 | const area = shape.getArea(); 1918 | shape.render(area); 1919 | }); 1920 | } 1921 | 1922 | const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)]; 1923 | renderLargeShapes(shapes); 1924 | ``` 1925 | 1926 | **[⬆ 맨 위로 이동](#목차)** 1927 | 1928 | ### 인터페이스 분리 원칙 (ISP) 1929 | 1930 | 인터페이스 분리 원칙은 "클라이언트는 사용하지 않는 인터페이스에 의존하지 않는다" 라는 것입니다. 이 원칙은 단일 책임 원칙과 많은 관련이 있습니다. 1931 | 이 말의 뜻은 클라이언트가 노출된 메소드를 사용하는 대신에 전체 파이를 얻지 않는 방식으로 추상화를 설계해야 한다는 것입니다. 그것은 또한 클라이언트에게 클라이언트가 실제로 필요하지 않은 메소드의 구현을 강요하는 것도 포함합니다. 1932 | 1933 | **Bad:** 1934 | 1935 | ```ts 1936 | interface SmartPrinter { 1937 | print(); 1938 | fax(); 1939 | scan(); 1940 | } 1941 | 1942 | class AllInOnePrinter implements SmartPrinter { 1943 | print() { 1944 | // ... 1945 | } 1946 | 1947 | fax() { 1948 | // ... 1949 | } 1950 | 1951 | scan() { 1952 | // ... 1953 | } 1954 | } 1955 | 1956 | class EconomicPrinter implements SmartPrinter { 1957 | print() { 1958 | // ... 1959 | } 1960 | 1961 | fax() { 1962 | throw new Error('Fax not supported.'); 1963 | } 1964 | 1965 | scan() { 1966 | throw new Error('Scan not supported.'); 1967 | } 1968 | } 1969 | ``` 1970 | 1971 | **Good:** 1972 | 1973 | ```ts 1974 | interface Printer { 1975 | print(); 1976 | } 1977 | 1978 | interface Fax { 1979 | fax(); 1980 | } 1981 | 1982 | interface Scanner { 1983 | scan(); 1984 | } 1985 | 1986 | class AllInOnePrinter implements Printer, Fax, Scanner { 1987 | print() { 1988 | // ... 1989 | } 1990 | 1991 | fax() { 1992 | // ... 1993 | } 1994 | 1995 | scan() { 1996 | // ... 1997 | } 1998 | } 1999 | 2000 | class EconomicPrinter implements Printer { 2001 | print() { 2002 | // ... 2003 | } 2004 | } 2005 | ``` 2006 | 2007 | **[⬆ 맨 위로 이동](#목차)** 2008 | 2009 | ### 의존성 역전 원칙 (DIP) 2010 | 2011 | 이 원칙은 두 가지 필수적인 사항을 명시합니다: 2012 | 2013 | 1. 상위 레벨의 모듈은 하위 레벨의 모듈에 의존하지 않아야 합니다. 두 모듈은 모두 추상화에 의존해야합니다. 2014 | 2015 | 2. 추상화는 세부사항에 의존하지 않아야 합니다. 세부사항은 추상화에 의존해야 합니다. 2016 | 2017 | 처음에 바로 이해하기는 어려울 수 있습니다. Angular를 사용해봤다면, 의존성 주입(DI)의 형태 안에서 이 원칙의 구현을 확인해봤을 것입니다. 동일한 개념은 아니지만, DIP는 상위 레벨의 모듈이 하위 레벨의 모듈의 세부사항에 접근하고 설정하지 못하도록 지킵니다. DI를 통해서도 마찬가지로 성취할 수 있습니다. 이것의 큰 장점은 모듈 사이의 결합도를 줄일 수 있다는 것입니다. 결합도는 코드를 리팩토링하기 어렵게 하기 때문에 매우 나쁜 개발 패턴입니다. 2018 | 2019 | DIP는 주로 IoC 컨테이너를 사용함으로써 달성됩니다. 타입스크립트를 위한 강력한 IoC 컨테이너의 예제는 [InversifyJs](https://www.npmjs.com/package/inversify)입니다. 2020 | 2021 | **Bad:** 2022 | 2023 | ```ts 2024 | import { readFile as readFileCb } from 'fs'; 2025 | import { promisify } from 'util'; 2026 | 2027 | const readFile = promisify(readFileCb); 2028 | 2029 | type ReportData = { 2030 | // .. 2031 | } 2032 | 2033 | class XmlFormatter { 2034 | parse(content: string): T { 2035 | // XML 문자열을 T 객체로 변환 2036 | } 2037 | } 2038 | 2039 | class ReportReader { 2040 | 2041 | // BAD: 특정 요청의 구현에 의존하는 것을 만들었습니다. 2042 | // `parse` 메소드에 의존하는 `ReportReader`를 만들어야 합니다. 2043 | private readonly formatter = new XmlFormatter(); 2044 | 2045 | async read(path: string): Promise { 2046 | const text = await readFile(path, 'UTF8'); 2047 | return this.formatter.parse(text); 2048 | } 2049 | } 2050 | 2051 | // ... 2052 | const reader = new ReportReader(); 2053 | await report = await reader.read('report.xml'); 2054 | ``` 2055 | 2056 | **Good:** 2057 | 2058 | ```ts 2059 | import { readFile as readFileCb } from 'fs'; 2060 | import { promisify } from 'util'; 2061 | 2062 | const readFile = promisify(readFileCb); 2063 | 2064 | type ReportData = { 2065 | // .. 2066 | } 2067 | 2068 | interface Formatter { 2069 | parse(content: string): T; 2070 | } 2071 | 2072 | class XmlFormatter implements Formatter { 2073 | parse(content: string): T { 2074 | // XML 문자열을 T 객체로 변환 2075 | } 2076 | } 2077 | 2078 | 2079 | class JsonFormatter implements Formatter { 2080 | parse(content: string): T { 2081 | // JSON 문자열을 T 객체로 변환 2082 | } 2083 | } 2084 | 2085 | class ReportReader { 2086 | constructor(private readonly formatter: Formatter) { 2087 | } 2088 | 2089 | async read(path: string): Promise { 2090 | const text = await readFile(path, 'UTF8'); 2091 | return this.formatter.parse(text); 2092 | } 2093 | } 2094 | 2095 | // ... 2096 | const reader = new ReportReader(new XmlFormatter()); 2097 | await report = await reader.read('report.xml'); 2098 | 2099 | // 또는 json 보고서가 필요한 경우 2100 | const reader = new ReportReader(new JsonFormatter()); 2101 | await report = await reader.read('report.json'); 2102 | ``` 2103 | 2104 | **[⬆ 맨 위로 이동](#목차)** 2105 | 2106 | ## 테스트 2107 | 2108 | 테스트는 배포보다 중요합니다. 테스트가 없거나 부족한 경우, 코드를 배포할 때마다 당신은 어떤 것이 작동하지 않을지 확실하지 않을 것입니다. 2109 | 적절한 양의 테스트를 구성하는 것은 당신의 팀에게 달려있지만, (모든 문장과 브랜치에서) 100%의 커버리지를 가진다면 매우 높은 자신감과 마음의 평화를 얻을 것입니다. 이는 훌륭한 테스트 프레임워크뿐만 아니라, 좋은 [커버리지 도구](https://github.com/gotwarlost/istanbul)를 사용해야 한다는 것을 의미합니다. 2110 | 2111 | 테스트를 작성하지 않을 이유는 없습니다. 타입스크립트의 타입을 지원하는 [많은 양의 좋은 자바스크립트 테스트 프레임워크](http://jstherightway.org/#testing-tools)가 있으므로 당신의 팀이 선호하는 것을 찾아 사용하세요. 당신의 팀에 적합한 테스트 프레임워크를 찾았다면, 당신이 만드는 모든 새로운 기능/모듈을 위한 테스트를 항상 작성하는 것을 목표로 하세요. 테스트 기반 개발(TDD)이 당신이 선호하는 방법이라면, 매우 좋습니다. 하지만 중요한 건 어떤 기능을 만들거나 기존의 것을 리팩토링하기 전에 목표하는 커버리지를 달성하는 것입니다. 2112 | 2113 | ### TDD의 세 가지 법칙 2114 | 2115 | 1. 실패하는 단위 테스트를 작성하기 전에는 실제 코드를 작성하지 마세요. 2116 | 2117 | 2. 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성하세요. 2118 | 2119 | 3. 실패하는 단위 테스트를 통과할 정도로만 실제 코드를 작성하세요. 2120 | 2121 | **[⬆ 맨 위로 이동](#목차)** 2122 | 2123 | ### F.I.R.S.T 규칙 2124 | 2125 | 명료한 테스트는 다음 규칙을 따라야 합니다: 2126 | 2127 | - **Fast** 테스트는 빈번하게 실행되므로 빨라야 합니다. 2128 | 2129 | - **Independent** 테스트는 서로 종속적이지 않습니다. 독립적으로 실행하든지 순서 상관없이 모두 실행하든지 동일한 결과가 나와야 합니다. 2130 | 2131 | - **Repeatable** 테스트는 어떤 환경에서든 반복될 수 있습니다. 테스트가 실패하는데에 이유가 없어야 합니다. 2132 | 2133 | - **Self-Validating** 테스트는 *통과* 혹은 *실패*로 답해야 합니다. 테스트가 통과되었다면 로그 파일을 보며 비교할 필요는 없습니다. 2134 | 2135 | - **Timely** 단위 테스트는 실제 코드를 작성하기 전에 작성해야 합니다. 실제 코드를 작성한 후에 테스트를 작성한다면, 테스트를 작성하는 것이 너무 고단하게 느껴질 것입니다. 2136 | 2137 | **[⬆ 맨 위로 이동](#목차)** 2138 | 2139 | ### 테스트 하나에 하나의 개념을 작성하세요 2140 | 2141 | 또한, 테스트는 *단일 책임 원칙*을 따라야 합니다. 단위 테스트 하나당 하나의 assert 구문을 작성하세요. 2142 | 2143 | **Bad:** 2144 | 2145 | ```ts 2146 | import { assert } from 'chai'; 2147 | 2148 | describe('AwesomeDate', () => { 2149 | it('handles date boundaries', () => { 2150 | let date: AwesomeDate; 2151 | 2152 | date = new AwesomeDate('1/1/2015'); 2153 | assert.equal('1/31/2015', date.addDays(30)); 2154 | 2155 | date = new AwesomeDate('2/1/2016'); 2156 | assert.equal('2/29/2016', date.addDays(28)); 2157 | 2158 | date = new AwesomeDate('2/1/2015'); 2159 | assert.equal('3/1/2015', date.addDays(28)); 2160 | }); 2161 | }); 2162 | ``` 2163 | 2164 | **Good:** 2165 | 2166 | ```ts 2167 | import { assert } from 'chai'; 2168 | 2169 | describe('AwesomeDate', () => { 2170 | it('handles 30-day months', () => { 2171 | const date = new AwesomeDate('1/1/2015'); 2172 | assert.equal('1/31/2015', date.addDays(30)); 2173 | }); 2174 | 2175 | it('handles leap year', () => { 2176 | const date = new AwesomeDate('2/1/2016'); 2177 | assert.equal('2/29/2016', date.addDays(28)); 2178 | }); 2179 | 2180 | it('handles non-leap year', () => { 2181 | const date = new AwesomeDate('2/1/2015'); 2182 | assert.equal('3/1/2015', date.addDays(28)); 2183 | }); 2184 | }); 2185 | ``` 2186 | 2187 | **[⬆ 맨 위로 이동](#목차)** 2188 | 2189 | ### 테스트의 이름은 테스트의 의도가 드러나야 합니다 2190 | 2191 | 테스트가 실패할 때, 테스트의 이름은 어떤 것이 잘못되었는지 볼 수 있는 첫 번째 표시입니다. 2192 | 2193 | **Bad:** 2194 | 2195 | ```ts 2196 | describe('Calendar', () => { 2197 | it('2/29/2020', () => { 2198 | // ... 2199 | }); 2200 | 2201 | it('throws', () => { 2202 | // ... 2203 | }); 2204 | }); 2205 | ``` 2206 | 2207 | **Good:** 2208 | 2209 | ```ts 2210 | describe('Calendar', () => { 2211 | it('should handle leap year', () => { 2212 | // ... 2213 | }); 2214 | 2215 | it('should throw when format is invalid', () => { 2216 | // ... 2217 | }); 2218 | }); 2219 | ``` 2220 | 2221 | **[⬆ 맨 위로 이동](#목차)** 2222 | 2223 | ## 동시성 2224 | 2225 | ### 프로미스 vs 콜백 2226 | 2227 | 콜백은 명료하지 않고, 지나친 양의 중첩된 *콜백 지옥*을 유발할 수 있습니다. 2228 | 콜백 방식을 사용하고 있는 기존의 함수를 프로미스를 반환하는 함수로 변환시켜주는 유틸리티 라이브러리가 있습니다. 2229 | (Node.js를 사용한다면 [`util.promisify`](https://nodejs.org/dist/latest-v8.x/docs/api/util.html#util_util_promisify_original)를 확인해주세요. 일반적인 목적이라면 [pify](https://www.npmjs.com/package/pify), [es6-promisify](https://www.npmjs.com/package/es6-promisify)를 확인해주세요.) 2230 | 2231 | **Bad:** 2232 | 2233 | ```ts 2234 | import { get } from 'request'; 2235 | import { writeFile } from 'fs'; 2236 | 2237 | function downloadPage(url: string, saveTo: string, callback: (error: Error, content?: string) => void) { 2238 | get(url, (error, response) => { 2239 | if (error) { 2240 | callback(error); 2241 | } else { 2242 | writeFile(saveTo, response.body, (error) => { 2243 | if (error) { 2244 | callback(error); 2245 | } else { 2246 | callback(null, response.body); 2247 | } 2248 | }); 2249 | } 2250 | }); 2251 | } 2252 | 2253 | downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html', (error, content) => { 2254 | if (error) { 2255 | console.error(error); 2256 | } else { 2257 | console.log(content); 2258 | } 2259 | }); 2260 | ``` 2261 | 2262 | **Good:** 2263 | 2264 | ```ts 2265 | import { get } from 'request'; 2266 | import { writeFile } from 'fs'; 2267 | import { promisify } from 'util'; 2268 | 2269 | const write = promisify(writeFile); 2270 | 2271 | function downloadPage(url: string, saveTo: string): Promise { 2272 | return get(url) 2273 | .then(response => write(saveTo, response)); 2274 | } 2275 | 2276 | downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html') 2277 | .then(content => console.log(content)) 2278 | .catch(error => console.error(error)); 2279 | ``` 2280 | 2281 | 프로미스는 코드를 더욱 간결하게 해주는 몇몇의 헬퍼 메소드를 지원합니다: 2282 | 2283 | | 패턴 | 설명 | 2284 | | ------------------------ | ----------------------------------------- | 2285 | | `Promise.resolve(value)` | 해결(resolve)된 프로미스로 값을 변환함. | 2286 | | `Promise.reject(error)` | 거부(reject)된 프로미스로 에러를 변환함. | 2287 | | `Promise.all(promises)` | 전달된 모든 프로미스가 이행한 값의 배열을 이행하는 새 프로미스 객체를 반환하거나 거부된 첫 번째 프로미스의 이유로 거부함. | 2288 | | `Promise.race(promises)`| 전달된 프로미스의 배열에서 가장 먼저 완료된 결과/에러로 이행/거부된 새 프로미스 객체를 반환함. | 2289 | 2290 | `Promise.all`는 병렬적으로 작업을 수행할 필요가 있을 때 유용합니다. `Promise.race`는 프로미스를 위한 타임아웃과 같은 것을 구현하는 것을 쉽게 할 수 있도록 도와줍니다. 2291 | 2292 | **[⬆ 맨 위로 이동](#목차)** 2293 | 2294 | ### 프로미스보다 `async`/`await`가 더 명료합니다 2295 | 2296 | `async`/`await` 구문을 사용하면 연결된 프로미스 구문보다 훨씬 더 명료하고 이해하기 쉬운 코드를 작성할 수 있습니다. `async` 키워드가 앞에 붙여진 함수는 `await` 키워드에서 코드의 실행을 멈춘다는 것을 자바스크립트 런타임에게 알려줍니다. 2297 | 2298 | **Bad:** 2299 | 2300 | ```ts 2301 | import { get } from 'request'; 2302 | import { writeFile } from 'fs'; 2303 | import { promisify } from 'util'; 2304 | 2305 | const write = util.promisify(writeFile); 2306 | 2307 | function downloadPage(url: string, saveTo: string): Promise { 2308 | return get(url).then(response => write(saveTo, response)); 2309 | } 2310 | 2311 | downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html') 2312 | .then(content => console.log(content)) 2313 | .catch(error => console.error(error)); 2314 | ``` 2315 | 2316 | **Good:** 2317 | 2318 | ```ts 2319 | import { get } from 'request'; 2320 | import { writeFile } from 'fs'; 2321 | import { promisify } from 'util'; 2322 | 2323 | const write = promisify(writeFile); 2324 | 2325 | async function downloadPage(url: string, saveTo: string): Promise { 2326 | const response = await get(url); 2327 | await write(saveTo, response); 2328 | return response; 2329 | } 2330 | 2331 | // somewhere in an async function 2332 | try { 2333 | const content = await downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html'); 2334 | console.log(content); 2335 | } catch (error) { 2336 | console.error(error); 2337 | } 2338 | ``` 2339 | 2340 | **[⬆ 맨 위로 이동](#목차)** 2341 | 2342 | ## 에러 처리 2343 | 2344 | 에러를 던지는 것은 좋은 것입니다! 에러를 던진다는 것은 런타임이 당신의 프로그램에서 뭔가 잘못되었을 때 식별하고 현재 스택에서 함수 실행을 멈추고, (노드에서) 프로세스를 종료하며, 스택 트레이스를 콘솔에 보여줌으로써 당신에게 해당 에러를 알려주는 것을 의미합니다. 2345 | 2346 | ### `throw` 또는 `reject` 구문에서 항상 `Error` 타입을 사용하세요 2347 | 2348 | 타입스크립트뿐만 아니라 자바스크립트는 어떤 객체든지 에러를 `throw` 하는 것을 허용합니다. 또한, 프로미스는 어떤 객체라도 거부될 수 있습니다. 2349 | `Error` 타입에는 `throw` 구문을 사용하는 것이 바람직합니다. 당신의 에러가 상위 코드의 `catch` 구문에서 잡힐 수 있기 때문입니다. 2350 | 문자열 메시지가 잡히는 것은 매우 혼란스러우며 이는 [디버깅을 더 고통스럽게](https://basarat.gitbook.io/typescript/type-system/exceptions#always-use-error) 만듭니다. 2351 | 이와 같은 이유로 당신은 `Error` 타입으로 프로미스를 거부해야합니다. 2352 | 2353 | **Bad:** 2354 | 2355 | ```ts 2356 | function calculateTotal(items: Item[]): number { 2357 | throw 'Not implemented.'; 2358 | } 2359 | 2360 | function get(): Promise { 2361 | return Promise.reject('Not implemented.'); 2362 | } 2363 | ``` 2364 | 2365 | **Good:** 2366 | 2367 | ```ts 2368 | function calculateTotal(items: Item[]): number { 2369 | throw new Error('Not implemented.'); 2370 | } 2371 | 2372 | function get(): Promise { 2373 | return Promise.reject(new Error('Not implemented.')); 2374 | } 2375 | 2376 | // 또는 아래와 동일합니다: 2377 | 2378 | async function get(): Promise { 2379 | throw new Error('Not implemented.'); 2380 | } 2381 | ``` 2382 | 2383 | `Error` 타입을 사용하는 장점은 `try/catch/finally` 구문에 의해 지원되고 암시적으로 모든 에러가 디버깅에 매우 강력한 `stack` 속성을 가지고 있기 때문입니다. 2384 | 또 하나의 대안은 있습니다. `throw` 구문을 사용하지 않는 대신, 항상 사용자 정의 객체를 반환하는 것입니다. 2385 | 타입스크립트는 이것을 훨씬 더 쉽게 만듭니다. 2386 | 아래의 예제를 확인하세요: 2387 | 2388 | ```ts 2389 | type Result = { isError: false, value: R }; 2390 | type Failure = { isError: true, error: E }; 2391 | type Failable = Result | Failure; 2392 | 2393 | function calculateTotal(items: Item[]): Failable { 2394 | if (items.length === 0) { 2395 | return { isError: true, error: 'empty' }; 2396 | } 2397 | 2398 | // ... 2399 | return { isError: false, value: 42 }; 2400 | } 2401 | ``` 2402 | 2403 | 이 아이디어의 자세한 설명은 [원문](https://medium.com/@dhruvrajvanshi/making-exceptions-type-safe-in-typescript-c4d200ee78e9)을 참고하세요. 2404 | 2405 | **[⬆ 맨 위로 이동](#목차)** 2406 | 2407 | ### `catch` 절에서 에러 처리 부분을 비워두지 마세요 2408 | 2409 | `catch` 절에서 단지 에러를 받는 것만으로는 해당 에러에 대응할 수 없습니다. 또한, 콘솔에 에러를 기록하는 것(`console.log`)은 콘솔에 출력된 많은 것들 사이에서 발견되지 못할 수 있기 때문에 그다지 좋은 선택은 아닙니다. 당신이 어떤 코드를 `try/catch`로 감쌌다면, 그 코드에서 에러가 일어날 수 있으며, 즉 에러가 발생했을 때에 대한 계획이나 장치가 있어야 한다는 것을 의미합니다. 2410 | 2411 | **Bad:** 2412 | 2413 | ```ts 2414 | try { 2415 | functionThatMightThrow(); 2416 | } catch (error) { 2417 | console.log(error); 2418 | } 2419 | 2420 | // 아래 예제는 훨씬 나쁩니다. 2421 | 2422 | try { 2423 | functionThatMightThrow(); 2424 | } catch (error) { 2425 | // 에러를 무시 2426 | } 2427 | ``` 2428 | 2429 | **Good:** 2430 | 2431 | ```ts 2432 | import { logger } from './logging' 2433 | 2434 | try { 2435 | functionThatMightThrow(); 2436 | } catch (error) { 2437 | logger.log(error); 2438 | } 2439 | ``` 2440 | 2441 | **[⬆ 맨 위로 이동](#목차)** 2442 | 2443 | ### 요청이 거부된 프로미스 객체를 무시하지 마세요 2444 | 2445 | 위와 같이 `try/catch` 절에서 받은 에러 처리 부분을 비워두면 안됩니다. 2446 | 2447 | **Bad:** 2448 | 2449 | ```ts 2450 | getUser() 2451 | .then((user: User) => { 2452 | return sendEmail(user.email, 'Welcome!'); 2453 | }) 2454 | .catch((error) => { 2455 | console.log(error); 2456 | }); 2457 | ``` 2458 | 2459 | **Good:** 2460 | 2461 | ```ts 2462 | import { logger } from './logging' 2463 | 2464 | getUser() 2465 | .then((user: User) => { 2466 | return sendEmail(user.email, 'Welcome!'); 2467 | }) 2468 | .catch((error) => { 2469 | logger.log(error); 2470 | }); 2471 | 2472 | // 또는 async/await 구문을 사용할 수 있습니다: 2473 | 2474 | try { 2475 | const user = await getUser(); 2476 | await sendEmail(user.email, 'Welcome!'); 2477 | } catch (error) { 2478 | logger.log(error); 2479 | } 2480 | ``` 2481 | 2482 | **[⬆ 맨 위로 이동](#목차)** 2483 | 2484 | ## 서식 2485 | 2486 | 서식은 주관적입니다. 여기에 있는 많은 규칙들과 같이 당신이 따르기 어려운 규칙은 없습니다. 중요한 점은 서식에 대해서 *논쟁하지 않는 것*입니다. 서식을 자동화하기 위한 도구들이 매우 많습니다. 그 중 하나를 사용하세요! 서식에 대해 논쟁하는 것은 엔지니어에게 시간과 돈 낭비일 뿐입니다. 따라야하는 일반적인 규칙은 *일관적인 서식 규칙을 지켜야하는 것입니다*. 2487 | 2488 | [TSLint](https://palantir.github.io/tslint/)라고 불리는 타입스크립트를 위한 강력한 도구가 있습니다. 이것은 코드의 가독성과 유지보수성을 극적으로 개선시키도록 도와주는 정적 분석 도구입니다. 2489 | 프로젝트에 참고할 수 있는 TSLint 설정을 사용할 준비가 되었습니다: 2490 | 2491 | - [TSLint Config Standard](https://www.npmjs.com/package/tslint-config-standard) - 표준 스타일 규칙 2492 | 2493 | - [TSLint Config Airbnb](https://www.npmjs.com/package/tslint-config-airbnb) - 에어비엔비 스타일 가이드 2494 | 2495 | - [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 규칙 2496 | 2497 | - [TSLint react](https://www.npmjs.com/package/tslint-react) - React & JSX와 관련된 lint 규칙 2498 | 2499 | - [TSLint + Prettier](https://www.npmjs.com/package/tslint-config-prettier) - [Prettier](https://github.com/prettier/prettier) 코드 포맷터를 위한 lint 규칙 2500 | 2501 | - [ESLint rules for TSLint](https://www.npmjs.com/package/tslint-eslint-rules) - 타입스크립트를 위한 ESLint 규칙 2502 | 2503 | - [Immutable](https://www.npmjs.com/package/tslint-immutable) - 타입스크립트에서 변경을 허락하지 않는 규칙 2504 | 2505 | 또한, 훌륭한 자료인 [타입스크립트 스타일 가이드와 코딩 컨벤션](https://basarat.gitbook.io/typescript/styleguide)을 참고해주세요. 2506 | 2507 | > 역자주: TSLint는 deprecated되었습니다. [Roadmap: TSLint -> ESLint](https://github.com/palantir/tslint/issues/4534) 이슈를 확인해주세요. 2508 | 2509 | ### 일관적으로 대소문자를 사용하세요 2510 | 2511 | 대소문자를 구분하여 작성하는 것은 당신에게 변수, 함수 등에 대해서 많은 것을 알려줍니다. 이 규칙은 주관적이어서, 당신의 팀이 원하는 것을 선택해야 합니다. 중요한 점은 어떤 걸 선택하였든지 간에 *일관적*이어야 한다는 것입니다. 2512 | 2513 | **Bad:** 2514 | 2515 | ```ts 2516 | const DAYS_IN_WEEK = 7; 2517 | const daysInMonth = 30; 2518 | 2519 | const songs = ['Back In Black', 'Stairway to Heaven', 'Hey Jude']; 2520 | const Artists = ['ACDC', 'Led Zeppelin', 'The Beatles']; 2521 | 2522 | function eraseDatabase() {} 2523 | function restore_database() {} 2524 | 2525 | type animal = { /* ... */ } 2526 | type Container = { /* ... */ } 2527 | ``` 2528 | 2529 | **Good:** 2530 | 2531 | ```ts 2532 | const DAYS_IN_WEEK = 7; 2533 | const DAYS_IN_MONTH = 30; 2534 | 2535 | const SONGS = ['Back In Black', 'Stairway to Heaven', 'Hey Jude']; 2536 | const ARTISTS = ['ACDC', 'Led Zeppelin', 'The Beatles']; 2537 | 2538 | function eraseDatabase() {} 2539 | function restoreDatabase() {} 2540 | 2541 | type Animal = { /* ... */ } 2542 | type Container = { /* ... */ } 2543 | ``` 2544 | 2545 | 클래스, 인터페이스, 타입 그리고 네임스페이스 이름에는 `PascalCase`를 사용하세요. 2546 | 변수, 함수 그리고 클래스 멤버 이름에는 `camelCase`를 사용하세요. 2547 | 2548 | **[⬆ 맨 위로 이동](#목차)** 2549 | 2550 | ### 함수 호출자와 피호출자를 가깝게 위치시키세요 2551 | 2552 | 함수가 다른 함수를 호출할 때, 코드에서 이 함수들을 수직적으로 가깝게 유지하도록 하세요. 이상적으로는, 호출하는 함수를 호출을 당하는 함수 바로 위에 위치시키는게 좋습니다. 2553 | 우리는 신문처럼 코드를 위에서 아래로 읽는 경향이 있기 때문에, 코드를 작성할 때에도 이런 방식으로 읽는 것을 고려해야 합니다. 2554 | 2555 | **Bad:** 2556 | 2557 | ```ts 2558 | class PerformanceReview { 2559 | constructor(private readonly employee: Employee) { 2560 | } 2561 | 2562 | private lookupPeers() { 2563 | return db.lookup(this.employee.id, 'peers'); 2564 | } 2565 | 2566 | private lookupManager() { 2567 | return db.lookup(this.employee, 'manager'); 2568 | } 2569 | 2570 | private getPeerReviews() { 2571 | const peers = this.lookupPeers(); 2572 | // ... 2573 | } 2574 | 2575 | review() { 2576 | this.getPeerReviews(); 2577 | this.getManagerReview(); 2578 | this.getSelfReview(); 2579 | 2580 | // ... 2581 | } 2582 | 2583 | private getManagerReview() { 2584 | const manager = this.lookupManager(); 2585 | } 2586 | 2587 | private getSelfReview() { 2588 | // ... 2589 | } 2590 | } 2591 | 2592 | const review = new PerformanceReview(employee); 2593 | review.review(); 2594 | ``` 2595 | 2596 | **Good:** 2597 | 2598 | ```ts 2599 | class PerformanceReview { 2600 | constructor(private readonly employee: Employee) { 2601 | } 2602 | 2603 | review() { 2604 | this.getPeerReviews(); 2605 | this.getManagerReview(); 2606 | this.getSelfReview(); 2607 | 2608 | // ... 2609 | } 2610 | 2611 | private getPeerReviews() { 2612 | const peers = this.lookupPeers(); 2613 | // ... 2614 | } 2615 | 2616 | private lookupPeers() { 2617 | return db.lookup(this.employee.id, 'peers'); 2618 | } 2619 | 2620 | private getManagerReview() { 2621 | const manager = this.lookupManager(); 2622 | } 2623 | 2624 | private lookupManager() { 2625 | return db.lookup(this.employee, 'manager'); 2626 | } 2627 | 2628 | private getSelfReview() { 2629 | // ... 2630 | } 2631 | } 2632 | 2633 | const review = new PerformanceReview(employee); 2634 | review.review(); 2635 | ``` 2636 | 2637 | **[⬆ 맨 위로 이동](#목차)** 2638 | 2639 | ### import 구문을 특정 순서대로 정리하세요 2640 | 2641 | `import` 구문을 읽기 쉽고 명료하게 하면 당신은 현재 코드의 의존성을 빠르게 확인할 수 있습니다. 다음과 같은 `import` 구문 정리를 위한 좋은 방법들을 적용해보세요: 2642 | 2643 | - import 구문은 알파벳 순서대로 배열하고 그룹화해야 합니다. 2644 | - 사용하지 않은 import 구문은 제거되어야 합니다. 2645 | - 이름이 있는 import 구문은 알파벳 순서대로 배열해야 합니다. (예: `import {A, B, C} from 'foo';`) 2646 | - import 하는 소스코드는 그룹 내에서 알파벳 순서대로 배열해야 합니다. (예: `import * as foo from 'a'; import * as bar from 'b';`) 2647 | - import 구문의 그룹은 빈 줄로 구분되어야 합니다. 2648 | - 그룹은 다음 순서를 준수해야 합니다: 2649 | - 폴리필 (예: `import 'reflect-metadata';`) 2650 | - Node 내장 모듈 (예: `import fs from 'fs';`) 2651 | - 외부 모듈 (예: `import { query } from 'itiriri';`) 2652 | - 내부 모듈 (예: `import { UserService } from 'src/services/userService';`) 2653 | - 상위 디렉토리에서 불러오는 모듈 (예: `import foo from '../foo'; import qux from '../../foo/qux';`) 2654 | - 동일한 계층의 디렉토리에서 불러오는 모듈 (예: `import bar from './bar'; import baz from './bar/baz';`) 2655 | 2656 | **Bad:** 2657 | 2658 | ```ts 2659 | import { TypeDefinition } from '../types/typeDefinition'; 2660 | import { AttributeTypes } from '../model/attribute'; 2661 | import { ApiCredentials, Adapters } from './common/api/authorization'; 2662 | import fs from 'fs'; 2663 | import { ConfigPlugin } from './plugins/config/configPlugin'; 2664 | import { BindingScopeEnum, Container } from 'inversify'; 2665 | import 'reflect-metadata'; 2666 | ``` 2667 | 2668 | **Good:** 2669 | 2670 | ```ts 2671 | import 'reflect-metadata'; 2672 | 2673 | import fs from 'fs'; 2674 | import { BindingScopeEnum, Container } from 'inversify'; 2675 | 2676 | import { AttributeTypes } from '../model/attribute'; 2677 | import { TypeDefinition } from '../types/typeDefinition'; 2678 | 2679 | import { ApiCredentials, Adapters } from './common/api/authorization'; 2680 | import { ConfigPlugin } from './plugins/config/configPlugin'; 2681 | ``` 2682 | 2683 | **[⬆ 맨 위로 이동](#목차)** 2684 | 2685 | ### 타입스크립트 앨리어스를 사용하세요 2686 | 2687 | `tsconfig.json`의 `compilerOptions` 섹션 안에서 `paths`와 `baseUrl` 속성을 정의해 더 보기 좋은 import 구문을 작성해주세요. 2688 | 2689 | 이 방법은 import 구문을 사용할 때 긴 상대경로를 작성하는 것을 피하게 도와줄 것입니다. 2690 | 2691 | **Bad:** 2692 | 2693 | ```ts 2694 | import { UserService } from '../../../services/UserService'; 2695 | ``` 2696 | 2697 | **Good:** 2698 | 2699 | ```ts 2700 | import { UserService } from '@services/UserService'; 2701 | ``` 2702 | 2703 | ```js 2704 | // tsconfig.json 2705 | ... 2706 | "compilerOptions": { 2707 | ... 2708 | "baseUrl": "src", 2709 | "paths": { 2710 | "@services": ["services/*"] 2711 | } 2712 | ... 2713 | } 2714 | ... 2715 | ``` 2716 | 2717 | **[⬆ 맨 위로 이동](#목차)** 2718 | 2719 | ## 주석 2720 | 2721 | 주석을 사용하는 것은 주석 없이 코드를 작성하는 것이 실패했다는 표시입니다. 코드는 단일 진실 공급원(Single source of truth)이어야 합니다. 2722 | 2723 | > 나쁜 코드에 주석들 달지 마라. 새로 짜라. 2724 | > — *Brian W. Kernighan and P. J. Plaugher* 2725 | 2726 | ### 주석 대신에 자체적으로 설명 가능한 코드를 작성하세요 2727 | 2728 | 주석은 변명일 뿐, 필요하지 않습니다. 좋은 코드는 대부분 그 존재 자체로 문서화가 됩니다. 2729 | 2730 | **Bad:** 2731 | 2732 | ```ts 2733 | // subscription이 활성화 상태인지 체크합니다. 2734 | if (subscription.endDate > Date.now) { } 2735 | ``` 2736 | 2737 | **Good:** 2738 | 2739 | ```ts 2740 | const isSubscriptionActive = subscription.endDate > Date.now; 2741 | if (isSubscriptionActive) { /* ... */ } 2742 | ``` 2743 | 2744 | **[⬆ 맨 위로 이동](#목차)** 2745 | 2746 | ### 당신의 코드를 주석 처리하지 마세요 2747 | 2748 | 버전 관리 시스템이 존재하는 이유입니다. 사용하지 않는 코드는 기록에 남기세요. 2749 | 2750 | **Bad:** 2751 | 2752 | ```ts 2753 | type User = { 2754 | name: string; 2755 | email: string; 2756 | // age: number; 2757 | // jobPosition: string; 2758 | } 2759 | ``` 2760 | 2761 | **Good:** 2762 | 2763 | ```ts 2764 | type User = { 2765 | name: string; 2766 | email: string; 2767 | } 2768 | ``` 2769 | 2770 | **[⬆ 맨 위로 이동](#목차)** 2771 | 2772 | ### 일기 같은 주석을 달지 마세요 2773 | 2774 | 버전 관리 시스템을 사용하세요! 죽은 코드, 주석 처리된 코드, 특히 일기 같은 주석은 필요 없습니다. 대신에 기록을 보기 위해 `git log` 명령어를 사용하세요! 2775 | 2776 | **Bad:** 2777 | 2778 | ```ts 2779 | /** 2780 | * 2016-12-20: 이해하지 못해서 모나드를 제거함 (RM) 2781 | * 2016-10-01: 특별한 모나드를 사용해 개선함 (JP) 2782 | * 2016-02-03: 타입 체킹 추가함 (LI) 2783 | * 2015-03-14: combine 함수를 구현함 (JR) 2784 | */ 2785 | function combine(a: number, b: number): number { 2786 | return a + b; 2787 | } 2788 | ``` 2789 | 2790 | **Good:** 2791 | 2792 | ```ts 2793 | function combine(a: number, b: number): number { 2794 | return a + b; 2795 | } 2796 | ``` 2797 | 2798 | **[⬆ 맨 위로 이동](#목차)** 2799 | 2800 | ### 코드의 위치를 설명하는 주석을 사용하지 마세요 2801 | 2802 | 이건 보통 코드를 어지럽히기만 합니다. 함수와 변수 이름을 적절한 들여쓰기와 서식으로 당신의 코드에 시각적인 구조가 보이도록 하세요. 2803 | 대부분의 IDE(통합 개발 환경)에서는 코드 블록을 `접기/펼치기` 할 수 있는 기능을 지원합니다. (Visual Studio Code의 [folding regions](https://code.visualstudio.com/updates/v1_17#_folding-regions)를 확인해보세요). 2804 | 2805 | **Bad:** 2806 | 2807 | ```ts 2808 | //////////////////////////////////////////////////////////////////////////////// 2809 | // Client 클래스 2810 | //////////////////////////////////////////////////////////////////////////////// 2811 | class Client { 2812 | id: number; 2813 | name: string; 2814 | address: Address; 2815 | contact: Contact; 2816 | 2817 | //////////////////////////////////////////////////////////////////////////////// 2818 | // public 메소드 2819 | //////////////////////////////////////////////////////////////////////////////// 2820 | public describe(): string { 2821 | // ... 2822 | } 2823 | 2824 | //////////////////////////////////////////////////////////////////////////////// 2825 | // private 메소드 2826 | //////////////////////////////////////////////////////////////////////////////// 2827 | private describeAddress(): string { 2828 | // ... 2829 | } 2830 | 2831 | private describeContact(): string { 2832 | // ... 2833 | } 2834 | }; 2835 | ``` 2836 | 2837 | **Good:** 2838 | 2839 | ```ts 2840 | class Client { 2841 | id: number; 2842 | name: string; 2843 | address: Address; 2844 | contact: Contact; 2845 | 2846 | public describe(): string { 2847 | // ... 2848 | } 2849 | 2850 | private describeAddress(): string { 2851 | // ... 2852 | } 2853 | 2854 | private describeContact(): string { 2855 | // ... 2856 | } 2857 | }; 2858 | ``` 2859 | 2860 | **[⬆ 맨 위로 이동](#목차)** 2861 | 2862 | ### TODO 주석 2863 | 2864 | 추후에 개선을 위해 코드에 메모를 남겨야할 때, `// TODO` 주석을 사용하세요. 대부분의 IDE는 이런 종류의 주석을 특별하게 지원하기 때문에 해야할 일 목록을 빠르게 검토할 수 있습니다. 2865 | 2866 | 하지만 *TODO* 주석이 나쁜 코드를 작성할 이유는 아니라는 것을 명심하세요. 2867 | 2868 | **Bad:** 2869 | 2870 | ```ts 2871 | function getActiveSubscriptions(): Promise { 2872 | // ensure `dueDate` is indexed. 2873 | return db.subscriptions.find({ dueDate: { $lte: new Date() } }); 2874 | } 2875 | ``` 2876 | 2877 | **Good:** 2878 | 2879 | ```ts 2880 | function getActiveSubscriptions(): Promise { 2881 | // TODO: ensure `dueDate` is indexed. 2882 | return db.subscriptions.find({ dueDate: { $lte: new Date() } }); 2883 | } 2884 | ``` 2885 | 2886 | **[⬆ 맨 위로 이동](#목차)** 2887 | 2888 | ## 번역 2889 | 2890 | 이 글을 다른 언어로도 읽을 수 있습니다: 2891 | - ![br](https://raw.githubusercontent.com/gosquared/flags/master/flags/flags/shiny/24/Brazil.png) **Brazilian Portuguese**: [vitorfreitas/clean-code-typescript](https://github.com/vitorfreitas/clean-code-typescript) 2892 | - ![cn](https://raw.githubusercontent.com/gosquared/flags/master/flags/flags/shiny/24/China.png) **Chinese**: 2893 | - [beginor/clean-code-typescript](https://github.com/beginor/clean-code-typescript) 2894 | - [pipiliang/clean-code-typescript](https://github.com/pipiliang/clean-code-typescript) 2895 | - ![ja](https://raw.githubusercontent.com/gosquared/flags/master/flags/flags/shiny/24/Japan.png) **Japanese**: [MSakamaki/clean-code-typescript](https://github.com/MSakamaki/clean-code-typescript) 2896 | - ![ru](https://raw.githubusercontent.com/gosquared/flags/master/flags/flags/shiny/24/Russia.png) **Russian**: [Real001/clean-code-typescript](https://github.com/Real001/clean-code-typescript) 2897 | - ![tr](https://raw.githubusercontent.com/gosquared/flags/master/flags/flags/shiny/24/Turkey.png) **Turkish**: [ozanhonamlioglu/clean-code-typescript](https://github.com/ozanhonamlioglu/clean-code-typescript) 2898 | - ![ko](https://raw.githubusercontent.com/gosquared/flags/master/flags/flags/shiny/24/South-Korea.png) **Korean**: [738/clean-code-typescript](https://github.com/738/clean-code-typescript) 2899 | 2900 | 번역이 완료되면 참고문헌에 추가됩니다. 2901 | 자세한 내용과 진행상황을 보고싶다면 이 [논의](https://github.com/labs42io/clean-code-typescript/issues/15)를 확인하세요. 2902 | 당신은 당신의 언어에 이 글을 번역함으로써 *클린 코드* 커뮤니티에 중요한 기여를 할 수 있습니다. 2903 | 2904 | **[⬆ 맨 위로 이동](#목차)** 2905 | 2906 | ## 번역에 도움을 주신 분들 2907 | 2908 | ### 리뷰 2909 | 2910 | - [@samslow](https://github.com/samslow) 2911 | - [@Space0726](https://github.com/Space0726) 2912 | - [@zwonlala](https://github.com/zwonlala) 2913 | - [@joonghyeob-shin](https://github.com/joonghyeob-shin) 2914 | 2915 | ### 참고자료 2916 | 2917 | - [qkraudghgh/clean-code-javascript-ko](https://github.com/qkraudghgh/clean-code-javascript-ko) 2918 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /clean-code-typescript.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/738/clean-code-typescript/c67d0fff2fe9fc8ea662eaf59ddad9e50c96a1fe/clean-code-typescript.png --------------------------------------------------------------------------------