├── .gitignore ├── README.md ├── ch01_타입스크립트_알아보기 ├── item01_chanu.md ├── item02 │ ├── ex00-noImplicitAny │ │ ├── index.ts │ │ ├── package.json │ │ ├── tsconfig.json │ │ └── yarn.lock │ ├── ex01-strictNullChecks │ │ ├── index.ts │ │ ├── package.json │ │ ├── tsconfig.json │ │ └── yarn.lock │ └── tsconfig.json ├── item02_호찬.md ├── item03_jungho.md ├── item04_dami.md └── item05_jungho.md ├── ch02_타입스크립트의_타입_시스템 ├── item06 │ ├── img_01.png │ ├── img_02.png │ ├── img_03.png │ ├── img_04.png │ ├── img_05.png │ ├── img_06.png │ └── img_07.png ├── item06_chanu.md ├── item07 │ └── extends_keyof.ts ├── item07_호찬.md ├── item08_dami.md ├── item09 │ └── index.ts ├── item09_호찬.md ├── item10_dami.md ├── item11 │ ├── img_01.png │ └── img_02.png ├── item11_chanu.md ├── item12_jungho.md ├── item13_혁주.md ├── item14_혁주.md ├── item15_호찬.md ├── item16_jungho.md ├── item17_dami.md └── item18_chanwoo.md ├── ch03_타입_추론 ├── item19_chanu.md ├── item20_dami.md ├── item21_호찬.md ├── item22_jungho.md ├── item23_혁주.md ├── item24_dami.md ├── item25_호찬.md ├── item26_chanu.md └── item27_혁주.md ├── ch04_타입_설계 ├── item28_jungho.md ├── item29_호찬.md ├── item30_혁주.md ├── item31_jungho.md ├── item32_dami.md ├── item33_chanu.md ├── item34_혁주.md ├── item35_dami.md ├── item36_chanu.md └── item37_호찬.md ├── ch05_any_다루기 ├── item38_jungho.md ├── item39_호찬.md ├── item41_dami.md ├── item42_혁주.md ├── item43_chanu.md └── item44_혁주.md ├── ch06_타입_선언과_@types ├── item45_호찬.md ├── item46_dami.md ├── item48 │ ├── img.png │ └── img_1.png ├── item48_chanu.md ├── item49_혁주.md ├── item50_jungho.md ├── item51_chanu.md └── item52_dami.md ├── ch07_코드를_작성하고_실행하기 ├── item53_호찬.md ├── item55_dami.md ├── item56_chanu.md ├── item57 │ ├── img │ │ ├── after-source-map.png │ │ └── before-source-map.png │ └── src │ │ ├── .gitignore │ │ ├── index.html │ │ ├── index.js │ │ ├── index.js.map │ │ ├── index.ts │ │ └── package.json └── item57_호찬.md └── ch08_타입스크립트로_마이그레이션하기 ├── item58_혁주.md ├── item59 ├── img │ ├── dom.png │ ├── global.png │ ├── ts-check.png │ └── unknown-library.png ├── index.js └── types.d.ts ├── item59_호찬.md ├── item60_혁주.md ├── item61_dami.md └── item62_jungho.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | node_modules/ 4 | 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | .yarn/cache 11 | .yarn/unplugged 12 | .yarn/build-state.yml 13 | .yarn/install-state.gz 14 | -------------------------------------------------------------------------------- /ch01_타입스크립트_알아보기/item01_chanu.md: -------------------------------------------------------------------------------- 1 |  2 | # TS와 JS 관계 이해하기 3 | 4 | JS의 가장 큰 특성은 5 | 6 | - 단일 스레드 7 | - 비동기 모델 8 | 9 | > 브라우저에서 가벼운 처리를 위해 도입한 단일 스레드와 비동기 모델이 단점으로 지적되어 왔지만, 현대의 멀티코어 병렬 프로그래밍 환경에는 적합한 메커니즘으로 인식되는 역설적인 상황이 벌어지고 있습니다. 10 | 11 |   하지만 단점으로 꼽히는 것이 바로 '**타입 불안정성**' 이다. 이는 JS가 변수를 선언하는 시점에 해당 변수가 어떠한 값을 가질 것인지에 대해서 명시할 필요할 필요가 없기 때문이다. (반면에 정적타이핑 언어인 C++는 변수를 선언하는 시점에 반드시 해당 변수가 가질 값과 타입이 반드시 명시되어야한다.) 12 | 13 |   TS는 JS가 가진 단점인 '**타입 불안정성**'에 대한 해결책으로, 독보적인 생산성을 가진 JS의 단점을 보완할 수 있는 언어이다. 또한 단순히 TS는 타입 시스템을 제공하는 것 뿐 만 아니라, 전체적인 언어 서비스 모음을 제공하고 있다. 즉, 타이핑만을 위해서 TS를 도입하는 것은 TS를 과소평가하고 있는 것이다. 14 | 15 | > 여담으로, 제임스웹 우주망원경에 JS가 들어가 있다고 한다.. ~~어디에나 있다.~~ 16 | 17 |
18 | 19 | ### 그럼 어떤 관계인가? 20 | 21 | > TS는 JS의 superset으로, JS를 포함하고 있다. 22 | 23 |   모든 JS파일은 TS파일로 확장자 변경이 가능하지만, TS파일을 JS파일의 확장자로의 변경은 불가능한 경우가 존재한다. 즉, TS만의 독립적인 문법이 존재한다고 볼 수 있다. 그러나.. 24 | 25 |
26 | 27 | > 포함관계를 떠나 문법의 유효성과 동작의 이슈는 독립적인 문제이다. 28 | 29 |   포함관계를 덕분에 JS코드의 TS 마이그레이션은 확장자의 변경 만으로도 가능하다. 물론 JS 문법의 오류가 없더라도 TS의 타입체커에 의해 오류로 검출될 수 있다. 하지만 검출된 오류는 JS의 컴파일과 실행에 전혀 영향을 주지 않는다. 실제로 JS 문법상 오류가 없다면 타입체커가 뱉어내는 오류와 전혀 무관하게 여전히 JS로 컴파일되어 실행 가능하다. '**문법의 유효성과 동작의 이슈는 독립적인 문제**'이기 때문이다. 에러가 에러가 아니면 이거 왜 쓰는데? 30 | 31 |
32 | 33 | ### 그럼 왜 사용하는가? 34 | 35 | > 타입 시스템의 목표 중 하나는 런타임에 오류를 발생시킬 코드를 미리 찾아내는 것이다. 36 | 37 |   앞에서 TS의 타입 시스템 도입을 통해 JS가 가진 '타입 불안정성'을 해결할 수 있다고 설명하였다. 생각해보자. 컴파일과 실행이 동시에 수행되는 JS의 특성상, 타입오류는 런타임 시점에 잡아낼 수 밖에 없다. 하지만 런타임 시점은 말그대로 코드가 실행되는 시점이기 때문에 실행이 유지되어야하는 프로그램의 경우 매우 위험할 수 있다. 그러므로 런타임 시점에 발생할 수 있는 오류를 미리 차단하는 것이 타입 시스템 도입의 이유이다. 38 | 39 |   그러나 아쉽게도 TS가 잠재적으로 발생할 모든 타입 오류를 차단하는 '천라지망'은 아니다. 아까도 말했듯이 TS의 오류는 프로그램의 동작과 전혀 무관하다. 타입 시스템의 타입 체커는 그저 단순히 타이핑 오류에 대한 가이드의 역할만을 수행한다. IDE의 빨간줄은 안고치면 실행했을 때 에러라도 뜨지.. 그럼 타입체커는 사실상 그 것보다 못한 존재인가? 40 | 41 | ```javascript 42 | const states = [ 43 | {name : 'Alabama', capitol : 'Montgomery'}, 44 | {name : 'Alaska', capitol : 'Juneau'}, 45 | {name : 'Arizona', capitol : 'Phoenix'}, 46 | ] 47 | 48 | for (const state of states) { 49 | console.log(state.capital); // 출력값 : undefined 50 | // ts : capital -> capitol guide 51 | } 52 | ``` 53 | 54 |   위의 코드는 JS엔진에 의해 정상적으로 실행되지만, 타입체커는 `state` 배열 원소 객체의 속성명으로 조회할 수 있도록 가이드하고 있다. JS 동작과 전혀 무관하기 때문에 에러로 검출되지 않았으나, 타입체커가 타입오류로 검출해낸 덕에 객체의 속성값을 조회할 수 있도록 수정할 수 있었다. 해당 코드는 동작에 전혀 문제가 되지 않았으나, 우리가 원하는 의도대로 되지 않았을 것이다. 이렇게 **코드의 '동작'과 '의도'를 단일화시키는 역할**을 TS가 수행한다. 55 | 56 |   이전 예제가 타입 구문을 추가하지 않고 오로지 TS의 타입체커를 통해서 '동작'과 '의도'를 단일화했다면, 타입 구문을 추가함을 통해 더 명확한 '의도'를 타입체커에게 제공할 수 있다. 실제로 위의 코드에서 `state` 배열 원소 객체의 속성명을 올바르게 가이드 하였으나, 실제 코드의 의도는 `capitol`이 아닌, `capital`을 속성명으로 사용하여 조회하는 것이었다. 아래와 같이 타입 구문을 추가하여 **'동작'과 '의도'를 단일화**할 뿐 만 아니라 **'의도'의 명확화** 또한 가능하게 된다. 57 | 58 | ```typescript 59 | interface State { 60 | name: string; 61 | capital: string; 62 | } 63 | 64 | const states : State[] = [ 65 | {name : 'Alabama', capitol : 'Montgomery'}, // ts : capitol -> capital guide 66 | {name : 'Alaska', capitol : 'Juneau'}, 67 | {name : 'Arizona', capitol : 'Phoenix'}, 68 | ] 69 | ``` 70 |   `interface`를 통해 타입구문을 선언하였다. `state` 배열의 객체가 가져야하는 속성을 명시적으로 제공하여 `state` 선언시점에서부터 코드의 '의도'와 다른 부분을 파악할 수 있게 되었다. 그만큼 코드의 '의도'가 명확해졌기 때문에 빠르게 파악할 수 있는 것이다. 71 | 72 |
73 | 74 | ### 추가적으로 주의할 것은? 75 | 76 | > 기본적으로 TS 타입 시스템은 JS의 런타임 동작을 '모델링'한다. 77 | 78 |   대부분의 경우, TS는 JS의 런타임 동작을 따라간다. 하지만 강제적인 형변환이 발생하는 코드는 타입이 제공하는 '의도'가 모호해지기 때문에 JS가 정상 동작함에도 타입체커가 오류로 가이드할 수 있다. 79 | 80 | ```typescript 81 | const a = null + 7; // 7 82 | const b = [] + 12; // '12' 83 | ``` 84 | 85 |   JS는 두 코드 모두 오류로 인식하지 않고 반환값을 제공한다. 이 과정에서 형변환이 발생한다. 만약 형변환을 의도하였다면, TS의 타입 시스템이 가이드하는 오류는 의미가 없다. 그러므로 TS의 사용 목적에 맞게 **최대한 타입이 변경되는 상황을 피하는 것이 좋다**. 즉, TS가 가이드를 잘 할 수 있도록 모호한 부분을 줄여나가는 것이 중요하다는 것을 의미한다. 86 | 87 |
88 | 89 | ### 정리하자면, 90 | 91 | #### 1. TS는 JS의 superset이지만, TS 문법의 유효성과 JS 동작의 이슈는 독립적인 문제이다. 92 | 93 | #### 2. TS의 타입구문을 통해 '동작'과 '의도'를 단일화 할 뿐 만 아니라 코드가 가진 '의도'를 명확히 할 수 있다. 94 | 95 | #### 3. 기본적으로 TS 타입 시스템은 JS의 런타임 동작을 '모델링' 하지만 형변환에 있어서는 최대한 피하는 것이 좋다. 96 | 97 | 98 | --- 99 | 100 |
101 |
102 | 103 | > Written with [StackEdit](https://stackedit.io/). 104 | -------------------------------------------------------------------------------- /ch01_타입스크립트_알아보기/item02/ex00-noImplicitAny/index.ts: -------------------------------------------------------------------------------- 1 | function add(a, b) { 2 | return a + b; 3 | } 4 | 5 | function subtract(a: number, b: number) { 6 | return a - b; 7 | } -------------------------------------------------------------------------------- /ch01_타입스크립트_알아보기/item02/ex00-noImplicitAny/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ex00", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "typescript": "^4.8.2" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /ch01_타입스크립트_알아보기/item02/ex00-noImplicitAny/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */ 8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | // "outDir": "./", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 43 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */ 44 | // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ 45 | 46 | /* Module Resolution Options */ 47 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 48 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 49 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 50 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 51 | // "typeRoots": [], /* List of folders to include type definitions from. */ 52 | // "types": [], /* Type declaration files to be included in compilation. */ 53 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 54 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 55 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 56 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 57 | 58 | /* Source Map Options */ 59 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 60 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 61 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 62 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 63 | 64 | /* Experimental Options */ 65 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 66 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 67 | 68 | /* Advanced Options */ 69 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 70 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /ch01_타입스크립트_알아보기/item02/ex00-noImplicitAny/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | typescript@^4.8.2: 6 | version "4.8.2" 7 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.2.tgz#e3b33d5ccfb5914e4eeab6699cf208adee3fd790" 8 | integrity sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw== 9 | -------------------------------------------------------------------------------- /ch01_타입스크립트_알아보기/item02/ex01-strictNullChecks/index.ts: -------------------------------------------------------------------------------- 1 | const x: number = null; 2 | 3 | const y: number | null = null; 4 | 5 | const el = document.getElementById('status'); 6 | el.textContent = 'Ready'; 7 | 8 | if (el) { 9 | el.textContent = 'Ready'; 10 | } 11 | el!.textContent = 'Ready'; -------------------------------------------------------------------------------- /ch01_타입스크립트_알아보기/item02/ex01-strictNullChecks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ex00", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "typescript": "^4.8.2" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /ch01_타입스크립트_알아보기/item02/ex01-strictNullChecks/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */ 8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | // "outDir": "./", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 43 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */ 44 | // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ 45 | 46 | /* Module Resolution Options */ 47 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 48 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 49 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 50 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 51 | // "typeRoots": [], /* List of folders to include type definitions from. */ 52 | // "types": [], /* Type declaration files to be included in compilation. */ 53 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 54 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 55 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 56 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 57 | 58 | /* Source Map Options */ 59 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 60 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 61 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 62 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 63 | 64 | /* Experimental Options */ 65 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 66 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 67 | 68 | /* Advanced Options */ 69 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 70 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /ch01_타입스크립트_알아보기/item02/ex01-strictNullChecks/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | typescript@^4.8.2: 6 | version "4.8.2" 7 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.2.tgz#e3b33d5ccfb5914e4eeab6699cf208adee3fd790" 8 | integrity sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw== 9 | -------------------------------------------------------------------------------- /ch01_타입스크립트_알아보기/item02/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */ 8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | // "outDir": "./", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 43 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */ 44 | // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ 45 | 46 | /* Module Resolution Options */ 47 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 48 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 49 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 50 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 51 | // "typeRoots": [], /* List of folders to include type definitions from. */ 52 | // "types": [], /* Type declaration files to be included in compilation. */ 53 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 54 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 55 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 56 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 57 | 58 | /* Source Map Options */ 59 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 60 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 61 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 62 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 63 | 64 | /* Experimental Options */ 65 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 66 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 67 | 68 | /* Advanced Options */ 69 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 70 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /ch01_타입스크립트_알아보기/item02_호찬.md: -------------------------------------------------------------------------------- 1 | # 아이템 2. 타입스크립트 설정 이해하기 2 | 3 | ### 설정 적용 방법 4 | 5 | 타입스크립트에는 `커맨드 라인`과 `tsconfig.json` 설정 적용 방법이 있습니다. 6 | 7 | 1. 커맨드 라인 8 | 9 | ```bash 10 | tsc --noImplicitAny program.ts 11 | ``` 12 | 13 | 2. tsconfig.json 14 | 15 | ```json 16 | { 17 | "compilerOptions": { 18 | "noImplicitAny": true 19 | } 20 | } 21 | ``` 22 | 23 | > `tsc --init`을 통해 기본 `tsconfig.json` 파일을 생성할 수 있습니다. 24 | 25 | ### Strictness 26 | 27 | 일부 사용자들은 `타입 검사`와는 다른 기능들을 경험하기 위해 `Typescript`를 사용하며, 느슨한 설정을 원합니다. 여러 타입 검사 설정들은 선택사항입니다. 예를들어, `null`나 `undefined` 같은 기본 값들을 검사하지 않게 설정할 수 있습니다. 기존 JavaScript를 마이그레이션하는 경우 이는 바람직한 첫 단계입니다. 28 | 29 | 많은 `Typescript` 사용자들은 가능한 한 즉시 유효성을 검사하는 것을 선호하며 이것이 언어가 `Strictness`을 제공하는 주요 이유중 하나입니다. 30 | 31 | TypeScript는 기본적으로 모든 flag들이 활성화됩니다. 32 | 33 | `tsconfig.json`에서 `"strict": true`을 사용해 모든 `strict flag`들을 동시에 켤 수 있습니다. `strict flag` 중 알아야 할 가장 큰 두 가지는 `noImplicitAny` 및 `strictNullChecks`입니다. 34 | 35 | #### `noImplicitAny` 36 | 37 | `any`를 사용하는것은 `Typescript`를 사용하는 목적을 무효화하는 경우가 많습니다. 프로그램은 타입이 많을수록 더 많은 유효성 검사를 할 수 있으므로, 코드 작성 시 버그가 더 적게 발생합니다. 38 | 39 | ```ts 40 | function add(a, b) { 41 | // Parameter 'a' implicitly has an 'any' type.ts(7006) 42 | // Parameter 'b' implicitly has an 'any' type.ts(7006) 43 | return a + b; 44 | } 45 | 46 | function subtract(a: number, b: number) { 47 | return a - b; 48 | } 49 | ``` 50 | 51 | #### `strictNullChecks` 52 | 53 | 기본적으로 `null`및 같은 값 `undefined`은 다른 유형에 할당할 수 있습니다. 54 | 55 | ```ts 56 | const x: number = null; 57 | ``` 58 | 59 | 이렇게 하면 일부 코드를 더 쉽게 작성할 수 있지만 `null`, `undefined`을 처리하는 것을 잊어버려서 수많은 버그의 원인이 될 수 있습니다. 60 | 61 | `strictNullChecks` 옵션은 `null`, `undefined` 관련된 오류를 잡아내는 데 많은 도움을 줍니다. 62 | 63 | ```ts 64 | const x: number = null; // type 'null' is not assignable to type 'number'.ts(2322) 65 | ``` 66 | 67 | 오류의 해결 방법으로는 아래 두 가지가 있습니다. 68 | 69 | 1. 의도적으로 명시해서 null 허용 가능합니다. 70 | 71 | ```ts 72 | const x: number | null = null; 73 | ``` 74 | 75 | 2. `null` 값을 허용하기를 원하지 않는다면, `null`을 검사하는 코드나 단언문(assertion)을 추가해야 합니다. 76 | 77 | ```ts 78 | const el = document.getElementById('status'); 79 | el.textContent = 'Ready'; // Object is possibly 'null'.ts(2531) 80 | 81 | if (el) { 82 | el.textContent = 'Ready'; 83 | } 84 | el!.textContent = 'Ready'; 85 | ``` 86 | 87 | ### 요약 88 | 89 | - 자바스크립트 프로젝트를 타입스크립트로 전환하는 것이 아니라면 `noImplicitAny`, `strictNullChecks`를 설정하는 것이 좋습니다. 90 | - 타입스크립트에서 엄격한 검사를 하고 싶다면 strict 설정을 고려해야 합니다. 91 | 92 | ### 참고 93 | 94 | - [tsconfig.json을 설정해보자](https://egas.tistory.com/120?category=481580) 95 | -------------------------------------------------------------------------------- /ch01_타입스크립트_알아보기/item04_dami.md: -------------------------------------------------------------------------------- 1 | # 아이템 4. 구조적 타이핑에 익숙해지기 2 | 3 | 자바스크립트는 덕 타이핑(`Duct Typing`) 기반입니다.덕 타이핑은 클래스 상속이나 인터페이스 구현으로 타입을 구분하는 대신,덕 타이핑은 객체가 어떤 타입에 걸맞은 변수와 메소드를 지니면 객체를 해당 타입에 속하는 것으로 간주합니다. 4 | 5 | ## 덕 타이핑 6 | 7 | ```ts 8 | interface Developer { 9 | name: string; 10 | study(): void; 11 | } 12 | 13 | class FrontendDeveloper implements Developer { 14 | name = "dami"; 15 | study = () => { 16 | console.log(`${this.name}는 공부중`); 17 | }; 18 | } 19 | 20 | class Robot { 21 | name = "reactbot"; 22 | study = () => { 23 | console.log(`${this.name}도 공부중`); 24 | }; 25 | } 26 | 27 | const frontendDeveloperInstance = new FrontendDeveloper(); 28 | const robotInstance = new Robot(); 29 | 30 | function doSometing(developer: Developer): void { 31 | developer.study(); 32 | } 33 | 34 | doSometing(frontendDeveloperInstance); //(1) "dami는 공부중" 35 | doSometing(robotInstance); //(2) "reactbot도 공부중" 36 | ``` 37 | 38 | 위 예제의 (1)번의 함수호출을 보면 `frontendDeveloperInstance`로 `doSometing`을 실행하는 것은 에러가 발생할 것으로 예상되지 않습니다. 39 | `doSometing`의 파라미터인 `developer`은 `Developer` 타입을 만족해야하는데 40 | `FrontendDeveloper` 클래스는 `Developer` 인터페이스로 구현되었고, 41 | `frontendDeveloperInstance`는 `FrontendDeveloper` 클래스의 인스턴스이기 때문입니다. 42 | 43 | 반면 (2)번의 함수호출을 보면, 파라미터로 들어간 `robotInstance`가 `Developer` 인터페이스로 정의되어 있지 않기 때문에 타입 에러가 발생 할 것 같은데 에러가 발생하지 않습니다. 여기서 타입스크립트의`덕 타이핑(Duct Typing)` 개념이 적용됩니다. `Robot` 클래스가 `Developer` 인터페이스를 `implements` 한 것은 아니지만 `Developer` 타입이 요구하는 `name` 프로퍼티와 `study` 메서드를 가지고 있고, `robotInstance`는 이 클래스의 인스턴스 이기때문에 타입 체크를 통과하게 됩니다. 44 | 45 | ## 구조적 타이핑 46 | 47 | 타입스크립트는 이렇게 자바스크립트 superset이기 때문에 이 특성을 그대로 따르기 때문에 타입스크립트의 타입시스템은 '구조적으로' 타입이 맞기만 한다면 타입 에러는 발생시키지 않습니다. 이러한 특성을 `구조적 타이핑(structural typing)`이라 부릅니다. 48 | 49 | 따라서 타입스크립트가 구조적 타이핑을 이해한다면 보다 결과를 예측하기 쉬워질 수 있습니다. 위의 덕 타이핑 예제에서 본 것 처럼 타입스크립트는 타입 확장에 대해 '열려' 있습니다. 명확한 상속관계(A - B)를 지향하는 명목적 타이핑과 다르게 구조적 타이핑은 `집합`으로 포함한다는 개념을 지향합니다. 50 | 51 | ```ts 52 | interface Vector2D { 53 | x: number; 54 | y: number; 55 | } 56 | 57 | function calculateLength(v: Vecotr2D) { 58 | return Math.sqrt(v.x * v.x + v.y * v.y); 59 | } 60 | 61 | interface NamedVector { 62 | name: string; 63 | x: number; 64 | y: number; 65 | } 66 | 67 | const v: NamedVector = { x: 3, y: 4, name: "zee" }; 68 | calculateLength(v); // OK , 결과 5 69 | ``` 70 | 71 | 위 예제에서 처럼 `Vector2D`와 `NamedVector`의 관계를 선언하지 않았음에도 `NamedVector`의 구조가 `Vector2D`와 호환되기 때문에 `calculateLength` 호출시 타입 에러가 발생하지 않습니다. 72 | 73 | ### 구조적 타이핑의 장점 74 | 75 | - 테스트를 작성할 때는 구조적 타이핑이 유리함 76 | - 라이브러리 간의 의존성을 완벽하게 분리할 수 있음 (item 51에서 계속) 77 | 78 | ```ts 79 | interface PostgresDB { 80 | //엄청 길고 복잡함 81 | //.. 82 | runQuery: (sql: string) => any[]; 83 | } 84 | interface Author { 85 | first: string; 86 | last: string; 87 | } 88 | 89 | function getAuthors(db: PostgresDB): Author[] { 90 | // authors 리턴 하는 로직 91 | } 92 | ``` 93 | 94 | `PostgresDB`를 테스트 한다고 할 때 테스트 코드에는 실제 환경에 대한 정보가 필요하지 않습니다. `getAuthors` 함수를 테스트 하기 위해서 함수 파라미터를 `PostgresDB`라는 실제 DB 구조에 대한 인터페이스가 아니라 구조적 타이핑 개념을 활용해서 좀 더 구체적인 인터페이스로 정의하는 것이 나은 방법입니다. 95 | 96 | ```ts 97 | interface DB { 98 | runQuery: (sql: string) => any[]; 99 | } 100 | 101 | function getAuthors(db: DB): Author[] { 102 | // authors 리턴 하는 로직 103 | } 104 | ``` 105 | 106 | `DB` 인터페이스에 `runQuery` 메서드가 있기 때문에 실제 환경에서도 `getAuthors`에 `PostgresDB`를 사용할 수 있습니다. 구조적 타이핑 덕분에 `PostgresDB`의 인터페이스를 명확히 선언할 필요가 없습니다. 추상화(`DB`)를 함으로써, 로직과 테스트를 특정한 구현(`PostgresDB`)으로 분리한 것입니다. 107 | 108 | ## 결론 109 | 110 | 타입스크립트는 구조적 타이핑이 가능하다. 타입스크립트의 유연한 특성을 잘 이해하고 사용하자. 111 | -------------------------------------------------------------------------------- /ch01_타입스크립트_알아보기/item05_jungho.md: -------------------------------------------------------------------------------- 1 | # 아이템5 any 타입 지양하기 2 | 3 | 타입스크립트의 타입 시스템은 점진적(gradual)이고 선택적(optional)이다. 4 | 5 | 코드에 타입을 조금씩 추가할 수 있어 점진적이고, 언제든지 타입 체커를 해제할 수 있기 때문에 선택적이다. 6 | 7 |
8 | 9 | 바꿔말하면, 타입을 통해 안정적인 코딩이 가능하도록 제한할 수 있지만, 자유도를 줄 수 있다는건데, 10 | 11 | 이 기능의 핵심은 `any` 타입이다. 12 | 13 | ```tsx 14 | let age: number; 15 | age = '12'; 16 | // ~~~ '"12"' 형식은 'number' 형식에 할당할 수 없습니다. 17 | age = '12' as any; // OK 18 | ``` 19 | 20 | 타입체커는 위와 같은 코드에서 `number` 타입으로 선언된 변수에 `string` 타입을 할당하면 타입오류를 내주는데, 21 | 22 | 여기서 `as any`를 추가해줌으로써 오류를 해결할 수 있다. 23 | 24 |
25 | 26 | 이처럼 타입 선언을 추가하기 귀찮거나, 당장 눈앞의 에러를 해결하기 위해 `any` 타입이나 `as any`를 사용하고 싶을 수 있다. 27 | 28 | 하지만, 이 같은 행위는 특별한 경우가 아니라면 **타입스크립트의 장점을 저버리는 행위**이다. (~~본 repo의 메인 이미지가 딱 이를 풍자하는 짤..~~) 29 | 30 | 부득이하게 `any`를 사용하더라도 이에 대한 위험성을 인지해야 한다. 31 | 32 |
33 | 34 | ## any 타입에는 타입 안정성이 없다 35 | 36 | 위 예제에서 `age`는 `number`타입으로 선언되었지만, `as any`를 사용함으로써 `string`타입을 할당할 수 있게 된다. 37 | 38 | 이때 타입 체커는 선언된대로 `age`를 `number`로 판단하게 되고, 이는 아래와 같은 부작용을 낳는다. 39 | 40 | ```tsx 41 | age += 1; // 런타임시 '121' 42 | ``` 43 | 44 |
45 | 46 | ## any는 함수 시그니처를 무시해 버린다 47 | 48 | 타입스크립트에서 함수를 작성할 때는 시그니처를 명시해야 한다. 49 | 50 | 약속된 타입의 입력을 제공하고, 함수는 약속된 타입의 출력을 반환한다. 이때 `any`타입을 사용하면 이 약속을 어기게 될 수 있다. 51 | 52 | ```tsx 53 | function calculateAge(birthDate: Date): number { 54 | // ... 55 | } 56 | let birthDate: any = '1990-01-19'; 57 | calculateAge(birthDate); // 정상 58 | ``` 59 | 60 | 위 예시처럼 `birthDate`매개변수는 `string`이 아니라 `Date`타입이어야 하지만, `any`타입을 사용함으로써 이를 무시해버릴 수 있다. 61 | 62 | 자바스크립트에서 발생하는 암시적 타입변환과 같은 부수효과를 낳을 수 있는 행위들을 제약하기 위해 타입스크립트를 사용하는 것인데, 이와 같은 행위는 **타입스크립트 사용목적에 반대**되는 행동이며, 코드의 문제를 야기할 수 있다. 63 | 64 |
65 | 66 | ## any 타입에는 언어 서비스가 적용되지 않는다 67 | 68 | `any`타입의 심벌을 사용하면 에디터가 속성에 대한 자동완성을 지원해주지 않는다. 69 | 70 | 또한, 에디터의 `Rename Symbol` 기능을 사용해도 `any`타입으로 선언된 심벌은 적용되지 않는다. 71 | 72 | 이는 개발 생산성에 있어서 마이너스인 부분이다. 73 | 74 |
75 | 76 | ## any 타입은 리팩토링시 버그를 감춘다 77 | 78 | 예를 들어, 아래와 같은 코드가 있을 때 79 | 80 | ```tsx 81 | interface ComponentProps { 82 | // 초기 개발 시 타입이 무엇인지 알 수 없어 any로 사용 83 | onSelectItem: (item: any) => void; 84 | } 85 | 86 | function renderSelector(props: ComponentProps) { 87 | /* ... */ 88 | } 89 | 90 | let selectedId: number = 0; 91 | 92 | function handleSelectItem(item: any) { 93 | selectedId = item.id; 94 | } 95 | 96 | renderSelector({ onSelectItem: handleSelectItem }); 97 | ``` 98 | 99 | 콜백함수에 실제로는 id만 필요함을 깨닫고 100 | 101 | ```tsx 102 | interface ComponentProps { 103 | // id만 필요하여 시그니처 수정 104 | onSelectItem: (id: number) => void; 105 | } 106 | ``` 107 | 108 | 다음과 같이 시그니처를 변경하여도, `handleSelectItem`은 `any`를 매개변수를 받도록 되어 있기 때문에, 타입 체커는 타입 오류를 발견하지 못한다. 109 | 110 | 이는 런타임시 에러발생으로 이어지게 된다. 111 | 112 |
113 | 114 | ## any는 타입 설계를 감춰버린다 115 | 116 | 상태 객체를 정의할 때 무분별하게 `any`타입을 사용해 버린다면, 이는 상태 객체의 설계를 감춰버린다. 117 | 118 | 깔끔하고 명료한 코드 작성을 위해서는 제대로 된 타입 설계가 필수다. 119 | 120 | `any`타입을 지양함으로써 타입 시스템과 타입 체커에 대한 신뢰도가 올라가게 되며, 그렇지 않다면 개발자는 개발을 하면서 타입 오류를 고쳐야 하고, 기존 자바스크립트 사용과 다름없이 머릿속에 실제 타입을 기억해야 한다. 121 | 122 | 어쩔 수 없이 `any`타입을 사용해야 하는 경우도 있지만, 5장에서 `any`의 단점을 어떻게 보완하는지 자세히 다룬다고 한다. 123 | -------------------------------------------------------------------------------- /ch02_타입스크립트의_타입_시스템/item06/img_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/11st-corp/effective-typescript/21099a52aa554a17a888ca5bde4b3c90d9fb0a1c/ch02_타입스크립트의_타입_시스템/item06/img_01.png -------------------------------------------------------------------------------- /ch02_타입스크립트의_타입_시스템/item06/img_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/11st-corp/effective-typescript/21099a52aa554a17a888ca5bde4b3c90d9fb0a1c/ch02_타입스크립트의_타입_시스템/item06/img_02.png -------------------------------------------------------------------------------- /ch02_타입스크립트의_타입_시스템/item06/img_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/11st-corp/effective-typescript/21099a52aa554a17a888ca5bde4b3c90d9fb0a1c/ch02_타입스크립트의_타입_시스템/item06/img_03.png -------------------------------------------------------------------------------- /ch02_타입스크립트의_타입_시스템/item06/img_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/11st-corp/effective-typescript/21099a52aa554a17a888ca5bde4b3c90d9fb0a1c/ch02_타입스크립트의_타입_시스템/item06/img_04.png -------------------------------------------------------------------------------- /ch02_타입스크립트의_타입_시스템/item06/img_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/11st-corp/effective-typescript/21099a52aa554a17a888ca5bde4b3c90d9fb0a1c/ch02_타입스크립트의_타입_시스템/item06/img_05.png -------------------------------------------------------------------------------- /ch02_타입스크립트의_타입_시스템/item06/img_06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/11st-corp/effective-typescript/21099a52aa554a17a888ca5bde4b3c90d9fb0a1c/ch02_타입스크립트의_타입_시스템/item06/img_06.png -------------------------------------------------------------------------------- /ch02_타입스크립트의_타입_시스템/item06/img_07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/11st-corp/effective-typescript/21099a52aa554a17a888ca5bde4b3c90d9fb0a1c/ch02_타입스크립트의_타입_시스템/item06/img_07.png -------------------------------------------------------------------------------- /ch02_타입스크립트의_타입_시스템/item07/extends_keyof.ts: -------------------------------------------------------------------------------- 1 | interface Point { 2 | x: number; 3 | y: number; 4 | } 5 | type PointKeys = keyof Point; 6 | 7 | function sortBy(vals: T[], key: K): T[] { 8 | // ... 9 | } 10 | const pts: Point[] = [{x: 1, y:1}, {x: 2, y: 0}] 11 | 12 | sortBy(pts, '') 13 | sortBy(pts, 'x') 14 | sortBy(pts, 'y') 15 | sortBy(pts, Math.random() < 0.5 ? 'x' : 'y') 16 | sortBy(pts, 'z') 17 | 18 | const tuple_1: [number, number] = [1,2]; 19 | const list_1: number[] = tuple_1 20 | 21 | const list_2 = [1, 2] 22 | const tuple_2: [number, number] = list_2; 23 | -------------------------------------------------------------------------------- /ch02_타입스크립트의_타입_시스템/item07_호찬.md: -------------------------------------------------------------------------------- 1 | # 아이템 7. 타입이 값들의 집합이라고 생각하기 2 | 3 | ### type 4 | 5 | '할당 가능한 값들의 집합'을 '타입' 또는 '타입의 범위'라고 한다. 6 | 7 | - 모든 숫자값의 집합: `number` 타입 8 | - `null`과 `undefined`는 `strictNullChecks` 여부에 따라 `number`에 해당될 수도 아닐 수도 있다. 9 | - `type score = number;` 10 | - 아무 값도 포함하지 않는 공집합: `never` 타입 11 | - 한 가지 값만 포함하는 타입: `unit` 또는 `literal` 타입 12 | - `type A = 'A';` 13 | - `type FourtyTwo = 42;` 14 | - 두개 혹은 세개의 이상의 값으로 묶인 타입: `union` 타입 15 | - `type AB = 'A' | 'B';` 16 | 17 | ### interface 18 | 19 | `interface`를 이용해서 원소를 서술하는 방법도 존재한다. 20 | 21 | ```typescript 22 | interface Identified { 23 | id: string; 24 | } 25 | ``` 26 | 27 | 두 타입의 교집합(intersection)을 계산하는 연산자는 `&` 연산자이다. 인터섹션 연산자는 각 타입 내의 속성을 모두 포함하는 것이 일반적인 규칙이다. 28 | 29 | ```typescript 30 | interface Person { 31 | name: string; 32 | } 33 | interface Lifespan { 34 | birth: Date; 35 | death?: Date; 36 | } 37 | type PersonSpan = Person & Lifespan; 38 | 39 | const ps: PersonSpan = { 40 | name: 'Alan Turing', 41 | birth: new Date('1912/06/23'), 42 | death: new Date('1954/06/07'), 43 | } 44 | ``` 45 | 46 | 두 인터페이스의 유니온에서는 속성에 대한 인터섹션을 하는것이 규칙이다. 따라서 아래 코드에서 유니온 타입에 속하는 값은 어떠한 키도 없기 떄문에, 유니온에 대한 keyof는 공집합(never)이어야 한다. 47 | 48 | ```typescript 49 | type K = keyof (Person | Lifesapn); 50 | ``` 51 | 52 | > 참고 53 | > 54 | > keyof (A&B) = (keyof A) | (keyof B) 55 | > 56 | > keyof (A|B) = (keyof A) & (keyof B) 57 | 58 | 조금 더 일반적으로 `PersonSpan`을 선언하는 방법은 `extends` 키워드를 사용하는 방법이 있다. 59 | 60 | ```typescript 61 | interface Person { 62 | name: string; 63 | } 64 | 65 | interface PersonSpan extends Person { 66 | birth: Date; 67 | death?: Date; 68 | } 69 | ``` 70 | 71 | `extends` 키워드는 제네릭 타입에서 한정자로도 쓰인다. `K`는 `string`의 부분 집합 범위를 가지는 어떠한 타입이다. 72 | 73 | ```typescript 74 | function getKey(val: any, key: K) { 75 | // ... 76 | } 77 | ``` 78 | 79 | ### Quiz 80 | 81 | 1. 다음 선언문은 컴파일 오류가 나지 않는다. (O / X) 82 | 83 | ```typescript 84 | const x: never = 12; 85 | ``` 86 | 87 |
88 | 정답 89 | 90 | **정답: X** 91 | 92 | ```typescript 93 | const x: never = 12; 94 | // ~ '12' 형식은 'never' 형식에 할당할 수 없습니다. 95 | ``` 96 | 97 | never 타입으로 선언된 변수의 범위는 공집합이기 때문에 아무런 값도 할당할 수 없다. (p.39) 98 |
99 | 100 | --- 101 | 102 | 2. 다음 중 컴파일 오류가 나지 않는 것들을 골라보자. 103 | 104 | ```typescript 105 | interface Point { 106 | x: number; 107 | y: number; 108 | } 109 | type PointKeys = keyof Point; 110 | 111 | function sortBy(vals: T[], key: K): T[] { 112 | // ... 113 | } 114 | const pts: Point[] = [{x: 1, y:1}, {x: 2, y: 0}] 115 | ``` 116 | 117 | 1. sortBy(pts, '') 118 | 2. sortBy(pts, 'x') 119 | 3. sortBy(pts, 'y') 120 | 4. sortBy(pts, Math.random() < 0.5 ? 'x' : 'y') 121 | 5. sortBy(pts, 'z') 122 | 123 |
124 | 정답 125 | 126 | **정답: 2,3,4** 127 | 128 | ```typescript 129 | // 'x' | 'y' 130 | type PointKeys = keyof Point; 131 | ``` 132 | 133 |
134 | 135 | --- 136 | 137 | 3. 다음 선언문은 typescript 컴파일 에러가 나지 않는다. (O / X) 138 | 139 | ```typescript 140 | const tuple_1: [number, number] = [1,2]; 141 | const list_1: number[] = tuple_1 142 | ``` 143 | 144 |
145 | 정답 146 | 147 | **정답: O** 148 | 149 | 타입스크립트는 숫자의 쌍을 `{0: number, 1: number, length: 2}`로 모델링하고, 튜플을 `{0: number, 1: number}`로 모델링한다. 150 |
151 | 152 | ### 참고 153 | 154 | - [keyof result contains undefined when used on mapped type with optional properties](https://github.com/microsoft/TypeScript/issues/34992) 155 | -------------------------------------------------------------------------------- /ch02_타입스크립트의_타입_시스템/item08_dami.md: -------------------------------------------------------------------------------- 1 | # 아이템 8. 타입 공간과 값 공간의 심벌 구분하기 2 | 3 | 타입스크립트의 심벌(symbol)은 이름이 같더라도 속하는 공간에 따라 타입일 수도 값일 수도 있기에 혼란스러울 수 있습니다. 4 | 5 | ```typescript 6 | interface Cylinder { 7 | radius: number; 8 | height: number; 9 | } // 타입 10 | 11 | const Cylinder = (radius: number, height: number) => ({radius, height}); // 값 12 | ``` 13 | 14 | 위 예제에서 `interface Culinder`는 `타입`, `const Cylinder`는 `값`으로 쓰입니다. 이런 점이 오류를 야기할 수 있습니다. 15 | 16 | ```typescript 17 | function calculateVolume(shape: unknown){ 18 | if(shape instanceof Cylinder) { 19 | shape.radius 20 | // 오류 : '{}' 형식에 'radius' 속성이 없습니다. 21 | } 22 | } 23 | ``` 24 | 25 | `instance of`는 자바스크립트의 런타임 연산자이고, 값에 대해서 연산을 하기에 `instance of Cylinder`는 타입이 아니라 함수를 참조합니다. 26 | 27 | ```typescript 28 | type T1 = 'string literal'; 29 | type T2 = 123; 30 | 31 | const v1 = 'string literal'; 32 | const v2 = 123; 33 | ``` 34 | 35 | `type` 이나 `interface` 다음에 나오는 심벌은 타입인 반면 `const`, `let` 선언에 쓰이는 것은 값입니다. 타입스크립트 코디에서 타입과 값은 번갈아 나올 수 있습니다. 36 | 37 | - `타입 선언(:)` 또는 `단언문(as)` 다음에 나오는 심벌은 `타입` 38 | - `=` 다음에 나오는 모든 것은 `값` 39 | 40 | `class`와 `enum`은 상황에 따라 `타입` 과 `값` 두가지 모두 가능한 **예약어**입니다. 41 | 42 | ```typescript 43 | class Cylinder { 44 | radius = 1; 45 | height = 1; 46 | } 47 | 48 | function calculateVolume(shape: unknown){ 49 | if(shape instanceof Cylinder) { 50 | shape // 정상, 타입은 Cylinder 51 | shape // 정상, 타입은 number 52 | } 53 | } 54 | ``` 55 | 56 | - 클래스가 `타입`으로 쓰일 때 : `형태(속성, 메서드)` 가 사용 57 | - 클래스가 `값`으로 쓰일 때 : `생성자`가 사용 58 | 59 | 60 | 61 | ```typescript 62 | type T1 = typeof p; // 타입은 Person 63 | type T2 = typeof email; // 타입은 64 | 65 | const v1 = typeof p; // 값은 "object" 66 | const v2 = typeof email; // 값은 "function" 67 | ``` 68 | 69 | **타입의 관점에서** 70 | 71 | - `typeof` 는 값을 읽어서 타입스크립트 타입을 반환 72 | - 타입 공간의 `typeof` 는 보다 큰 타입의 일부분으로 사용 가능 73 | - `type` 구문으로 이름을 붙이는 용도로 사용 가능 74 | 75 | 76 | 77 | **값의 관점에서** 78 | 79 | - 자바스크립트 런타임의 `typeof` 연산자 80 | - 대상 심벌의 런타임 타입을 가리키는 문자열을 반환 81 | - 자바스크립트 런타임 타입 6개 :`string`, `number`, `boolean`, `undefined`, `object`, `function` 82 | 83 | 84 | 85 | ```typescript 86 | class Cylinder { 87 | radius = 1; 88 | height = 1; 89 | } 90 | const v = typeof Cylinder; // 값이 "function" 91 | type T = typeof Cylinder; // 타입이 typeof Cylinder 92 | ``` 93 | 94 | 95 | 96 | ```typescript 97 | declare let fn: T; 98 | const c = new fn(); // 타입이 Cylinder 99 | 100 | type C = InstanceType; // 타입이 Cylinder 101 | ``` 102 | 103 | 위 예제처럼 `InstanceType` 제네릭을 사용해 생성자 타입과 인스턴스 타입을 전환할 수 있습니다. 104 | 105 | 106 | 107 | 타입의 속성을 얻을 때는 `obj.field`가 아니라 반드시 `obj['field']` 를 사용해야 합니다. 두 방법으로 얻은 값은 동일하더라도 **타입은 다를 수 있기 때문**입니다. 108 | 109 | 인덱스 위치에는 `유니온 타입`과 `기본형 타입`을 포함한 **어떠한 타입이든 사용**할 수 있습니다. 110 | 111 | ```typescript 112 | type PersonEl = Person['first' | 'last']; // 타입은 string 113 | type Tuple = [string, number, Date]; // 타입은 string | number | Date 114 | ``` 115 | 116 | ## 두 공간 사이에서 다른 의미를 가지는 코드 패턴 117 | 118 | ### this 119 | 120 | - 값으로 쓰이는 `this`는 자바스크립트의 `this` 키워드입니다. 121 | - 타입으로 쓰이는 `this`는 `다형성(polumorphic)` `this`라고 불리는 `this`의 타입스크립트 타입입니다. 서브클래스의 메서드 체인을 구현할 때 유용합니다. 122 | 123 | ### `&` 와 `|` 124 | 125 | - 값: `AND`와 `OR` 비트연산 126 | - 타입: `인터섹션`과 `유니온` 127 | 128 | ### const, as const 129 | 130 | - `const`: 새 변수를 선언 131 | - `as const`: 리터럴 또는 리터럴 표현식의 추론된 타입을 바꿉니다. 132 | 133 | ### extends 134 | 135 | - 서브클래스 (`class A extends B`) 136 | - 서브타입 (`interface A extends B`) 137 | - 제너릭 타입의 한정자를 정의할 수 있습니다. (``Generic``) 138 | 139 | ### in 140 | 141 | - 루프 (`for (key in object)`) 또는 매핑된 타입(`mapped`)에 등장합니다. 142 | 143 | 144 | 145 | 타입스크립트 코드가 잘 동작하지 않는다면 타입 공간과 값 공간을 혼동해서 작성했을 가능성이 큽니다. 예를 들어, 단일 객체 매개변수를 받도록 email 함수를 변경했다고 생각해 보겠습니다. 146 | 147 | ```typescript 148 | function email(options: { person: Person, subject: string, body: string }) { 149 | //... 150 | } 151 | ``` 152 | 153 | 자바스크립트에서는 객체 내의 각 속성을 로컬 변수로 만들어 주는 구조 분해(`destructuring`) 할당을 사용할 수 있습니다. 154 | 155 | ```typescript 156 | function email({ person, subject, body }){ 157 | //... 158 | } 159 | ``` 160 | 161 | 그런데 타입스크립트에서 구조 분해 할당을 하면, 이상한 오류가 발생합니다. 162 | 163 | ```typescript 164 | function email({ 165 | person: Person, // 오류: ~~바인딩 요소 'Person'에 암시적으로 'any' 형식이 있습니다. 166 | subject: string, // 'string' 식별자가 중복되었습니다. 167 | // 오류: ~~바인딩 요소 'Person'에 암시적으로 'any' 형식이 있습니다. 168 | subject: string, // 'string' 식별자가 중복되었습니다. 169 | // 오류: ~~바인딩 요소 'Person'에 암시적으로 'any' 형식이 있습니다. 170 | }) 171 | ``` 172 | 173 | 값의 관점에서 `Person`과`string`이 해석되었기 때문에 오류가 발생했습니다. `Person`이라는 변수명과 `string`이라는 이름을 가지는 두 개의 변수를 생성하려한 것입니다. 문제를 해결하려면 **타입과 값을 구분해야합니다.** 174 | 175 | ```typescript 176 | function email({ person, subject, body }: { person: Person, subject: string, body: string }){ 177 | //... 178 | } 179 | ``` 180 | 181 | 위와 같이 객체 자체에 대해 `type`을 지정해줘야 합니다. 182 | 183 | 184 | 185 | ## 더 생각해볼 거리: interface, type 네이밍 컨벤션 186 | 187 | 타입스크립트에서 interface를 정의할때 I-prefix를 붙이는 것은 권장되지 않는 추세입니다. 188 | 189 | ### 일관성을 파괴하는 네이밍 컨벤션 190 | 하나의 프로젝트 (또는 어떤 다른 기준)에서 snake_case를 사용한다던가, camelCase를 사용한다던가 이런 표기법은 일관성이 가장 중요합니다. 191 | 다른 변수나 함수 네이밍에는 헝가리식 표기법을 적용하지 않다가, 인터페이스에만 헝가리식 표기법을 적용하는 것은 잘못된 것입니다. 192 | 193 | ### 컨벤션의 목적과 사용되는 언어가 근본적으로 맞지 않는 문제 194 | 자바스크립트와 같은 언어에서 변수 또는 함수에 자료형을 드러내기 위해서 사용하던 헝가리식 표기법을 구조적 타이핑을 기반으로 하는 타입스크립트에 적용하는 것은 더욱 말이 안됩니다. 195 | 196 | 출처: [타입스크립트에서 interface 정의시 I- prefix를 권장하지 않는 이유](https://zereight.tistory.com/948) 197 | 198 | 199 | 200 | ## 결론 201 | 202 | 타입스크립트를 사용할 때는 값인지 타입인지 맥락을 잘 이해해서 적용하자. 203 | -------------------------------------------------------------------------------- /ch02_타입스크립트의_타입_시스템/item09/index.ts: -------------------------------------------------------------------------------- 1 | interface Person { 2 | name: string; 3 | }; 4 | 5 | const bob = { 6 | name: 'Bob', 7 | } as Person; 8 | const bob2 = { 9 | name: 'Bob', 10 | } -------------------------------------------------------------------------------- /ch02_타입스크립트의_타입_시스템/item09_호찬.md: -------------------------------------------------------------------------------- 1 | # 아이템 9. 타입 단언보다는 타입 선언을 사용하기 2 | 3 | ### 타입 단언과 타입 선언 4 | 5 | 아래 코드에서 `alice`는 타입 선언, `bob`은 타입 단언이다. 6 | 7 | ```typescript 8 | interface Person { 9 | name: string; 10 | }; 11 | 12 | // 타입 선언 13 | const alice: Person = { 14 | name: 'Alice', 15 | }; 16 | // 타입 단언 17 | const bob = { 18 | name: 'Bob', 19 | } as Person; 20 | const bob2 = { 21 | name: 'Bob2', 22 | } 23 | ``` 24 | 25 | ### 타입 단언을 지양해야하는 이유 26 | 27 | ```typescript 28 | interface Person { 29 | name: string; 30 | }; 31 | 32 | const alice: Person = {}; // Property 'name' is missing in type '{}' but required in type 'Person'.ts(2741) 33 | const bob = {} as Person 34 | ``` 35 | 36 | 타입 단언은 타입스크립트가 추론한 타입이 있더라도 단언된 타입으로 간주된다. `bob`에서는 오류가 발생하지 않는데, 타입 단언을 했으니 타입 체커에게 오류를 무시하라고 했기 떄문이다. 37 | 38 | 속성을 추가하는 경우에도 마찬가지이다. 타입 선언에서는 `잉여 속성 체크`가 동작하지만, 타입 단언에서는 동작하지 않기 때문이다. 39 | 40 | ```typescript 41 | interface Person { 42 | name: string; 43 | }; 44 | 45 | const alice: Person = { 46 | name: 'Alice', 47 | occupation: 'TypeScript developer', 48 | // Type '{ name: string; occupation: string; }' is not assignable to type 'Person'. 49 | // Object literal may only specify known properties, and 'occupation' does not exist in type 'Person'.ts(2322) 50 | }; 51 | 52 | const bob = { 53 | name: 'Bob', 54 | occupation: 'Javascript developer', 55 | } as Person; 56 | ``` 57 | 58 | ### 타입 단언을 사용하는 경우 59 | 60 | 타입스크립트는 DOM에 접근할 수 없다. 61 | 62 | ```typescript 63 | document.querySelector('#myButton').addEventListener('click', e => { 64 | // e.currentTarget의 타입은 EventTarget 65 | // button의 타입은 HTMLButtonElement 66 | const button = e.currentTarget as HTMLButtonElement; 67 | }) 68 | ``` 69 | 70 | 타입스크립트가 알지 못하는 정보를 우리는 알고 있기 때문에, 위 코드에서 타입 단언문을 사용하는 것은 타당하다. 71 | 72 | ### `!` 73 | 74 | ```typescript 75 | const elNull = document.getElementById('foo'); // 타입은 HTMLElement | null 76 | const el = document.getElementById('foo')!; // 타입은 HTMLElement 77 | ``` 78 | 79 | - 접두사로 쓰인 !는 boolean의 부정문이다. 80 | - 접미사로 쓰인 !는 그 값이 null이 아니라는 단언문이다. 81 | 82 | ### 요약 83 | 84 | - 타입 단언(`as Type`)보다 타입 선언(`: Type`)을 사용하자. 85 | - 타입스크립트보다 타입 정보를 더 잘 알고 있는 상황에서는 타입 단언문과 null 아님 단언문을 사용하자. 86 | -------------------------------------------------------------------------------- /ch02_타입스크립트의_타입_시스템/item10_dami.md: -------------------------------------------------------------------------------- 1 | # 아이템 10. 객체 래퍼 타입 피하기 2 | 3 | 자바스크립트 기본형들은 불변이며 메서드를 가지지 않는다는 점에서 객체와 구분됩니다. 4 | 5 | ```js 6 | 'primitive'.charAt(3) //"m" 7 | ``` 8 | 9 | 위 예제에서 사실 charAt은 string의 메서드가 아닙니다. 기본형 string에는 메서드가 없지만 자바스크립트에는 메서드를 가지는 String '객체' 타입을 자유롭게 반환합니다. string 기본형에 charAt 같은 메서드를 사용할 때, 자바스크립트는 기본형을 String 객체로 래핑 (Wrap)하고, 메서드를 호출하고 마지막에 래핑한 객체를 버립니다. 10 | 11 | > #### 래퍼 객체(wrapper object) 12 | > 13 | > [참고 링크](http://www.tcpschool.com/javascript/js_standard_object) 14 | > 15 | > ```js 16 | > const str = "문자열"; // 문자열 생성 17 | > 18 | > const len = str.length; // 문자열 프로퍼티인 length 사용 19 | > ``` 20 | > 21 | > 위의 예제에서 생성한 문자열 리터럴 str은 객체가 아닌데도 length 프로퍼티를 사용할 수 있습니다. 프로그램이 문자열 리터럴 str의 프로퍼티를 참조하려고 하면, 자바스크립트는 new String(str)을 호출한 것처럼 문자열 리터럴을 객체로 자동 변환해주기 때문입니다. 22 | > 23 | > 이렇게 생성된 임시 객체는 String 객체의 메소드를 상속받아 프로퍼티를 참조하는 데 사용됩니다. 24 | > 25 | > 이후 프로퍼티의 참조가 끝나면 사용된 임시 객체는 자동으로 삭제됩니다. 26 | > 27 | > 이렇게 숫자, 문자열, 불리언 등 원시 타입의 프로퍼티에 접근하려고 할 때 생성되는 임시 객체를 래퍼 객체(wrapper object)라고 합니다. 28 | > 29 | > 30 | > 31 | > ##### 예제 32 | > 33 | > ```js 34 | > const str = "문자열"; // 문자열 리터럴 생성 35 | > 36 | > const strObj = new String(str); // 문자열 객체 생성 37 | > 38 | > str.length; // 리터럴 값은 내부적으로* 래퍼 *객체를 생성한 후에 length 프로퍼티를 참조함. 39 | > 40 | > str == strObj; // 동등 연산자는 리터럴 값과 해당* 래퍼 *객체를 동일하게 봄. 41 | > 42 | > str === strObj; // 일치 연산자는 리터럴 값과 해당* 래퍼 *객체를 구별함. 43 | > 44 | > typeof str; // string 타입 45 | > 46 | > typeof strObj; // object 타입 47 | > ``` 48 | 49 | ```js 50 | //실제로는 이렇게 하지 마세요! 51 | const originalCharAt = String.prototype.charAt; 52 | String.prototype.charAt = function(pos) { 53 | console.log(this, typeof this, pos); 54 | return originalCharAt.call(this, pos); 55 | } 56 | console.log("primitive".charAt(3)); 57 | ``` 58 | 59 | 이 코드는 다음을 출력합니다 60 | 61 | ```js 62 | [String: "primitive"] "object" 3 63 | ``` 64 | 65 | 66 | 67 | 메서드 내의 `this`는 `string 기본형`이 아닌 `String 객체 래퍼`입니다. `String 객체 래퍼`는 `String 객체`를 직접 생성할 수도 있으며, string 기본형처럼 동작합니다. 68 | 69 | `string(기본형)`과 `String(객체 래퍼)`은 항상 동일하게 동작하지 않습니다. 예를 들어, `String 객체`는 오직 자기 자신하고만 동일합니다. 70 | 71 | ```js 72 | "hello" === new String("hello") //false 73 | new String("hello") === new String("hello") //false 74 | String === String //true 75 | ``` 76 | 77 | 78 | 79 | 객체 래퍼 타입의 자동 변환은 당황스러운 동작을 보일 때가 있습니다. 예를 들어 어떤 속성을 기본형에 할당했을 때 그 속성이 사라집니다. 80 | 81 | ```js 82 | x = "hello"; 83 | x.language = "English" 84 | x.language //undefined 85 | 86 | ``` 87 | 88 | - 실제로는 `x`가 `String` 객체로 변환된 후 language 속성이 추가되었고, language 속성이 추가된 객체는 버려진 것입니다. 89 | 90 | 91 | 92 | 다른 기본형에도 동일하게 객체 래퍼 타입이 존재합니다. 93 | 94 | - number - Number 95 | - Boolean - Boolean 96 | - symbol - Symbol 97 | - Bigint - BigInt 98 | 99 | `null`과 `undefined`에는 객체 래퍼가 없습니다. 100 | 101 | 이 래퍼 타입들 덕분에 기본형 값에 메서드를 사용할 수 있고, 정적 메서드 (``String.fromCharCode` 등)도 사용할 수 있습니다. 그러나 보통은 래퍼 객체를 직접 생성할 필요가 없습니다. 타입스크립트는 기본형과 객체 래퍼 타입을 별도로 모델링합니다. 102 | 103 | - string과 String 104 | - number와 Number 105 | - boolean과 Boolean 106 | - symbol과 Symbol 107 | - bigint와 BigInt 108 | 109 | `string`을 사용할 때는 특히 유의해야 하는데, `string`을 `String`이라고 잘못 타이핑하더라도 처음에는 잘 동작하는 것처럼 보이기 때문입니다. 110 | 111 | ```js 112 | function getStringLen(foo: String) { 113 | return foo.length; 114 | } 115 | 116 | getStringLen("hello"); //정상 117 | getStringLen(new String("hello")); //정상 118 | ``` 119 | 120 | 그러나 `string`을 매개변수로 받는 메서드에 `String 객체`를 전달하는 순간 문제가 발생합니다. 121 | 122 | ```js 123 | function isGreeting(phrase: String) { 124 | return [ 125 | 'hello', 126 | 'good day' 127 | ].includes(phrase); 128 | } 129 | ``` 130 | 131 | - `phrase`에서 에러 발생 : ``'String'`` 형식의 인수는 ``'string'`` 형식의 매개변수에 할당될 수 없습니다. ``'string'``은(는) 기본 개체이지만 ``'String'``은(는) 래퍼 개체입니다. 가능한 경우 ``'string'``을(를) 사용하세요. 132 | 133 | `string`은 `String`에 할당할 수 있지만 `String`은 `string`에 할당할 수 없습니다. 오류 메세지대로 `string` 타입을 사용해야 합니다. 134 | 135 | 136 | 137 | ```js 138 | const s: String = "primitive"; 139 | const n: Number = 12; 140 | const b: Boolean = true; 141 | ``` 142 | 143 | 위 예제에서 런타임의 값은 기본형입니다. 그러나 **기본형 타입은 객체 래퍼에 할당할 수 있기 때문**에 타입스크립트는 기본형 타입을 객체 래퍼에 할당하는 선언을 허용합니다. 그러나 기본형 타입을 객체 래퍼에 할당하는 구문은 오해하기 쉽고, 그렇게 할 필요도 없기 때문에 그냥 기본형 타입을 사용하는 것이 낫습니다. (아이템 19에서 계속) 144 | 145 | ### BigInt, Symbol 146 | 147 | ```js 148 | typeof BigInt(1234) // "bigint" 149 | typeof Symbol('sym') // "symbol" 150 | ``` 151 | 152 | 그런데 `new` 없이 `BigInt`와 `Symbol`을 호출하는 경우는 **기본형을 생성**하기 때문에 사용해도 좋습니다. 위 예제는 `bigint`와 `symbol`타입의 값이 됩니다. 153 | 154 | 155 | 156 | ## 결론 157 | 158 | - String 대신 string 159 | - Number 대신 number 160 | - Boolean 대신 boolean 161 | - Symbol 대신 symbol 162 | - BigInt 대신 bigint 163 | 164 | 를 사용하자. 165 | 166 | -------------------------------------------------------------------------------- /ch02_타입스크립트의_타입_시스템/item11/img_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/11st-corp/effective-typescript/21099a52aa554a17a888ca5bde4b3c90d9fb0a1c/ch02_타입스크립트의_타입_시스템/item11/img_01.png -------------------------------------------------------------------------------- /ch02_타입스크립트의_타입_시스템/item11/img_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/11st-corp/effective-typescript/21099a52aa554a17a888ca5bde4b3c90d9fb0a1c/ch02_타입스크립트의_타입_시스템/item11/img_02.png -------------------------------------------------------------------------------- /ch02_타입스크립트의_타입_시스템/item11_chanu.md: -------------------------------------------------------------------------------- 1 | # 잉여 속성 체크의 한계 인지하기 2 | 3 | > **타입이 명시된 변수**에 **객체 리터럴**을 할당할 때, 잉여 속성 체크를 수행합니다. 4 | 5 | - 변수가 가진 타입에 할당되는 객체가 실제로 타입이 가진 속성을 모두 가지고 있는지, 그 이외의 추가적인 속성을 가지고 있는지를 체크한다. 6 | 7 | ```typescript 8 | interface Room { 9 | numDoors: number; 10 | ceilingHeightFt: number; 11 | } 12 | 13 | const r: Room = { 14 | numDoors: 1, 15 | ceilingHeightFt: 10, 16 | elephant: 'present' 17 | } 18 | ``` 19 | - 여기서 `r`이라는 변수가 **타입이 명시된 변수** 이고, = 뒤에 나오는 `{}`이 바로 **객체 리터럴**이다. 20 | - 객체를 선언하는 시점에 해당 변수에 할당되는 것을 볼 수 있다. 즉, 객체 리터럴에 대한 잉여 속성 체크는 객체를 선언하는 시점에 수행된다. 21 | 22 |
23 | 24 | 25 | 26 | - 실제로 위 코드에서 `Room` 인터페이스 내에 존재하지 않는 `elephant` 속성을 변수에 할당하고자 할 때, 오류를 발생시키고 있다. 27 | 28 |
29 |
30 | 31 | > 하지만 구조적 타이핑 관점에서 **객체 리터럴**로 선언된 객체는 잉여 속성을 가질 뿐, `Room` 타입이 가진 속성을 모두 가지고 있기 때문에 `Room` 객체가 될 수 있다고 볼 수 있다. 32 | 33 | ```typescript 34 | const obj = { 35 | numDoors: 1, 36 | ceilingHeightFt: 10, 37 | elephant: 'present' 38 | } 39 | 40 | const r: Room = obj; 41 | ``` 42 | 43 | - 위의 설명과 같이, **객체 리터럴**로 선언된 객체 `obj`는 `Room` 타입의 변수 `r`에 할당이 가능하다. 44 | 45 |
46 | 47 | 두 상황의 차이점을 통해 객체가 선언되는 시점에 잉여 속성 체크가 수행되고, 이후 할당하는 시점에는 이루어지지 않는다는 것이다. 48 | - 첫 번째에서는 선언하는 시점과 할당하는 시점이 동일하였기 때문에 잉여 속성 체크를 수행하였다. 49 | - 두 번째에서는 타입의 정의 없이 객체가 선언되었고 잉여 속성 체크를 하지 않았다. 할당하는 시점에는 단순히 속성 체크(할당 가능 체크)만 수행하였다. 50 | 51 | 52 | 추가적으로, 53 | ```typescript 54 | interface Options { 55 | title: string; 56 | darkMode?: boolean; 57 | } 58 | 59 | const o1: Options = document; 60 | const o2: Options = new HTMLAnchorElement; 61 | ``` 62 | 63 | - `document`는 전역객체로, `title`이라는 `string` 타입의 속성을 가지고 있다. 또한 이미 선언되어있는 객체를 할당하기 때문에 할당 가능 체크를 수행하였다. 64 | - `new`연산자를 통해 객체를 생성하였고, 생성된 객체는 `title`이라는 `string` 타입의 속성을 가지고 있다. (실제로 HTMLElement 타입에 존재한다.) 또한 생성된 객체를 할당하기 때문에 할당 가능 체크를 수행하였다. 65 | 66 |
67 | 68 | > 생성하는 시점에 선언되는 것이 아닌가? 라는 생각이 든다면 맞다. 즉, 선언시점에 할당 가능 체크를 수행하기 때문이 아니라 **객체 리터럴**로 명시적으로 선언하였기 때문인 것이다. 결국 **객체 리터럴**을 통한 명시적 선언과 할당이 동시에 이루어졌을 때, 잉여 속성 체크가 발생한다고 볼 수 있겠다. 69 | 70 | 71 | ```typescript 72 | 73 | interface Options { 74 | title: string; 75 | } 76 | 77 | const o: Options = {darkMode : true, title:'Ski Free'} // error 78 | 79 | const obj2 = {darkMode : true, title:'Ski Free'} 80 | const o2: Options = obj2; 81 | ``` 82 | 83 | - `o`객체와 `obj2` 모두 **객체 리터럴**로 선언되었으나, 선언 시점에 타입 객체로 할당되었는가의 차이에 따라 달라지는 것을 볼 수 있다. 84 | 85 |

86 | 87 | 객체 리터럴을 사용하되 잉여 속성 체크를 하지 않으려면 세가지 방법이 존재한다. 88 | 89 | 90 | #### 1. 타입 단언문 사용 91 | 92 | ```typescript 93 | interface Options { 94 | title: string; 95 | } 96 | 97 | const o = {darkMode : true, title:'Ski Free'} as Options; 98 | ``` 99 | 100 | - 객체를 선언하고 난 이후에 타입을 지정하는 방식이므로, 선언하는 시점에는 잉여 속성 체크를 하지 않는다. 101 | 102 |
103 | 104 | #### 2. 인덱스 시그니처 사용 105 | ```typescript 106 | interface Options { 107 | title: string; 108 | [otherOptions: string] : unknown; 109 | } 110 | 111 | const o: Options = {darkMode : true, title:'Ski Free'}; 112 | ``` 113 | 114 | - 인덱스 시그니처를 사용하여 추가적인 속성이 존재한다는 것을 명시, 허용할 수 있다. 115 | 116 |
117 | 118 | #### 3. 약한 타입 사용 119 | 120 | ```typescript 121 | interface LineChartOptions { 122 | logscale?: boolean; 123 | invertedYAxis?: boolean; 124 | areaChart?: boolean; 125 | } 126 | 127 | const opts = {logScale: true}; 128 | const op: LineChartOptions = opts; 129 | ``` 130 | - `LineChartOptions`과 같이 Optional 속성만으로 선언된 타입을 약한 타입이라고 한다. 구조적 타이핑 관점에서 약한 타입은 어떠한 객체의 형태든 포함되기 때문에 매우 큰 범위의 타입으로 볼 수 있다. 즉, 타이핑의 의미가 존재하지 않는다. 131 | - TS는 약한 타입에 대해서 잉여 속성 체크, 할당 가능 체크가 아닌 공통된 속성에 대한 체크를 수행한다. 132 | - **잉여 속성 체크** - 할당되는 객체가 선언된 모든 속성을 가지고 있는가? + 이외의 속성을 가지고 있지는 않는가? 133 | - **할당 가능 체크** - 할당되는 객체가 선언된 모든 속성을 가지고 있는가? 134 | - **공통 속성 체크** - 할당되는 객체가 선언된 속성을 하나라도 가지고 있는가? 135 | 136 |
137 | 138 | 139 | - 실제로 `opts`객체는 `LineChartOptions`의 속성을 한 개도 가지고 있지 않다. 이로 인해 할당 시점에 오류를 발생시킨다. 140 | 141 | 142 |
143 | 144 | ### 정리하자면, 145 | 146 | #### 1. 잉여 속성 체크는 타입이 명시된 변수에 타입 리터럴을 할당할 때만 발생하며, 오류를 찾는 효과적인 방법이다. 147 | 148 | #### 2. 잉여 속성 체크를 회피하는 할 수 있는 방법은 다양한다. 149 | 150 | #### 3. 속성 체크 종류는 다음과 같다. 151 | | 타입 체크 종류 | 타입 체크 내용 | 타입 체크 조건 | 152 | |:------------------------------------:|:-------------------------------------------------------:|:------------------------------| 153 | | 잉여 속성 체크 | - 할당되는 객체가 선언된 모든 속성을 가지고 있는가?
- 이외의 속성을 가지고 있는가? | 타입이 명시된 변수에 **타입 리터럴**을 할당할 때 | 154 | | 할당 가능 체크 | - 할당되는 객체가 선언된 모든 속성을 가지고 있는가? | 타입이 명시된 변수에 객체를 할당할 때 | 155 | | 공통 속성 체크 | - 할당되는 객체가 선언된 속성을 하나라도 가지고 있는가? | **약한 타입**이 명시된 변수에 객체를 할당할 때 | 156 | 157 | --- -------------------------------------------------------------------------------- /ch02_타입스크립트의_타입_시스템/item12_jungho.md: -------------------------------------------------------------------------------- 1 | # 아이템12 함수 표현식에 타입 적용하기 2 | 3 | 자바스크립트에서는 함수 문장(함수선언문)과 함수표현식을 다르게 인식한다. 4 | 5 | ```tsx 6 | function rollDice1(sides: number): number { 7 | /* COMPRESS */ return 0; /* END */ 8 | } // Statement 9 | const rollDice2 = function (sides: number): number { 10 | /* COMPRESS */ return 0; /* END */ 11 | }; // Expression 12 | const rollDice3 = (sides: number): number => { 13 | /* COMPRESS */ return 0; /* END */ 14 | }; // Also expression 15 | ``` 16 | 17 | 타입스크립트에서는 함수 표현식을 사용하는 것이 좋다. 18 | 19 | 함수의 매개변수부터 반환값까지 전체를 함수 타입으로 선언하여 함수 표현식에 재사용할 수 있다는 장점이 있기 때문이다. 20 | 21 | ```tsx 22 | type DiceRollFn = (sides: number) => number; 23 | const rollDice: DiceRollFn = sides => { 24 | /* COMPRESS */ return 0; /* END */ 25 | }; 26 | ``` 27 | 28 |
29 | 30 | 함수 타입 선언의 장점 31 | 32 | - 불필요한 코드의 반복을 줄임 33 | - 하나의 함수 타입으로 통합 가능 34 | - 함수 매개변수에 타입 선언하는 것보다 코드가 간결해지고 안전해진다. 35 | 36 | ```tsx 37 | function add(a: number, b: number) { 38 | return a + b; 39 | } 40 | function sub(a: number, b: number) { 41 | return a - b; 42 | } 43 | function mul(a: number, b: number) { 44 | return a * b; 45 | } 46 | function div(a: number, b: number) { 47 | return a / b; 48 | } 49 | ``` 50 | 51 | ```tsx 52 | type BinaryFn = (a: number, b: number) => number; 53 | const add: BinaryFn = (a, b) => a + b; 54 | const sub: BinaryFn = (a, b) => a - b; 55 | const mul: BinaryFn = (a, b) => a * b; 56 | const div: BinaryFn = (a, b) => a / b; 57 | ``` 58 | 59 | 위 예제는 함수 타입 선언을 이용했던 예제보다 타입 구문이 적다. 60 | 61 | 함수 구현부도 분리 되어 있어 로직이 분명해진다. 62 | 63 | 모든 함수 표현식의 반환타입까지 `number`로 선언한 셈이다. 64 | 65 |
66 | 67 | 라이브러리에서는 공통 함수 시그니처를 타입으로 제공하기도 한다 68 | 69 | - 이를 활용하면 코드가 간결하고 안전해진다 70 | 71 | 예를 들어, `lib.dom.d.ts`를 보면 아래와 같이 `fetch` 함수에 대한 시그니처가 선언되어 있다. 72 | 73 | ```tsx 74 | declare function fetch(input: RequestInfo, init?: RequestInit): Promise; 75 | ``` 76 | 77 | 이를 활용하면 78 | 79 | ```tsx 80 | // 아래와 같이 매개변수에 타입을 정의하는 것 대신 81 | async function checkedFetch(input: RequestInfo, init?: RequestInit) { 82 | const response = await fetch(input, init); 83 | if (!response.ok) { 84 | throw new Error('Request failed: ' + response.status); 85 | } 86 | return response; 87 | } 88 | 89 | // 이런식으로 typeof를 활용하여 간결하게 작성할 수 있다 90 | const checkedFetch: typeof fetch = async (input, init) => { 91 | const response = await fetch(input, init); 92 | if (!response.ok) { 93 | throw new Error('Request failed: ' + response.status); 94 | } 95 | return response; 96 | }; 97 | ``` 98 | 99 |
100 | 101 | 요약 102 | 103 | - 매개변수나 반환 값에 타입을 명시하기보다는 함수 표현식 전체에 타입구문을 적용하는 것이 좋다 104 | - 만약 같은 타입 시그니처를 반복적으로 작성한 코드가 있다면 함수 타입을 분리해 내거나 이미 존재하는 타입을 찾아보도록 한다. 라이브러리를 직접 만든다면 공통 콜백에 타입을 제공해야한다. 105 | - 다른 함수 시그니처를 참조하려면 `typeof fn`을 사용하면 된다. 106 | -------------------------------------------------------------------------------- /ch02_타입스크립트의_타입_시스템/item13_혁주.md: -------------------------------------------------------------------------------- 1 | # 타입과 인터페이스의 차이점 알기 2 | 3 | 타입스크립트에서 명명된 타입을 정의하는 방법은 두 가지가 있습니다. 4 | 5 | - Type 6 | - Interface 7 | 8 | ```typescript 9 | type TState = { 10 | name: string; 11 | capital: string; 12 | }; 13 | 14 | interface IState { 15 | name: string; 16 | capital: string; 17 | } 18 | ``` 19 | 20 | > 앞으로의 예제에서는 Type과 Interface를 구분하기 위해 prefix(접두사)를 사용했지만, 실제 코드에서는 이렇게 사용하면 안 됩니다.(C#에서 영향을 받아서 초창기에는 이렇게 작성되었지만 지양해야 할 스타일로 여겨짐) FE개발팀에서도 기존의 prefix를 제거하는 방향으로 가고 있습니다. 21 | 22 | 대부분의 경우에는 타입을 사용해도 되고 인터페이스를 사용해도 됩니다. 그러나 둘 중 하나로 통일하여 일관성을 지키는 것이 중요합니다. 23 | 24 | 공통점과 차이점을 분명하게 알고 사용하는 것과 모르고 사용하는 것은 큰 차이가 있기에, 지금부터 공통점과 차이점을 알아보도록 하겠습니다. 25 | 26 |
27 | 28 | ## 타입과 인터페이스의 공통점 29 | 30 | 제너릭이 가능합니다. 31 | 32 | ```typescript 33 | type TPair = { 34 | first: T; 35 | second: T; 36 | }; 37 | 38 | interface IPair { 39 | first: T; 40 | second: T; 41 | } 42 | ``` 43 | 44 | 클래스에서 구현(implements)이 가능합니다. 45 | 46 | ```typescript 47 | class StateT implements TState { 48 | name: string = ''; 49 | capital: string = ''; 50 | } 51 | 52 | class StateI implements IState { 53 | name: string = ''; 54 | capital: string = ''; 55 | } 56 | ``` 57 | 58 | 확장이 가능하고, 인터페이스가 타입을 확장하거나 타입이 인터페이스를 확장할 수 있습니다. 59 | 60 | ```typescript 61 | interface IStateWithPop extends TState { 62 | population: number; 63 | } 64 | 65 | type TStateWithPop = IState & { population: number }; //intersection type 66 | ``` 67 | 68 | > 주의: 인터페이스는 유니온 타입같은 복잡한 타입을 확장하지 못합니다. 따라서 복잡한 타입을 확장하고 싶다면 타입과 &를 활용해야 합니다. 69 | 70 |
71 | 72 | ## 타입과 인터페이스의 차이점 73 | 74 | 타입만 유니온 타입을 사용할 수 있습니다. 75 | 76 | ```typescript 77 | type AorB = 'A' | 'B'; 78 | ``` 79 | 80 | - 보통의 경우 type 키워드가 interface보다 쓰임새가 많습니다. 81 | 82 | 인터페이스만 보강 기법을 사용할 수 있습니다. 83 | 84 | ```typescript 85 | interface IState { 86 | name: string; 87 | capital: string; 88 | } 89 | 90 | interface IState { 91 | population: number; 92 | } 93 | 94 | const wyoming: IState = { 95 | name: 'Wyoming', 96 | capital: 'Cheyenne', 97 | population: 500000, 98 | }; 99 | 100 | type TState = { 101 | // ~~~ Duplicate identifier 'TState'. 102 | name: string; 103 | capictal: string; 104 | }; 105 | 106 | type TState = { 107 | // ~~~ Duplicate identifier 'TState'. 108 | population: number; 109 | }; 110 | ``` 111 | 112 | - 위 예제처럼 속성을 확장하는 것을 선언 병합(declaration merging)이라고 합니다. 113 | 114 |
115 | 116 | ## 언제 어떤 것을 사용해야 할까? 117 | 118 | - 복잡한 타입(ex 유니온 타입)이라면 타입을 사용하면 됩니다. 119 | 120 | - 둘 다 사용가능한 경우 121 | 122 | 1. 일관성: 일관되게 인터페이스를 사용하는 환경에서는 인터페이스를, 일관되게 타입을 사용하는 환경에서는 타입을 사용하면 됩니다 123 | 124 | 2. 보강: 아직 스타일이 확립되지 않은 프로젝트라면 향후에 보강의 가능성이 있을지 생각해 봐야 합니다. 125 | - 어떤 **API**에 대한 타입 선언을 작성해야 한다면 인터페이스를 사용하는게 좋습니다. API가 변경될 때 사용자가 인터페이스를 통해 새로운 필드를 병합할 수 있어 유용하기 때문입니다. 126 | - 프로젝트 내부적으로 사용되는 타입에 선언 병합이 발생하는 것은 잘못된 설계입니다(간단하게 생각해봐도 오류를 발생시키기 쉬운 구조). 이럴 때는 타입을 사용해야 합니다. 127 | 128 | ## 결론 129 | 130 | - 타입과 인터페이스의 공통점과 차이점을 이해하여 적재적소에 활용해야 합니다. 131 | - 프로젝트에서 어떤 문법을 사용할지 결정할 때 한 가지 일관된 스타일을 확립해야 하고, 보강 기법이 필요한지 고려해야 합니다. 132 | -------------------------------------------------------------------------------- /ch02_타입스크립트의_타입_시스템/item14_혁주.md: -------------------------------------------------------------------------------- 1 | # 타입 연산과 제너릭 사용으로 반복 줄이기 2 | 3 | 혹시 **DRY**(Don't Repeat Yourself) 라는 조언을 들어보셨나요? 4 | 5 | 한국말로 말하면 `반복하지 마라!`의 약자인데요, 시스템 내에서 특정한 기능이나 로직은 단 한곳에서 명확하고 신뢰할 수 있게 존재해야 합니다. 6 | 7 | 비슷한 코드가 반복된다면, 수정이 일어날 경우 반복되는 모든 곳에서 동일하게 수정을 해줘야 하고 실수할 확률이 올라갑니다. 8 | 9 | 그런데 반복된 코드를 열심히 제거하며 DRY 원칙을 지켰던 개발자라도 타입에 관해서는 잘 지키지 못했을지도 모릅니다. 10 | 11 | 타입 시스템에 어떻게 **DRY 원칙**을 잘 적용할 수 있을까요? 12 | 13 |
14 | 15 | ## 타입에 이름 붙이기 16 | 17 | 반복을 줄이는 가장 간단한 방법은 타입에 이름을 붙여주는 것입니다. 18 | 19 | ```typescript 20 | function distance(a: { x: number; y: number }, b: { x: number; y: number }) { 21 | return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2)); 22 | } 23 | ``` 24 | 25 | ```typescript 26 | interface Point2D { 27 | x: number; 28 | y: number; 29 | } 30 | 31 | function distance(a: Point2D, b: Point2D) { 32 | return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2)); 33 | } 34 | ``` 35 | 36 | 중복된 타입은 종종 문법에 의해 가려지기도 합니다. 37 | 38 | ```typescript 39 | function get(url: string, opts: Options): Promise { 40 | /* ... */ 41 | } 42 | function post(url: string, opts: Options): Promise { 43 | /* ... */ 44 | } 45 | ``` 46 | 47 | ```typescript 48 | type HTTPFunction = (url: string, opts: Object) => Promise; 49 | const get: HTTPFunction = (url, opts) => { 50 | /* ... */ 51 | }; 52 | const post: HTTPFunction = (url, opts) => { 53 | /* ... */ 54 | }; 55 | ``` 56 | 57 | > 같은 타입 시그니처를 공유한다면 해당 시그니처를 명명된 타입으로 분리 58 | 59 |
60 | 61 | ## 타입의 확장 62 | 63 | ```typescript 64 | interface Person { 65 | firstName: string; 66 | lastName: string; 67 | } 68 | 69 | interface PersonWithBirthDate { 70 | firstName: string; 71 | lastName: string; 72 | birth: Date; 73 | } 74 | ``` 75 | 76 | ```typescript 77 | interface Person { 78 | firstName: string; 79 | lastName: string; 80 | } 81 | 82 | interface PersonWithBirthDate extends Person { 83 | birth: Date; 84 | } 85 | ``` 86 | 87 | > 한 인터페이스가 다른 인터페이스를 확장하여 반복을 제거 88 | 89 | ```typescript 90 | type PersonWithBirthDate = Person & { birth: Date }; 91 | ``` 92 | 93 | > 이미 존재하는 타입을 확장할 때, 일반적이지는 않지만 인터섹션 연산자를 사용할 수도 있습니다. 94 | 95 |
96 | 97 | ## Pick 의 사용 98 | 99 | 전체 애플리케이션의 상태를 표현하는 State 타입과 단지 부분만 표현하는 TopNavState가 있는 경우를 살펴보겠습니다. 100 | 101 | ```typescript 102 | interface State { 103 | userId: string; 104 | pageTitle: string; 105 | recentFiles: string[]; 106 | pageContents: string; 107 | } 108 | 109 | interface TopNavState { 110 | userId: string; 111 | pageTitle: string; 112 | recentFiles: string[]; 113 | } 114 | ``` 115 | 116 | 이럴 경우 State를 인덱싱하는 방법이 있습니다. 117 | 118 | ```typescript 119 | interface TopNavState { 120 | userId: State['userId']; 121 | pageTitle: State['pageTitle']; 122 | recentFiles: State['recentFiles']; 123 | } 124 | ``` 125 | 126 | 그러나 중복 제거가 아직 끝나지 않았습니다. 이 때 `매핑된 타입`을 사용하면 좀 더 나아집니다. 127 | 128 | ```typescript 129 | type TopNavState = { 130 | [k in 'userId' | 'pageTitle' | 'recentFiles']: State[k]; 131 | }; 132 | ``` 133 | 134 | 마우스를 올렸을 때 기존의 type과 동일함을 확인할 수 있습니다. 135 | 136 | 스크린샷 2022-10-05 오전 1 34 57 137 | 138 | 매핑된 타입은 배열의 필드를 루프 도는 것과 같은 방식입니다. 이 패턴은 표준 라이브러리에서도 일반적으로 찾을 수 있으며, PICK 이라고 합니다. 139 | 140 | ```typescript 141 | // 완벽한 정의는 아님 142 | type Pick = { 143 | [k in K]: T[k]; 144 | }; 145 | ``` 146 | 147 | ```typescript 148 | type TopNavState = Pick; 149 | ``` 150 | 151 | > 여기서 Pick은 제너릭 타입입니다. 마치 함수에서 두 개의 매개변수 값을 받아서 결괏값을 반환하는 것처럼, Pick은 T,K 두 가지 타입을 받아서 결과 타입을 반환합니다. 152 | 153 |
154 | 155 | ## Partial의 사용 156 | 157 | 생성하고 난 다음에 업데이트가 되는 클래스를 정의한다면, update 메서드 매개변수 타입은 생성자와 동일한 매개변수이면서, 타입 대부분이 선택적 필드가 됩니다. 158 | 159 | ```typescript 160 | interface Options { 161 | width: number; 162 | height: number; 163 | color: string; 164 | label: string; 165 | } 166 | interface OptionUpdate { 167 | width?: number; 168 | height?: number; 169 | color?: string; 170 | label?: string; 171 | } 172 | class UIwidget { 173 | constructor(init: Options) {...} 174 | update(options: OptionUpdate){...} 175 | } 176 | ``` 177 | 178 | 매핑된 타입과 `keyof`를 사용하면 Options로부터 OptionsUpdate를 만들 수 있습니다. 179 | 180 | ```typescript 181 | type OptionsUpdate = { [k in keyof Options]?: Options[k] }; 182 | ``` 183 | 184 | `keyof`는 타입을 받아서 속성 타입의 유니온을 반환합니다. 185 | 186 | ```typescript 187 | type OptionsKeys = keyof Options; 188 | // 타입이 "width" | "height" | "color" | "label" 189 | ``` 190 | 191 | 이 패턴 역시 일반적이며 표준 라이브러리에 `Partial`이라는 이름으로 포함되어 있습니다. 192 | 193 | ```typescript 194 | interface Options { 195 | width: number; 196 | height: number; 197 | color: string; 198 | label: string; 199 | } 200 | 201 | class UIwidget { 202 | constructor(init: Options) {...} 203 | update(options: Partial){...} 204 | } 205 | ``` 206 | 207 |
208 | 209 | ## 제너릭 타입에서 매개변수를 제한하는 방법 210 | 211 | 제네릭 타입은 타입을 위한 함수와 같습니다. 그리고 함수는 코드에 대한 DRY 원칙을 지킬 때 유용하게 사용됩니다. 212 | 213 | 여기서 생각해 볼 점이 있습니다. 214 | 215 | 함수에서 매개변수로 매핑할 수 있는 값을 제한하기 위해 타입 시스템을 사용합니다. 그럼 제네릭 타입에서 매개변수를 제한할 수 있는 방법은 무엇일까요? 216 | 217 | > 바로 extends를 사용하는 것입니다. 218 | 219 | ```typescript 220 | interface Name { 221 | first: string; 222 | last: string; 223 | } 224 | type DancingDuo = [T, T]; 225 | 226 | const couple1: DancingDuo = [ 227 | { first: 'Fred', last: 'Astaire' }, 228 | { first: 'Ginger', last: 'Rogers' }, 229 | ]; 230 | 231 | const couple2: DancingDuo<{ first: string }> = [ 232 | // ~~~ 'Name' 타입에 필요한 'last' 속성이 {first: string} 타입에 없습니다. 233 | { first: 'Fred' }, 234 | { first: 'Ginger' }, 235 | ]; 236 | ``` 237 | 238 | 앞에서 나온 Pick의 정의도 extends를 사용해서 완성할 수 있습니다. 239 | 240 | ```typescript 241 | type Pick = { 242 | [k in K]: T[k]; 243 | }; 244 | ``` 245 | 246 | 기존 정의에서 K는 T와 무관하고 범위도 너무 넓습니다. K는 실제로 T의 key의 부분집합, 즉 keyof T 가 되어야 합니다. 247 | 248 | ```typescript 249 | type Pick = { 250 | [k in K]: T[K]; 251 | }; 252 | ``` 253 | 254 | ## 요약 255 | 256 | - DRY 원칙을 타입에도 최대한 적용해야 합니다. 257 | - 타입에 이름을 붙여서 반복을 피해야 합니다. extends를 사용해서 인터페이스 필드의 반복을 피해야 합니다. 258 | - 타입들 간의 매핑을 위해 타입스크립트가 제공한 도구들을 공부하면 좋습니다. 259 | - keyof, typeof, 인덱싱, 매핑된 타입들이 포함 260 | - 제너릭 타입은 타입을 위한 함수와 같습니다. 제너릭 타입을 제한하려면 extends를 사용하면 됩니다. 261 | - 표준 라이브러리에 정의된 Pick, Partial같은 제너릭 타입에 익숙해져야 합니다. 262 | -------------------------------------------------------------------------------- /ch02_타입스크립트의_타입_시스템/item15_호찬.md: -------------------------------------------------------------------------------- 1 | # 아이템 15. 동적 데이터에 인덱스 시그니처 사용하기 2 | 3 | ### 인덱스 시그니처 4 | 5 | 타입스크립트에서는 타입에 `인덱스 시그니처`를 명시하여 유연하게 매핑을 표현할 수 있다. 6 | 7 | ```typescript 8 | type Rocket = { [property: string]: string }; 9 | 10 | const rocket: Rocket = { 11 | name: 'Falcon 9', 12 | variant: 'v1.0', 13 | thrust: '4,940 kN', 14 | }; 15 | ``` 16 | 17 | `[property: string]: string`가 인덱스 시그니처이며, 아래와 같은 특징을 갖는다. 18 | 19 | - **키의 이름:** 키의 위치만 표시하는 용도로, 타입 체커에서는 사용하지 않는다. 20 | - **키의 타입:** `string`, `number` 혹은 `symbol`의 조합으로 구성되어야 한다. 21 | - **값의 타입:** 모든 타입이 가능하다. 22 | 23 | ### 인덱스 시그니처 단점 24 | 25 | - **모든 키를 허용한다.** `name` 대신 `Name`으로 작성해도 유효한 `Rocket` 타입으로 취급된다. 26 | - **특정 키가 필요하지 않다.** `{}`도 유효하게 취급된다. 27 | - **키마다 다른 타입을 가질 수 없다.** `thrust`만 `number` 타입을 갖고 싶어도 불가능하다. 28 | - **타입스크립트에서 제공하는 언어 서비스를 이용하지 못한다.** 키는 무엇이든지 가능하기 때문에 자동 완성 기능이 동작하지 않는다. 29 | 30 | ### 인덱스 시그니처의 적절한 사용 용도 31 | 32 | `인덱스 시그니처`는 타입이 부정확해서 런타임 때까지 객체의 속성을 모르는 임의의 데이터에 대해 사용하는게 좋다. 33 | 34 | 객체의 속성을 알고 있는 데이터에 대해서는 `인터페이스`와 같은 정확한 타입을 정의할 수 있는 문법을 사용하자. 35 | 36 | ```typescript 37 | interface Rocket { 38 | name: string; 39 | variant: string; 40 | thrust_kN: number; 41 | } 42 | const falconHeavy: Rocket = { 43 | name: 'Falcon Heavy', 44 | variant: 'v1', 45 | thrust_kN: 15_200, 46 | } 47 | ``` 48 | 49 | 이제 타입스크립트에서 제공하는 언어 서비스를 이용할 수 있다. (자동 완성, 정의로 이동, 이름 바꾸기 등등) 50 | 51 | 모르는 임의의 데이터에 대해 `인덱스 시그니처`를 사용하되, 열 이름을 알고 있는 특정한 상황으로 좁혀진다면 미리 선언된 타입 단언문을 사용할 수 있다. 52 | 53 | ```typescript 54 | function parseCSV(input: string): {[columnName: string]: string}[]{ 55 | //... 56 | } 57 | 58 | interface ProductRow { 59 | productId: string; 60 | name: string; 61 | price: string; 62 | } 63 | 64 | declare let csvData: string; 65 | const products = parseCSV(csvData) as unknown as ProductRow[]; 66 | ``` 67 | 68 | ### 정확한 타입을 정의할 수 있는 방법들 69 | 70 | ```typescript 71 | interface Row1 { [column: string]: number } 72 | interface Row2 { a: number; b?: number; c?: number; d?: number; } // 그나마 최선 73 | type Row3 = 74 | | { a: number; } 75 | | { a: number; b: number; } 76 | | { a: number; b: number; c: number; } 77 | | { a: number; b: number; c: number; d: number; } 78 | ``` 79 | 80 | `Row1`는 너무 광범위하며, `Row3`가 가장 정확하지만 사용하기 번거롭다. 81 | 82 | 해당 경우에서는 `Record`나 `매핑된 타입`을 활용해볼 수 있다. 83 | 84 | #### Record 85 | 86 | ```typescript 87 | type Vec3D = Record<'x' | 'y' | 'z', number>; 88 | // Type Vec3D = { 89 | // x: number; 90 | // y: number; 91 | // z: number; 92 | //} 93 | ``` 94 | 95 | #### 매핑된 타입 96 | 97 | ```typescript 98 | type Vec3D = {[k in 'x' | 'y' | 'z']: number}; 99 | // Type Vec3D = { 100 | // x: number; 101 | // y: number; 102 | // z: number; 103 | //} 104 | 105 | type ABC = {[k in 'a' | 'b' | 'c']: k extends 'b' ? string : number}; 106 | // Type Vec3D = { 107 | // a: number; 108 | // b: string; 109 | // c: number; 110 | //} 111 | ``` 112 | 113 | ### 결론 114 | 115 | - 런타임 때까지 객체의 속성을 알 수 없는 경우에만 `인덱스 시그니처`를 사용한다. 116 | - 안전한 접근을 위해 `인덱스 시그니처`를 사용할 때, 값 타입에 `undefined`를 추가하는 것을 고려해보아야 한다. 117 | - 가능하다면 `인덱스 시그니처`보다는 `인터페이스`, `Record`, `매핑된 타입`등 정확한 타입을 사용하자. 118 | -------------------------------------------------------------------------------- /ch02_타입스크립트의_타입_시스템/item16_jungho.md: -------------------------------------------------------------------------------- 1 | # 아이템 16 number 인덱스 시그니처보다 Array, 튜플, ArrayLike 사용하기 2 | 3 | 자바스크립트에서 객체의 키값은 항상 문자열만 올 수 있다. 4 | 5 | 하지만 배열은 분명히 객체이지만 숫자로 인덱싱 할 수 있다. 6 | 7 | ```tsx 8 | const x = [1, 2, 3]; 9 | 10 | console.log(x[0]); // 1 11 | ``` 12 | 13 | ```tsx 14 | Object.keys(x); // ['0', '1', '2'] 15 | ``` 16 | 17 | 하지만 실제로 배열의 키를 나열하면 키가 문자열로 출력된다. 18 | 19 | > 실제로 자바스크립트의 배열은 배열이 아닌 객체로 배열의 동작을 흉내낸 객체이기 때문이다. 20 | > 21 | > 참고 : [https://poiemaweb.com/js-array-is-not-arrray](https://poiemaweb.com/js-array-is-not-arrray) 22 | 23 |
24 | 25 | 타입스크립트는 위의 혼란을 바로잡기 위해, **_숫자 키를 허용_**하고 문자열 키와 다른 것으로 인식한다. 26 | 27 | ```tsx 28 | interface Array { 29 | [n: number]: T; 30 | } 31 | ``` 32 | 33 | 이를 통해 타입 체크 시점에 오류를 잡아준다.(인덱스 시그니처로 사용된 `number`타입은 버그를 잡기 위한 순수 TS 코드) 34 | 35 |
36 | 37 | 때문에, 배열을 순회하는 방법에 있어 차이를 알아보자. 38 | 39 | ```tsx 40 | const xs = [1, 2, 3] 41 | console.log(xs['0']) // 에러 발생(인덱스 식이 number형식이 아닌 string이라 에러) 42 | // 타입 시스템에서 string이 number에 할당될 수 없음 43 | 44 | --- 45 | 46 | for(const key in xs) { 47 | key // string 48 | cosnt x = xs[key] // number 49 | } 50 | ``` 51 | 52 | 위의 같이 `for...in`에 사용되는 상황은 실용적인 허용이라고 생각하는 것이 좋다 53 | 54 | 또한, `for...in` 을 사용하는게 배열을 순회하기에 좋은 방법이 아니다. 55 | 56 | - 따라서, 인덱스가 필요하지 않는 경우 `for...of` 를 사용하는 것이 좋다. 57 | - 만약 인덱스가 필요한 경우, `forEach` 메서드를 사용하면 된다. 58 | - 순회 중간에 멈춰야 하는 경우 `for(;;)` 방법을 사용하는 것이 좋다. 59 | - 또, `for...in` 의 경우 일반적으로 다른 배열 순회 방법보다 몇 배나 느리다 60 | 61 |
62 | 63 | 요약하면, 64 | 65 | - 배열은 객체이므로 `key`는 숫자가 아닌 문자열이다. 인덱스 시그니처로 사용되는 `number`타입은 버그를 잡기 위한 타입스크립트의 코드이다. 66 | - `number`를 인덱스 시그니처로 사용하기 보다, `Array`나 `튜플`, `ArrayLike` 타입을 사용하는 것이 좋다. 67 | - 일반적으로 `key` 자리에 `string` 대신 `number`를 타입으로 사용할 이유가 많지 않기 때문이다. 68 | - 이는 오히려 숫자 속성이 어떤 특별한 의미를 가지고 있다는 오해를 불러 일으킬 수 있다. 69 | - 또한, `push`, `concat`등 `Array` 메서드가 불필요하고, 인덱스와 길이등 일반적인 배열같은 정보만 필요한 경우 `ArrayLike`를 사용하는 것이 좋다 70 | -------------------------------------------------------------------------------- /ch02_타입스크립트의_타입_시스템/item17_dami.md: -------------------------------------------------------------------------------- 1 | # 아이템 17. 변경 관련된 오류 방지를 위해 readonly 사용하기 2 | 3 | 4 | 5 | ```typescript 6 | function arraySum(arr: number[]) { 7 | let sum = 0, num; 8 | while ((num = arr.pop()) !== undefined) { 9 | sum += num; 10 | } 11 | return sum; 12 | } 13 | ``` 14 | 15 | 위 함수는 배열 안의 숫자를 모두 합치는 계산이 끝나면 원래 배열은 전부 비게됩니다. 자바스크립트는 배열의 내용을 변경할 수 있기 때문에, 타입스크립트에서도 오류가 발생하지 않습니다. 16 | 17 | 위 함수가 배열을 변경했을 때 발생할 수 있는 오류의 범위를 좁히기 위해 `readonly` 접근 제어자를 사용하여 `arraySum`이 배열을 변경하지 않는다는 선언을 할 수 있습니다. 18 | 19 | ```typescript 20 | function arraySum(arr: readonly number[]) { 21 | let sum = 0, num; 22 | while ((num = arr.pop()) !== undefined) { 23 | // 에러: ~~'readonly number[]' 형식에 'pop' 속성이 없습니다. 24 | sum += num; 25 | } 26 | return sum; 27 | } 28 | ``` 29 | 30 | 31 | 32 | ## `readonly number[]`가 `number[]`와 구분되는 몇 가지 특징 33 | 34 | - 배열의 요소를 읽을 수 있지만, 쓸 수는 없다. 35 | - `length`를 읽을 수 있지만, 바꿀 수는 없다. 36 | - 배열을 변경하는 메서드를 호출할 수 없다. (예: pop) 37 | 38 | `number[]`는 `readonly number[]` 보다 기능이 많기 때문에, `readonly number[]`의 서브타입이 됩니다. (아이템 7 참고) 39 | 40 | 따라서 변경 가능한 배열을 `readonly` 배열에 할당할 수 있습니다. 하지만 그 반대는 불가능합니다. 41 | 42 | ```typescript 43 | const a: number[] = [1, 2, 3]; 44 | const b: readonly number[] = a; // 에러 발생 x 45 | const c: number[] = b; // 에러 발생 O 46 | // ~ 'readonly number[]' 타입은 'readonly'이므로 47 | // 변경 가능한 'number[]' 타입에 할당될 수 없습니다. 48 | ``` 49 | 50 | 51 | 52 | ## 매개변수를 `readonly`로 선언하면 생기는 일 53 | 54 | - 타입스크립트는 매개변수가 함수 내에서 변경이 일어나는지 체크한다. 55 | - 호출하는 쪽에서는 함수가 매개변수를 변경하지 않는다는 보장을 받는다. 56 | - 호출하는 쪽에서 함수에 `readonly 배열`을 매개변수로 넣을 수도 있다. 57 | 58 | 자바스크립트, 타입스크립트에서는 함수가 매개변수를 변경하지 않는다고 가정하나, 이런 암묵적인 방법이 타입 체크에 문제를 일으킬 수 있습니다. (아이템 30, 31에서 계속) 따라서 명시적인 방법을 사용하는 것이 컴파일러와 사람 모두에게 좋습니다. 59 | 60 | 앞서 작성했던 arraySum 함수는 배열을 변경하지 않는 방법으로 수정하여 에러를 해결할 수 있습니다. 61 | 62 | ```typescript 63 | function arraySum(arr: readonly number[]) { 64 | let sum = 0; 65 | for (const num of arr) { 66 | sum += num; 67 | } 68 | return sum; 69 | } 70 | ``` 71 | 72 | 73 | 74 | ### `readonly`로 선언했을 때의 장점 75 | 76 | - 더 넓은 타입으로 호출 할 수 있다. (아이템 29) 77 | - 의도치 않은 변경은 방지된다. 78 | - 지역 변수와 관련된 모든 종류의 변경 오류를 방지할 수 있다. 79 | 80 | 81 | 82 | ### `readonly`로 선언했을 때의 단점 83 | 84 | - 매개변수가 `readonly`로 선언되지 않은 함수를 호출해야 할 경우도 있다. 85 | - 해결) 그 함수가 매개변수를 변경하지 않는 함수라면 `readonly`로 선언하면 된다. 86 | - 어떤 함수를 readonly로 만들면, 그 함수를 호출하는 다른 함수도 모두 `readonly`로 만들어야 한다. 87 | - 해결) 이를 통해 인터페이스를 명확히 하고 타입 안정성을 높일 수 있기 때문에 단점이라고 볼 수만은 없다. 만약 다른 라이브러리에 있는 함수를 호출하는 경우라면, 타입 선언을 바꿀 수 없기에 `타입 단언문(as number[])`을 사용해야 한다. 88 | 89 | 90 | 91 | ### `readonly`로 선언시 발생하는 문제 해결 예시 92 | 93 | ```typescript 94 | function parseTaggedText(lines: string[]): string[][] { 95 | const currPara: readonly string[] = []; 96 | const paragraph: string[][] = []; 97 | ... 98 | paragraph.push(currPara) // 에러(1): ~~ 'readonly string[]' 형식의 인수는 'string[]' 형식의 매개변수에 할당될 수 없습니다. 99 | ... 100 | currPara.length = 0; // 에러(2): ~~ 읽기 전용 속성이기 때문에 'length'에 할당할 수 없습니다. 101 | ... 102 | curPara.push(line); // 에러(3): ~~ 'readonly string[]' 형식에 'push' 속성이 없습니다. 103 | } 104 | ``` 105 | 106 | 위 오류를 바로 잡는 방법은 세 가지입니다. 107 | 108 | 1. `currPara`의 복사본을 만드는 방법 109 | 2. `paragraphs(그리고 함수의 반환 타입)`를 `readonly string[]`의 배열로 변경하는 방법 110 | 3. 배열의 `readonly` 속성을 제거하기 위해 단언문을 쓰는 방법 111 | 112 | 113 | 114 | ## `readonly`는 얕게(shallow) 동작한다. 115 | 116 | ```typescript 117 | interface Outer { 118 | inner: { 119 | x: number; 120 | } 121 | } 122 | const o: Readonly = {inner: {x: 0}}; 123 | o.inner = { x: 1}; // 에러: ~~ 읽기 전용 속성이기 때문에 'inner'에 할당할 수 없습니다. 124 | o.inner.x = 1; // 정상 125 | ``` 126 | 127 | `readonly`는 얕게(shallow) 동작한다는 것에 유의하며 사용해야 합니다. 현재 시점에는 깊은(deep) `readonly` 타입이 기본으로 지원되지 않지만, 제너릭을 만들면 깊은 `readonly`타입을 사용할 수 있습니다. 그러나 이는 까다롭기에 라이브러리를 사용하는 게 낫습니다. (Ts-essentials 내 DeepReadonly 제너릭) 128 | 129 | 130 | 131 | ## const, readonly - 공통점 132 | 133 | const와 readonly는 초기 때 할당된 값을 변경할 수 없습니다. 134 | 135 | 136 | 137 | ## const, readonly - 차이점 138 | 139 | ### Const 140 | 141 | - 변수 참조를 위한 것입니다. 142 | - 변수에 다른 값을 할당할 수 없습니다. 143 | 144 | ```typescript 145 | const eleven = 11 146 | eleven = '11st' // 불가 147 | ``` 148 | 149 | ### readonly 150 | 151 | - 속성을 위한 것입니다. 152 | 153 | ```typescript 154 | type eleven = { 155 | readonly name: string 156 | }; 157 | 158 | const x: eleven = {name: '11st'}; 159 | x.name = '12st'; // 불가 160 | ``` 161 | 162 | 163 | 164 | ## 인덱스 시그니처에 `readonly` 활용 165 | 166 | ```typescript 167 | let obj: {readonly [k: string]: number} = {}; 168 | ``` 169 | 170 | 인덱스 시그니처에도 `readonly`를 쓸 수 있습니다. 인덱스 시그니처에 `readonly`를 사용하면 객체의 속성이 변경되는 것을 방지할 수 있습니다. 171 | 172 | 173 | 174 | ## 결론 175 | 176 | - 변경으로 발생한 오류를 방지하고, 변경이 발생하는 코드도 쉽게 찾을 수 있습니다. 177 | - `readonly`는 얕게 동작합니다. 178 | - `const`와 `readonly`의 차이를 이해해야합니다. -------------------------------------------------------------------------------- /ch02_타입스크립트의_타입_시스템/item18_chanwoo.md: -------------------------------------------------------------------------------- 1 | 2 | # 매핑된 타입을 사용하여 값을 동기화하기 3 | 4 | ---- 5 |
6 | 7 |   지금까지 구조적 타이핑 관점에서 TS는 두 객체가 가진 속성의 동일성 여부와 잉여 속성의 여부를 판단하여 객체 형식의 동일 여부를 판단한다고 배웠다. 이번에는 형식이 아니라 객체의 동일성을 판단하는 방식을 배우고자 한다. 8 | 9 | > 객체의 동일성을 판단하는 방식을 TS를 이용하여 정의함으로서 객체를 동기화하는 작업을 최적화할 수 있고, 비동기화되는 오류를 잡을 수 있다. 10 | 11 |   특히 해당 객체가 렌더링에 영향을 주는 경우, 렌더링은 성능에 직결되기 때문에 동일성 여부를 엄격하게 판단하여 필요한 시점에만 동기화되도록 최적화할 필요가 있다. 예제로 산점도를 그리기 위한 UI 컴포넌트를 작성하는 것을 가정하자. 12 | 13 | ```typescript 14 | interface ScatterProps{ 15 | // Value 16 | xs: number[]; 17 | ys: number[]; 18 | 19 | // Display 20 | xRange: [number, number]; 21 | yRange: [number, number]; 22 | color: string; 23 | 24 | // Event 25 | onClick: (x: number, y: number, index: number) => void; 26 | } 27 | ``` 28 | - 위에서부터 해당 객체가 가지는 값, 화면 상에 보여지는 범위, 이벤트핸들러의 형식을 정의해두었다. 29 | - 이 중에서 값과 화면상에 보여지는 범위는 렌더링에 직결되는 속성이다. 그러므로 속성의 값이 변경되면 변경되기 이전의 객체와 동일하지 않다고 판단, 리렌더링을 할 수 있도록 한다. 30 | - 반면, 이벤트 핸들러는 변경되어도 렌더링과는 무관하기 때문에 변경되더라도 이전 객체와 동일하다고 판단, 불필요한 리렌더링을 줄이고자 한다. 31 | 32 |
33 |
34 | 35 | ## 1. 실패에 닫힌 접근법 36 | 37 | ```typescript 38 | function shouldUpdate( 39 | oldProps: ScatterProps, 40 | newProps: ScatterProps 41 | ) { 42 | let k: keyof ScatterProps; 43 | for (k in oldProps) { 44 | if(oldProps[k] !== newProps[k]){ 45 | if(k !== 'onClick') return true; 46 | } 47 | } 48 | return false; 49 | } 50 | ``` 51 | 52 | > `onClick` 속성 이외의 모든 속성 값이 변경되게 되면 두 객체가 동일하지 않다고 판단하고 리렌더링을 수행하게 하는 방식이다. 53 | 54 | - 기존의 객체가 가지는 모든 속성을 for loop를 통해 돌면서, 속성 값의 일치여부를 확인한다. 이 때, 이벤트 핸들러의 속성명일 때만 예외적으로 일치여부 판단을 넘긴다. 55 | - `onClick`이 아닌 새로운 속성이 추가되면, 해당 속성 값이 변경되어도 리렌더링이 된다. 즉, `onClick`에 대해서만 적용되는 방식이기 때문에 **보수적인** 접근법이라고 볼 수 있다. 56 | 57 |
58 |
59 | 60 | ## 2. 실패에 열린 접근법 61 | 62 | ```typescript 63 | function shouldUpdate( 64 | oldProps: ScatterProps, 65 | newProps: ScatterProps 66 | ) { 67 | oldProps.xs !== newProps.xs || 68 | oldProps.ys !== newProps.ys || 69 | oldProps.xRange !== newProps.xRange || 70 | oldProps.yRange !== newProps.yRange || 71 | oldProps.color !== newProps.color 72 | } 73 | ``` 74 | 75 | > 렌더링에 직결된 속성 값이 변경되게되면 두 객체가 동일하지 않다고 판단하고 리렌더링을 수행하게 하는 방식이다. 76 | 77 | - 기존의 객체가 가진 속성 중, 렌더링에 영향을 주는 속성만 비교, 속성 값의 일치여부를 확인한다. 이 때, 그 이외의 모든 속성에 대해서는 일치여부를 판단하지 않는다. 78 | - `onClick`이 아닌 새로운 속성이 추가되면, 해당 속성 값이 변경되어도 리렌더링이 되지 않는다. 그러므로 **'열려있는'** 접근법이라고 볼 수 있다. 79 | - 하지만 반대로, 렌더링에 영향을 주는 새로운 속성이 추가되었을 때 리렌더링이 되지 않을 수 있다. 80 | 81 |
82 |
83 | 84 | ## 3. TS의 타입체커를 사용한 방법 85 | 86 | > **핵심은 매핑된 타입과 객체를 사용하는 것입니다.** 87 | 88 | ```typescript 89 | const REQUIRES_UPDATE: {[k in keyof ScatterProps] : boolean} = { 90 | xs: true, 91 | ys: true, 92 | xRange: true, 93 | yRange: true, 94 | color: true, 95 | onClick: false 96 | } 97 | 98 | function shouldUpdate( 99 | oldProps: ScatterProps, 100 | newProps: ScatterProps 101 | ) { 102 | let k: keyof ScatterProps; 103 | for (k in oldProps) { 104 | if(oldProps[k] !== newProps[k] && REQUIRES_UPDATE[k]){ 105 | return true; 106 | } 107 | } 108 | return false; 109 | } 110 | ``` 111 | 112 | - 매핑된 타입의 객체 `REQUIRES_UPDATE`은 `[k in keyof ScatterProps]`에 의해 반드시 `ScatterProps`와 동일한 속성을 가지도록 구현되어있다.~~(타입이 맵핑되었다.)~~ 그러므로 속성이 추가되는 경우, 반드시 객체에 명시해야한다. **새로운 속성이 추가되었으나 매핑된 타입에 존재하지 않는 경우 타입체커가 가이드한다.** 113 | - 매핑된 타입의 객체 `REQUIRES_UPDATE`은 실제 값으로 비교해야하기 때문에 타입이 아닌 boolean 값을 가진 객체로 구현되어 있다. 114 | - `REQUIRES_UPDATE`을 수정함을 통해 속성에 대한 렌더링의 여부를 결정할 수 있다. 렌더링에 직결되는 속성일 경우에는 `true`를, 직결되지 않는 속성은 `false`의 값을 가지도록 하여 새로운 속성이 추가되었을 때 발생하는 모든 문제를 해결할 수 있다. 115 | 116 |
117 |
118 | 119 | --- 120 | 121 | ### 정리하자면, 122 | 123 | #### 인터페이스에 속성을 추가할 때, 속성에 대한 검사여부를 명시하도록 강제하기 위해 매핑된 타입 객체를 고려해야한다. 124 | -------------------------------------------------------------------------------- /ch03_타입_추론/item19_chanu.md: -------------------------------------------------------------------------------- 1 | # 추론 가능한 타입을 사용해 장황한 코드 방지하기 2 | 3 | > TS의 많은 타입 구문은 사실 불필요합니다. 모든 변수에 타입을 선언하는 것은 비생산적이며 형편없는 스타일로 여겨집니다. 4 | 5 | 타입선언이 불필요한 상황에 대해 먼저 알아보자. 6 | 7 |
8 | 9 | #### 1. 객체에 대한 타입 구문은 필요하지 않다. 10 | > 타입시스템은 선언된 객체의 값을 통해서 타입을 추론한다. 실제로 아래 두개의 객체의 타입은 동일하다. 11 | ```typescript 12 | const doNotPerson : { 13 | name: string, 14 | born: { where: string, when: string }, 15 | died: { where: string, when: string } 16 | } = { 17 | name: 'Sojour Thruth', 18 | born: { 19 | where: 'south korea', 20 | when: 'c.1797', 21 | }, 22 | died: { 23 | where: 'Battle Vreek, MI', 24 | when: 'Nov. 26, 1883' 25 | } 26 | } 27 | 28 | 29 | const person = { 30 | name: 'Sojour Thruth', 31 | born: { 32 | where: 'south korea', 33 | when: 'c.1797', 34 | }, 35 | died: { 36 | where: 'Battle Vreek, MI', 37 | when: 'Nov. 26, 1883' 38 | } 39 | } 40 | ``` 41 | 42 | 43 |
44 | 45 | 46 | #### 2. 타입 추론이 가능한 비구조화 할당에는 타입 구문이 필요하지 않다. 47 | > 비구조화 할당시에 속성 타입이 추론이 가능하기 때문에 타입구문이 불필요하다. 48 | ```typescript 49 | interface Product { 50 | id: number; 51 | name: string; 52 | price: number; 53 | } 54 | 55 | function logProduct(product : Product) { 56 | const localId = product.id; // number 57 | const { id }: { id: number } = product; // number 58 | } 59 | ``` 60 | - 사실 지역변수로 선언하여도 타입추론이 가능하다. 61 | 62 | > 함수 내에서 생성된 지역 변수에는 타입 구문을 넣지 않습니다. **타입 구문을 생략하여 방해되는 것들을 최소화하고 코드를 읽는 사람이 구현 로직에 집중할 수 있게 하는 것이 좋습니다.** 63 | 64 |
65 | 66 | #### 3. 기본값이 존재하는 함수의 파라미터에 대해서는 타입 구문이 필요하지 않다. 67 | 68 | ```typescript 69 | function parseNumber(str: string, base=10) { 70 | base.toExponential(10); 71 | } 72 | ``` 73 | - base가 number 타입이기 때문에 `toExponential()` 메서드를 사용할 수 있다. 74 | 75 |
76 | 77 | > 타입정보가 있는 라이브러리의 콜백 함수의 파라미터 타입은 자동으로 추론된다. 78 | ```typescript 79 | app.get('health', (request: express.Request, response: express.Response)=> { 80 | response.send('ok') 81 | }) 82 | 83 | app.get('health', (request, response)=> { 84 | response.send('ok') 85 | }) 86 | ``` 87 | 88 |
89 | 90 | ### 타입 구문이 필요한 경우 91 | 92 | 93 | #### 4. 객체의 속성체크가 필요한 경우 타입 구문으로 객체의 타입을 선언한다. 94 | ```typescript 95 | interface Product { 96 | id: number; 97 | name: string; 98 | price: number; 99 | } 100 | 101 | function logProduct(product : Product) { 102 | const localId = product.id; // number 103 | const { id }: { id: number } = product; // number 104 | } 105 | 106 | const elmo = { 107 | id: '048188 627', 108 | name: 'Tickle Me Elmo', 109 | price: 28.99, 110 | } 111 | 112 | logProduct(elmo); // Types of property 'id' are incompatible. Type 'string' is not assignable to type 'number'. 113 | ``` 114 | - `elmo` 객체를 `logProduct`의 인자로 사용하고자 하나, `Product`의 `id`의 타입과 다르기 때문에 오류가 발생한다. 115 | - 객체에 대한 타입 선언을 하지 않았기 때문에 객체를 사용하는 시점에 오류가 발생한다. 116 | 117 | ```typescript 118 | const elmo: Product = { 119 | id: '048188 627', // Type 'string' is not assignable to type 'number'. 120 | name: 'Tickle Me Elmo', 121 | price: 28.99, 122 | } 123 | ``` 124 | - 변수를 사용하는 시점이 아닌 할당하는 시점에 오류를 발생한다. 125 | 126 | 127 |
128 | 129 | #### 5. 함수 반환값은 타입 구문을 통해 타입을 선언한다. 130 | ```typescript 131 | const cache: {[ticker:string]: number} = {}; 132 | function getQuote(ticker: string) { 133 | if(ticker in cache) { 134 | return cache[ticker]; //number 135 | } 136 | return new Promise((resolve, reject)=> { 137 | resolve(1); 138 | }).then(response => { 139 | cache[ticker] = response; 140 | return response; // Promise 141 | }) 142 | } 143 | 144 | getQuote('wow').then() //Property 'then' does not exist on type 'number | Promise'. Property 'then' does not exist on type 'number'. 145 | ``` 146 | - 반환값에 대한 타입 선언을 하지 않으면, 분기에 따라 반환값의 형식이 달라질 경우 사용 시점에 오류가 발생한다. 147 | - 반환값에 대한 타입 선언을 통해서 분기에 따라 반환값이 달라질 수 있다는 것을 선언 시점에 확인할 수 있다. 148 | 149 |
150 | 151 | ```typescript 152 | interface Vector2D { x: number; y: number;} 153 | function add(a: Vector2D, b: Vector2D) { 154 | return { x: a.x + b.x, y: a.y + b.y }; 155 | } 156 | // function add(a: Vector2D, b: Vector2D): {x: number, y: number} 157 | 158 | function add(a: Vector2D, b: Vector2D): Vector2D { 159 | return { x: a.x + b.x, y: a.y + b.y }; 160 | } 161 | // function add(a: Vector2D, b: Vector2D): Vector2D 162 | ``` 163 | - 함수 반환 값에 대한 타입 선언을 명명된 타입을 사용하여 해당 함수를 조금 더 직관적으로 이해할 수 있다. 164 | 165 | 166 | 167 |
168 | 169 | ### 정리하자면, 170 | 171 | #### 1. 맹목적인 타입 추가보다는 타입시스템이 충분히 추론 가능한 타입에 대해서는 타입 구문을 피하자. 172 | 173 | #### 2. 함수 내의 지역 변수에는 타입구문을 줄여 로직에 대한 가독성을 높이자. 174 | 175 | #### 3. 속성체크를 위한 객체 선언과 함수 반환값에 대해서는 타입 선언을 하자. 176 | -------------------------------------------------------------------------------- /ch03_타입_추론/item20_dami.md: -------------------------------------------------------------------------------- 1 | # 아이템 20. 다른 타입에는 다른 변수 사용하기 2 | 3 | 4 | 5 | ## 변수를 여러 타입으로 재사용할 경우 생기는 문제 6 | 7 | 자바스크립트에서는 한 변수를 다른 목적을 가지는 다른 타입으로 재사용해도 되지만, 타입스크립트에서는 오류가 발생합니다. 8 | 9 | ### 자바스크립트 10 | 11 | ```typescript 12 | let id = '12-34-56'; 13 | fetchProduct(id); //string으로 사용 14 | id = 123456; 15 | fetchProductBySeriaNumber(id); //number로 사용 16 | ``` 17 | 18 | 19 | 20 | ### 타입스크립트 21 | 22 | ```typescript 23 | let id = '12-34-56'; 24 | fetchProduct(id); 25 | id = 123456; 26 | // 에러: ~~ '123456' 형식은 'string' 형식에 할당할 수 없습니다. 27 | fetchProductBySeriaNumber(id); 28 | // 에러: ~~ 'string' 형식의 인수는 'number' 형식의 매개변수에 할당될 수 없습니다. 29 | ``` 30 | 31 | 32 | 33 | 편집기에서 타입스크립트는 '12-34-56' 을 보고 id 타입을 `string`으로 추론했고 `string` 타입에는 `number`타입을 할당할 수 없기 때문에 오류가 발생합니다. 34 | 35 | ### 변수의 값은 바뀔 수 있지만 그 타입은 보통 바뀌지 않는다 36 | 37 | - 타입을 바꿀 수 있는 한 가지 방법은 **범위를 좁히는 것** (아이템 22) 입니다. 38 | - 새로운 변수값을 포함하도록 확장하는 것이 아니라 타입을 **더 작게 제한하는 것**입니다. -> **타입 지정 방법**(아이템 41)은 이 관점에 반하는데, 어디까지나 예외이지 규칙은 아닙니다. 39 | 40 | 41 | 42 | ## 변수를 여러 타입으로 재사용할 경우 생기는 문제 해결 43 | 44 | 앞서 오류가 발생한 예제에서 id의 타입을 바꾸지 않고 문제를 해결하기 위해서는 `유니온(union)` 타입을 사용해 `string` 과 `number`를 모두 포함할 수 있도록 타입을 확장하면 됩니다. 45 | 46 | ### 유니온 타입 47 | 48 | ```typescript 49 | let id: string|number = '12-34-56'; // string 50 | fetchProduct(id); 51 | id = 123456; // 정상 52 | fetchProductBySeriaNumber(id); // 정상 - number 53 | ``` 54 | 55 | 유니온 타입으로 에러를 제거할 수 있지만, id를 사용할 때마다 값이 어떤 타입인지 확인해야 하기 때문에 오히려 더 많은 문제를 야기할 수 있습니다. 따라서 두 값이 관련이 없다면 별도의 변수에 할당하는 것이 낫습니다. 56 | 57 | 58 | 59 | ### 변수에 할당 60 | 61 | ```typescript 62 | const id: string|number = '12-34-56'; 63 | fetchProduct(id); 64 | 65 | const serial = 123456; // 정상 66 | fetchProductBySeriaNumber(id); // 정상 67 | ``` 68 | 69 | 70 | 71 | ### 변수 사용의 장점 72 | 73 | - 서로 관련이 없는 두 개의 값을 분리합니다(`id`와 `serial`). 74 | - 변수명을 더 구체적으로 지을 수 있습니다. 75 | - 타입 추론을 향상시키며, 타입 구문이 불필요해집니다. 76 | - 타입이 좀 더 간결해집니다(`string | number` 대신 `string`과 `number`를 사용). 77 | - `let` 대신 `const`로 변수를 선언할 수 있습니다. 변수를 `const`로 선선하면 코드가 간결해지고, 타입 체커가 타입을 추론하기에도 좋습니다. 78 | 79 | 타입이 바뀌는 변수는 되도록 피해야하며, 목적이 다른 곳에는 별도의 변수명을 사용해야 합니다. 그런데 재사용 되는 변수와, 다음 예제 내 `가려지는(shadowed)` 변수를 혼동해서는 안 됩니다. 80 | 81 | 82 | 83 | ### 가려지는(shadowed) 변수 84 | 85 | ```typescript 86 | const id = '12-34-56'; 87 | fetchProduct(id); 88 | { 89 | const id = 123456; // 정상 90 | fetchProductBySerialNumber(id); //정상 91 | } 92 | ``` 93 | 94 | - 두 `id`는 이름이 같지만 실제론 관계가 없기에 각 `id`에 다른 타입을 사용해도 잘 동작하지만, 이 방식은 다른 개발자에게 혼란을 줄 수 있습니다. 95 | 96 | - 따라서 목적이 다른 곳에는 별도의 변수명을 사용하는 것이 좋습니다. 97 | 98 | - 많은 개발팀에서는 lint 규칙을 통해 가려지는(shadowed) 변수를 사용하지 못하도록 하고 있습니다. 99 | 100 | - 참고 101 | 102 | [ESLint no-shadow](https://eslint.org/docs/latest/rules/no-shadow) 103 | 104 | Disallow variable declarations from shadowing variables declared in the outer scope 105 | 106 | 107 | 108 | 109 | ## 결론 110 | 111 | - 변수의 값은 바뀔 수 있으나, 타입은 일반적으로 바뀌지 않습니다. 112 | - 타입이 다른 값을 다룰 때는 변수를 재사용하지 말고, 그 값에 맞는 변수를 생성합시다. 113 | -------------------------------------------------------------------------------- /ch03_타입_추론/item21_호찬.md: -------------------------------------------------------------------------------- 1 | # 아이템 21. 타입 넓히기 2 | 3 | - 런타임에 모든 변수는 **유일한 값을** 가진다. 4 | - 컴파일타임에 타입스크립트에서 변수는 **가능한 값들의 집합인 타입을** 가진다. 5 | 6 | 상수를 사용해서 변수를 초기화할 때 타입을 명시하지 않으면 타입 체커는 타입을 결정해야한다. 7 | 8 | ```typescript 9 | let age = 42; 10 | ``` 11 | 12 | 즉, 지정된 단일한 값을 가지고 할당 가능한 값들의 집합을 유추해야한다. 타입스크립트에서 이러한 과정을 **넓히기(widening)** 이라 한다. 13 | 14 | ```typescript 15 | interface Vector3 { x: number; y: number; z: number; } 16 | function getComponent(vector: Vector3, axis: 'x' | 'y' | 'z') { 17 | return vector[axis]; 18 | } 19 | 20 | let x = 'x'; // here 21 | let vec = {x: 10, y: 20, z: 30}; 22 | getComponent(vec, x); 23 | // Argument of type 'string' is not assignable to parameter of type '"x" | "y" | "z"'.ts(2345) 24 | ``` 25 | 26 | 실행이 되지만 컴파일 오류가 발생한다. x의 타입은 할당 시점에서 `넓히기`가 동작해서 `string`으로 추론되었기 때문이다. 27 | 28 | ```typescript 29 | const mixed = ['x', 1] 30 | ``` 31 | 32 | `mixed`의 타입이 될 수 있는 후보는 상당히 많은걸 알 수 있다. 33 | 34 | ```typescript 35 | ('x' | 1)[] 36 | ['x', 1] 37 | [string, number] 38 | readonly [string, number] 39 | (string|number)[] // 이걸로 추론 40 | readonly (string|number)[] 41 | [any, any] 42 | any[] 43 | ``` 44 | 45 | **타입스크립트는 명확성과 유연성 사이에서 균형을 유지하며 추론하려고 한다.** 46 | 47 | ```typescript 48 | let x = 'x'; 49 | x = /x|y\z/; 50 | x = ['x', 'y', 'z']; 51 | ``` 52 | 53 | 일반적인 규칙은 변수가 선언된 후로는 타입이 바뀌지 않아야 하므로, `string|RegExp` 나 `string|string[]`이나 any보다는 `string`으로 `x`를 할당 시점에서 추론을 한다. 54 | 55 | ### 넓히기 과정 제어하기 56 | 57 | #### `const` 58 | 59 | `let` 대신 `const`로 변수를 선언하면 더 좁은 타입으로 선언할 수 있다. 60 | 61 | ```diff 62 | interface Vector3 { x: number; y: number; z: number; } 63 | function getComponent(vector: Vector3, axis: 'x' | 'y' | 'z') { 64 | return vector[axis]; 65 | } 66 | 67 | - let x = 'x'; // 타입: string 68 | + const x = 'x'; // 타입: 'x' 69 | let vec = {x: 10, y: 20, z: 30}; 70 | getComponent(vec, x); 71 | ``` 72 | 73 | 하지만, **객체와 배열의 경우 여전히 문제가 발생한다.** 74 | 75 | #### 타입스크립트의 기본 동작 재정의 하기 76 | 77 | **타입스크립트의 기본 동작을 재정의를 통해 타입 추론의 강도를 직접 제어할 수 있다.** 방법은 아래 3가지가 있다. 78 | 79 | 1. 명시적 타입 구문 제공 80 | 81 | ```typescript 82 | const v: {x: 1|3|5 } = { 83 | x: 1 84 | }; // 타입: { x: 1|3|5; } 85 | ``` 86 | 87 | 2. 타입 체커에 추가적인 문맥 제공 88 | 89 | 아래 코드는 컴파일 시간에 오류가 발생하지 않는다. 90 | 91 | ```typescript 92 | function setLanguage(language){ /* ... */ } 93 | 94 | setLanguage('Javascript'); 95 | 96 | let language = 'Javascript'; 97 | setLanguage(language); 98 | ``` 99 | 100 | setLanguage 매개변수에 타입을 지정해주면 타입을 좁힐 수 있따. 101 | 102 | ```typescript 103 | type Language = 'Javascript' | 'Typescript' | 'Python'; 104 | function setLanguage(language: Language){ /* ... */ } 105 | 106 | setLanguage('Javascript'); 107 | 108 | let language = 'Javascript'; 109 | setLanguage(language); 110 | // Argument of type 'string' is not assignable to parameter of type 'Language'.ts(2345) 111 | ``` 112 | 113 | 3. `const` 단언문 사용 114 | 115 | ```typescript 116 | const v1 = { 117 | x: 1, 118 | y: 2, 119 | }; // 타입: { x: number; y: number; } 120 | 121 | const v2 = { 122 | x: 1 as const, 123 | y: 2, 124 | }; // 타입: { x: 1; y: number; } 125 | 126 | const v3 = { 127 | x: 1, 128 | y: 2, 129 | } as const; // 타입: { readonly x: 1; readonly y: 2; } 130 | ``` 131 | 132 | 배열도 사용 가능하다. 133 | 134 | ```typescript 135 | const a1 = [1, 2, 3]; // 타입: nunmber[] 136 | const a2 = [1, 2, 3] as const; // 타입: readonly [1, 2, 3] 137 | ``` 138 | 139 | ### 요약 140 | 141 | - 타입스크립트가 `넓히기`를 통해 상수의 타입을 명확성과 유연성 사이에서 균형을 유지하며 추론한다. 142 | - 동작에 영향을 주는 방법인 `const`, 타입 구문, 문맥, `as const`에 익숙해지자. 143 | -------------------------------------------------------------------------------- /ch03_타입_추론/item22_jungho.md: -------------------------------------------------------------------------------- 1 | # 아이템22 타입 좁히기 2 | 3 | 타입 좁히기는 타입스크립트가 넓은 타입으로부터 좁은 타입으로 진행하는 과정이다. 4 | 5 | > 이를 통해 타입체커와 IDE의 언어 서비스 지원을 좀 더 명확히 받아 안정적인 코드를 작성할 수 있는 이점이 있다. 6 | 7 |
8 | 9 | 타입 좁히기의 가장 일반적인 예시로 DOM요소를 가져와 `null`인지 체크하는 과정이 있으며, 10 | 11 | 타입을 좁히는 방법에는 아래와 같은 방법들이 있다. 12 | 13 | - 조건문, 분기문 14 | - instanceof 15 | - Array.isArray와 같은 내장 함수 16 | - 명시적 ‘태그' (태그된 유니온, 구별된 유니온) 17 | - 사용자 정의 타입 가드 18 | 19 |
20 | 21 | 조건문 22 | 23 | ```tsx 24 | const el = document.getElementById('foo'); // 타입이 HTMLElement | null 25 | 26 | // 조건문으로 처리하기 27 | if (el) { 28 | // 타입이 HTMLElement 29 | } 30 | 31 | // 분기문에서 예외처리 32 | if (!el) throw new Error(); 33 | // 타입이 HTMLElement 34 | ``` 35 | 36 | **instanceof** 37 | 38 | ```tsx 39 | function contains(text: string, search: string | RegExp) { 40 | if (search instanceof RegExp) { 41 | search; // RegExp 타입 42 | } 43 | 44 | search; // string 타입 45 | } 46 | ``` 47 | 48 | **속성 체크** 49 | 50 | ```tsx 51 | interface A { 52 | a: number; 53 | } 54 | interface B { 55 | b: number; 56 | } 57 | 58 | function pickAB(ab: A | B) { 59 | if ('a' in ab) { 60 | ab; // 타입 A 61 | } else { 62 | ab; // 타입 B 63 | } 64 | ab; // 타입 A | B 65 | } 66 | ``` 67 | 68 | **내장 함수** 69 | 70 | ```tsx 71 | function contains(text: string, terms: string | string[]) { 72 | const termList = Array.isArray(terms) ? terms : [terms]; 73 | termList; // 타입이 string[] 74 | } 75 | // ArrayConstructor.isArray(arg: any): arg is any[] 76 | ``` 77 | 78 | **명시적 ‘태그' 붙이기** 79 | 80 | ```tsx 81 | interface UploadEvent { 82 | type: 'upload'; 83 | filename: string; 84 | contents: string; 85 | } 86 | interface DownloadEvent { 87 | type: 'download'; 88 | filname: string; 89 | } 90 | type AppEvent = UploadEvent | DownloadEvent; 91 | function handleEvent(e: AppEvent) { 92 | switch (e.type) { 93 | case 'download': 94 | e; // DownloadEvent 95 | break; 96 | case 'upload': 97 | e; // UploadEvent 98 | break; 99 | } 100 | } 101 | ``` 102 | 103 | 이 패턴은 태그된 유니온 또는 구별된 유니온이라고 불린다. 104 | 105 | **사용자 정의 타입 가드 사용하기** 106 | 107 | ```tsx 108 | function isDefined(x: T | undefined): x is T { 109 | return x !== undefined; 110 | } 111 | 112 | const members = ['Janet', 'Micael'].map(who => jackson5.find(n => n === who)).filter(isDefined); // 타입이 string[] 113 | ``` 114 | 115 | 반환타입에 `is` 구문을 넣어 타입을 좁힐 수 있음을 알려준다. 116 | 117 | > 위와 같이 반환타입 위치에 `x is T`가 `true`인 경우 118 | > 타입 체커에게 매개변수의 타입을 좁힐 수 있다고 알려준다. 119 | 120 |
121 | 122 | # 요약 123 | 124 | - 분기문 외에도 여러 종류의 제어 흐름을 살펴보며 타입스크립트가 타입을 좁히는 과정을 이해해야 한다. 125 | - 태그된/구별된 유니온과 사용자 정의 타입 가드를 사용하여 타입 좁히기 과정을 원할하게 만들 수 있다. 126 | -------------------------------------------------------------------------------- /ch03_타입_추론/item23_혁주.md: -------------------------------------------------------------------------------- 1 | # 한꺼번에 객체 생성하기 2 | 3 | 객체를 생성할 때는 속성을 하나씩 추가하기보다는 여러 속성을 포함해서 **한꺼번에 생성**해야 타입 추론에 유리합니다. 4 | 5 | 자바스크립트에서 2차원 점을 표현하는 객체를 생성한다고 가정하겠습니다. 6 | 7 | ```typescript 8 | const pt = {}; 9 | pt.x = 3; 10 | pt.y = 4; 11 | ``` 12 | 13 | 타입스크립트에서는 각 할당문에 오류가 발생합니다. 14 | 15 | ```typescript 16 | const pt = {}; 17 | pt.x = 3; 18 | // ~ '{}' 형식에 'x' 속성이 없습니다. 19 | pt.y = 4; 20 | // ~ '{}' 형식에 'y' 속성이 없습니다. 21 | ``` 22 | 23 | - 원인: 첫 번재 줄의 pt 타입은 {} 값을 기준으로 추론되기 때문입니다. 존재하지 않는 속성을 추가할 수는 없습니다. 24 | 25 | 만약 Point 인터페이스를 정의한다면 오류가 다음처럼 바뀝니다. 26 | 27 | ```typescript 28 | interface Point { 29 | x: number; 30 | y: number; 31 | } 32 | const pt: Point = {}; 33 | // ~ '{}' 형식에 'Point' 형식의 x, y 속성이 없습니다. 34 | pt.x = 3; 35 | pt.y = 4; 36 | ``` 37 | 38 | 이러한 문제들은 객체를 한번에 정의하면 해결할 수 있습니다. 39 | 40 | ```typescript 41 | const pt = { 42 | x: 3, 43 | y: 4, 44 | }; 45 | ``` 46 | 47 | 객체를 반드시 제각각 나눠서 만들어야 한다면, 타입 단언문을 사용해 타입 체커를 통과하게 할 수 있습니다. 48 | 49 | ```typescript 50 | const pt = {} as Point; 51 | pt.x = 3; 52 | pt.y = 4; 53 | ``` 54 | 55 | 물론 이 경우에도 선언할 떄 객체를 한꺼번에 만드는 게 더 낫습니다. 56 | 57 | ```typescript 58 | const pt: Point = { 59 | x: 3, 60 | y: 4, 61 | }; 62 | ``` 63 | 64 | 작은 객체들을 조합해서 큰 객체를 만들어야 하는 경우에도 여러 단계를 거치는 것은 좋지 않은 생각입니다. 65 | 66 | ```typescript 67 | const pt = { x: 3, y: 4 }; 68 | const id = { name: 'Pythagoras' }; 69 | const namedPoint = {}; 70 | Object.assign(namedPoint, pt, id); // { x: 3, y: 4, name: 'Pythagoras' } 71 | namedPoint.name; 72 | // ~ '{}'형식에 'name' 속성이 없습니다. -> ts가 추론하지 못함 73 | ``` 74 | 75 | 다음과 같이 `객체 전개 연산자` ... 를 사용하면 큰 객체를 한꺼번에 만들어 낼 수 있습니다. 76 | 77 | ```typescript 78 | const namedPoint = { ...pt, ...id }; 79 | namedPoint.name; //정상, 타입이 string 80 | ``` 81 | 82 | 객체 전개 연산자를 사용하면 타입 걱정 없이 필드 단위로 객체를 생성할 수도 있습니다. 이때 모든 업데이트마다 새 변수를 사용하여 각각 새로운 타입을 얻도록 하는 게 중요합니다. 83 | 84 | ```typescript 85 | const pt0 = {}; 86 | const pt1 = { ...pt0, x: 3 }; 87 | const pt: Point = { ...pt1, y: 4 }; 88 | ``` 89 | 90 | - 이 방법은 간단한 객체를 만들기 위해 우회하기는 했지만, 객체에 속성을 추가하고 타입스크립트가 새로운 타입을 추론할 수 있게 해 유용합니다. 91 | 92 | ### 조건부 속성 추가 93 | 94 | 타입에 안전한 방식으로 조건부 속성을 추가하려면, 속성을 추가하지 않는 `null` 또는 `{}`으로 객체 전개를 사용하면 됩니다. 95 | 96 | ```typescript 97 | declare let hasMiddle: boolean; 98 | const firstLast = { first: 'Harry', last: 'truman' }; 99 | const president = { ...firstLast, ...(hasMiddle ? { middle: 'S' } : {}) }; 100 | ``` 101 | 102 | president 심벌에 마우스를 올려보면, 다음과 같이 표시됩니다. 103 | middle이 선택적 속성을 가진 것으로 추론이 되는 것을 확인할 수 있습니다. 104 | 105 | 스크린샷 2022-10-11 오후 9 41 54 106 | 107 | 전개 연산자로 한꺼번에 여러 속성을 추가할 수도 있습니다. 108 | 109 | ```typescript 110 | declare let hasDates: boolean; 111 | const nameTitle = { name: 'Khufu', title: 'Pharaoh' }; 112 | const pharaoh = { 113 | ...nameTitle, 114 | ...(hasDates ? { start: -2589, end: -2566 } : {}), 115 | }; 116 | ``` 117 | 118 | 편집기에서 pharaoh 심벌에 마우스를 올리면 다음과 같이 표시됩니다. 119 | 120 | 스크린샷 2022-10-11 오후 9 42 11 121 | 122 | 그러나 책에서는 다음과 같이 타입이 유니온으로 추론된다고 적혀있는데요, 123 | 124 | ```typescript 125 | const pharaoh: 126 | | { 127 | start: number; 128 | end: number; 129 | name: string; 130 | title: string; 131 | } 132 | | { 133 | name: string; 134 | title: string; 135 | }; 136 | ``` 137 | 138 | 이렇게 차이가 나는 이유는 타입스크립트가 활발히 업데이트되고 있는 언어이기 때문입니다. 따라서 너무 책만보고 공부를 하다보면 예전에는 옳았던 것들이 지금은 틀린 것이 될 수 있겠습니다. 139 | 140 | ## 요약 141 | 142 | - 속성에 제각각 추가하지 말고 한꺼번에 객체로 만들어야 합니다. 안전한 타입으로 속성을 추가하려면 객체 전개({...a, ...b})를 사용하면 됩니다. 143 | - 객체에 조건부로 속성을 추가하는 방법을 익히도록 합니다. (null, {}) 144 | -------------------------------------------------------------------------------- /ch03_타입_추론/item24_dami.md: -------------------------------------------------------------------------------- 1 | # 아이템 24. 일관성 있는 별칭 사용하기 2 | 3 | 4 | 5 | ```typescript 6 | const borough = {name: 'Brooklyn', location: [40.688, -73.979]}; 7 | const loc = borough.location; 8 | ``` 9 | 10 | `loc`라는 `별칭(alias)`의 값을 변경하면`` borough`의 속성값도 변경됩니다. 11 | 12 | ```typescript 13 | loc[0] = 0; 14 | borough.location // [0, -73.979] 15 | ``` 16 | 17 | 18 | 19 | 별칭을 남발해서 사용하면 제어 흐름을 분석하기 어렵기에 별칭을 신중히 사용해서 코드 가독성을 높이고 오류를 쉽게 찾을 수 있게끔 해야합니다. 20 | 21 | ```typescript 22 | interface Coordiante { 23 | x: number; 24 | y: number; 25 | } 26 | 27 | interface BoundingBox { 28 | x: [number, number]; 29 | y: [number, number]; 30 | } 31 | 32 | interface Polygon { 33 | exterior: Coordinate[]; 34 | holes: Coordiname[][]; 35 | bbox?: BoundingBox; // 어떤 점이 다각형에 포함되는지 36 | } 37 | ``` 38 | 39 | 위 예제에서 `bbox`는 필수가 아닌 최적화 속성이며, 이 속성을 사용하면 어떤 점이 다각형에 포함되는지 체크할 수 있습니다. 40 | 41 | 42 | 43 | ```typescript 44 | function isPointInPolygon(polygon: Polygon, pt: Coordinate) { 45 | const box = polygon.box; 46 | if (polygon.bbox) { 47 | if (pt.x < box.x[0] || pt.x > box.x[1] || pt.y < box.y[0] || pt.y > box.y[1] ) { 48 | // 오류: ~~ 객체가 'undefined'일 수 있습니다. 49 | } 50 | } 51 | } 52 | ``` 53 | 54 | 위 예제는 동작하나, 에디터에서 오류로 표시됩니다. `polygon.bbox` 를 별도의 `box`라는 별칭을 만든 결과 제어 흐름 분석을 방해하기 되었기 때문입니다. 위 예제를 뜯어보면 아래와 같습니다. 55 | 56 | ```typescript 57 | function isPointInPolygon(polygon: Polygon, pt: Coordinate) { 58 | (1) polygon.bbox // BoundingBox | undefined 59 | const box = polygon.bbox; 60 | (2) box // BoundingBox | undefined 61 | if (polygon.bbox) { 62 | (3) polygon.bbox // undefined가 정제되어서 BoundingBox 타입만 가지게 됨 63 | (4) box // undefined가 정제되지 않아서 BoundingBox | undefined 타입을 그대로 가지게 됨 64 | } 65 | } 66 | } 67 | ``` 68 | 69 | ` if (polygon.bbox)` 속성 체크가`` polygon.bbox` 타입을 정제했으나` box` 타입은 정제하지 않아 오류가 발생합니다. `box`를 속성 체크 하게끔 하면 문제가 해결됩니다. 70 | 71 | 72 | 73 | ```typescript 74 | function isPointInPolygon(polygon: Polygon, pt: Coordinate) { 75 | const box = polygon.bbox; 76 | if (box) { 77 | const { x, y } = bbox; 78 | ... 79 | } 80 | } 81 | } 82 | ``` 83 | 84 | 하지만 위 예제에서는 실제 `polygon`의 속성인 `bbox`과 다른 이름의 변수명 box을 사용합니다. 가독성을 위해서는 이를 일치시켜 주는 것이 좋습니다. 85 | 86 | 87 | 88 | ## 객체 비구조화 할당 (`destructuring`)을 사용하자 89 | 90 | 비구조화 할당을 통해 일관된 이름을 사용할 수 있습니다. 91 | 92 | ```typescript 93 | function isPointInPolygon(polygon: Polygon, pt: Coordinate) { 94 | const { bbox } = polygon; 95 | if (bbox) { 96 | const { x, y } = bbox; 97 | ... 98 | } 99 | } 100 | } 101 | ``` 102 | 103 | ### 객체 비구조화 할당 시 주의할 점 104 | 105 | - 해당 속성이 `선택적(optional) 속성`일 경우 타입의 경계에 `null` 값을 추가하는 것이 좋습니다 (아이템 31). 106 | 107 | 108 | 109 | ## 지역변수를 사용하자 110 | 111 | 별칭은 타입 체커뿐만 아니라 런타임에도 혼동을 야기할 수 있습니다. 112 | 113 | ```typescript 114 | const { bbox } = polygon; 115 | if (!bbox) { 116 | calculatePolygonBbox(polygon); // polygon.bbox가 채워지는 함수 117 | // polygon.bbox와 bbox는 다른 값을 참조합니다. 118 | } 119 | ``` 120 | 121 | 이렇듯 지역 변수 사용시 타입스크립트의 제어 흐름 분석이 잘 작동합니다. 122 | 123 | 124 | 125 | 아래의 경우에는 함수가 타입 정제를 무효화할 수 있습니다. 126 | 127 | ```typescript 128 | function fn (p: Polygon) { /*...*/ }; //polygon.bbox를 제거할 가능성이 있다. 129 | 130 | polygon.bbox // BoundingBox | undefined 131 | if( polygon.bbox ) { 132 | // (1) polygon.bbox 타입은 BoundingBox 133 | fn(polygon); //polygon.bbox가 제거되었을 수도 있다. 134 | // (2) polygon.bbox 타입은 그대로 BoundingBox 135 | } 136 | ``` 137 | 138 | `fn(polygon)` 호출이 `polygon.bbox` 를 제거할 가능성이 있으니 `fn(polygon)` 호출 이후에는 `polygon.bbox` 타입을 `BoundingBox | undefined` 로 되돌리는 것이 안전합니다. 하지만 그럴 경우 함수 호출 내에서 속성 체크를 해야하기 때문에 좋지 않습니다. 139 | 140 | 이러한 이유로 타입스크립트는 함수가 타입 정제를 무효화 하지 않는다고 가정합니다. 실제로는 위 예제처럼 함수로 인해 타입 정제가 무효화 될 수 있습니다. 그렇기 때문에 타입 정제가 필요한 값을 지역변수로 뽑아내서 사용하면 객체 속성을 바로 사용하는 것보다 타입 정제를 믿을 수 있습니다. 141 | 142 | 143 | 144 | ## 결론 145 | 146 | - 별칭은 타입스크립트가 타입을 좁히는 것을 방해하기 때문에 별칭을 사용할 때는 일관되게 사용하자. 147 | - 객체 속성을 사용할 때는 비구조화 할당을 사용하자. 148 | - 객체 속성을 직접 사용하기 보다는 지역 변수에 할당하여 사용하자. -------------------------------------------------------------------------------- /ch03_타입_추론/item25_호찬.md: -------------------------------------------------------------------------------- 1 | # 아이템 25. 비동기 코드에는 콜백 대신 async 함수 사용하기 2 | 3 | ### Callback 4 | 5 | 과거 자바스크립트에서는 비동기 동작을 모델링 하기위해 콜백을 사용했다. 6 | 7 | ```js 8 | fetchURL(url1, function(response1) { 9 | fetchURL(url2, function(response2) { 10 | fetchURL(url3, function(response3) { 11 | //... 12 | console.log(1); 13 | }) 14 | console.log(2); 15 | }) 16 | console.log(3); 17 | }) 18 | console.log(4); 19 | ``` 20 | 21 | ### Promise (ES2015) 22 | 23 | ES2015에서는 콜백 지옥을 극복하기 위해 프로미스 개념을 도입했다. 실행 순서도 코드 순서와 같다. 24 | 25 | ```js 26 | const page1Promise = fetch(url1); 27 | page1Promise.then(response1 => { 28 | return fetch(url2); 29 | }).then(response2 => { 30 | return fetch(url3); 31 | }).then(response3 => { 32 | // ... 33 | }).catch(error => { 34 | // ... 35 | }) 36 | ``` 37 | 38 | ### async / await (ES2017) 39 | 40 | ES2017에서는 `async`와 `await` 키워드를 도입하여 더욱 간단하게 처리할 수 있게 되었다. 41 | 42 | ```js 43 | async function fetchPages() { 44 | const response1 = await fetch(url1); 45 | const response2 = await fetch(url2); 46 | const response3 = await fetch(url3); 47 | } 48 | ``` 49 | 50 | `await` 키워드는 `Promise`가 resolve(처리)될 때까지 fetchPages 함수의 실행을 멈춘다. reject(거절)되면 예외를 던지는데 이때 `try/catch` 구문을 사용해서 처리할 수 있다. 51 | 52 | ```js 53 | async function fetchPages() { 54 | try { 55 | const response1 = await fetch(url1); 56 | const response2 = await fetch(url2); 57 | const response3 = await fetch(url3); 58 | } catch (e) { 59 | // ... 60 | } 61 | } 62 | ``` 63 | 64 | ### 콜백보다 프로미스를 사용해야하는 이유 65 | 66 | - 콜백보다 프로미스가 코드를 작성하기 쉽다. 67 | - 콜백보다 프로미스가 타입을 추론하기 쉽다. 68 | 69 | #### 병렬 처리 - Promise.all 70 | 71 | ```js 72 | async function fetchPages() { 73 | const [response1, response2, response3] = await Promise.all([ 74 | fetch(url1), fetch(url2), fetch(url3) 75 | ]); 76 | // ... 77 | } 78 | ``` 79 | 80 | ### Callback 81 | 82 | ```js 83 | function fetchPagesCB() { 84 | let numDone = 0; 85 | const response: string[] = []; 86 | const done = () => { 87 | const [response1, response2, response3] = response; 88 | // ... 89 | }; 90 | const urls = [url1, url2, url3]; 91 | urls.forEach((url, i) => { 92 | fetchURL(url, r => { 93 | response[i] = url; 94 | numDone++; 95 | if (numDone === urls.length) done(); 96 | }) 97 | }) 98 | } 99 | ``` 100 | 101 | 이 코드에 오류 처리를 포함하거나 `Promise.all` 같은 일반적인 코드로 확장하는것은 쉽지 않다. 102 | 103 | ### 프로미스를 직접 사용하기보다는 `async` / `await` 를 활용해야하는 이유 104 | 105 | - 일반적으로 더 간결하고 직관적인 코드를 작성할 수 있다. 106 | - `async` 함수는 항상 프로미스를 반환하도록 강제된다. 107 | - `async` 함수에서 프로미스를 반환하면 또 다른 프로미스로 래핑되지 않는다. 108 | 109 | ```typescript 110 | const getNumber = async () => 42; // 타입이 () => Promise 111 | 112 | const getNumber = () => Promise.reslove(42); // 타입이 () => Promise 113 | ``` 114 | 115 | ### 요약 116 | 117 | - 콜백보다는 프로미스를 사용하는게 코드 작성과 타입 추론 면에서 유리하다. 118 | - 가능하면 프로미스보다는 `async` / `await`를 사용하자. 간결하고 직관적인 코드를 작성할 수 있고 모든 종류의 오류를 제거할 수있다. 119 | - 어떤 함수가 프로미스를 반환한다면 `async`로 선언하는것이 좋다. -------------------------------------------------------------------------------- /ch03_타입_추론/item26_chanu.md: -------------------------------------------------------------------------------- 1 | # 타입 추론에서 문맥이 어떻게 사용되고 있는가 이해하기 2 | 3 | > TS의 변수는 정적 체크 시점에 단일 값이 아닌, 가질 수 있는 값의 집합인 '타입'을 가진다. 4 | 5 | ```typescript 6 | let num = 10; 7 | ``` 8 | 9 | '타입 넓히기' 아이템에서 설명하였듯 `num`변수는 값인 10이 아닌 number 타입을 가진다. 즉, 선언된 값을 통해 추론하되, 단순히 그 값만을 고려하지 않고 문맥을 함께 고려한다는 것이다. 10 | 11 | ```typescript 12 | type Language = 'JavaScript' | 'TypeScript' | 'Python'; 13 | 14 | function setLanguage(language: Language) { 15 | console.log(language); 16 | } 17 | 18 | setLanguage('JavaScript') // inline 19 | 20 | let language = 'JavaScript'; 21 | setLanguage(language); // variable 22 | ``` 23 | - `setLanguage`는 값의 union 타입인 `Language` 타입의 인자를 받는다. 24 | - 인라인으로 호출한 `setLanguage`와 변수를 사용하여 호출한 `setLanguage`는 값만 따져보았을 때 동일한 값으로 인자로 호출하였다. 25 | - 하지만 아래 변수로 호출한 코드는 에러가 발생하는데 변수 `language`는 값이 아닌 집합으로 'string' 타입을 가지기 때문이다. 26 | 27 | > 값을 변수로 분리해내게 되면, 변수의 할당시점에 '타입'을 추론하기 때문이다. 28 | 29 | - 인라인으로 호출하는 경우, 할당을 수행하지 않기 때문에 문맥이 존재하지 않으므로 단순히 '값'으로 본다. 30 | - 하지만 변수를 선언하는 경우, 값 뿐 만이 아니라 선언하는 문맥까지 고려하게 되고, 할당 시점에 추론된 타입인 'number'을 가지게 된다. 31 | 32 | #### 변수로 재선언하였으므로, 우리는 '문맥'으로부터 '값'을 분리하는 작업을 한 것이다. 33 | 34 |
35 | 36 | ### '문맥'으로부터 '값'을 분리하는 작업으로 인해 발생한 불일치를 해결하는 방법으로 '타입 넓히기'에서 두가지 방법을 제시하고 있다. 37 | 38 | #### 1. 변수 선언시, 타입 구문 추가히기 39 | 40 | ```typescript 41 | let language: Language = 'JavaScript'; 42 | setLanguage(language); // variable 43 | ``` 44 | 45 | - 값 뿐 만 아니라, 인자의 `Language` 타입과 동일하게 맞춰줌으로써 에러를 방지할 수 있다. 46 | 47 | #### 2. `const` 사용하기 48 | 49 | ```typescript 50 | const language = 'JavaScript'; 51 | setLanguage(language); // variable 52 | ``` 53 | - 할당 시점에 ‘집합’이 아닌 ‘값’을 타입으로 가지도록 한다. 이는 문맥상 값의 변경이 존재하지 않다는 것이 명확하기 때문이다. 54 | 55 |
56 | 57 | ### '문맥'으로부터 '값'을 분리하는 작업을 수행하는 경우, 오류가 발생할 수 있다. 58 | 59 | > 이러한 문제가 발생하는 이유는 기본적으로 `const`는 '얕게' 동작하기 때문이다. 60 | 61 | 객체나 배열에 `const`를 선언하게 된다고 내부의 원소나 속성이 불변하지 않는다. 참조가 변하지 않는다는 보장이기 때문에 내부의 원소나 속성은 충분히 변경가능하며, 그러므로 'let'과 동일하게 타입을 추론한다. 62 | 63 | #### 1. 튜플 사용시 64 | 65 | ```typescript 66 | function panTo(where: [number, number]){ 67 | console.log(where); 68 | } 69 | panTo([10, 20]); // inline 70 | 71 | const loc = [10, 20]; // 문맥에서 값을 분리함 72 | panTo(loc); 73 | ``` 74 | 75 | `loc`는 문맥에서 값을 분리하였고, 할당시점에 `number[]`로 추론된다. 단일 값에 대해서는 `const`로 선언하였을 경우, 값의 불변을 보장하기 때문에 `[number, number]`가 아닌가 생각할 수 있으나 배열에 대해서는 다르게 동작하기 때문에 `number[]`으로 추론된다. 76 | 77 | 1. **타입구문 추가하기** 78 | ```typescript 79 | const loc: [number, number] = [10, 20]; 80 | ``` 81 |
82 | 83 | 2. **타입 단언문 `as const` 사용하기** 84 | 85 | `const`가 배열과 객체에는 의도대로 동작하지 않기 때문에 `as const`를 사용하는 것으로 배웠다. 86 | ```typescript 87 | const loc = [10, 20] as const; 88 | ``` 89 | 이렇게 하면 될줄 알았으나, 실제로 `loc`는 `readonly [10, 20]`의 타입을 가진다. 즉, 두개의 원소를 가진 배열임은 확실하나 값 또한 변하지 못한다는 것까지 포함하여 추론되었다. 이는 두개의 원소를 가지지만 값의 불변성까지는 보장하지 않는 `[number, number]` 타입과 다르게 된다. 그래서 이런 경우, `panTo` 함수에 `readonly`를 추가하는 것으로 해결책을 가이드하고 있다. 90 | 91 | ```typescript 92 | function panTo(where: readonly [number, number]){ 93 | console.log(where) 94 | } 95 | const loc = [10, 20] as const; // 문맥에서 값을 분리함 96 | panTo(loc); 97 | ``` 98 | 99 | 하지만 이러한 회피기는 또 다른 문제를 발생하게 하는데, 타입 단언문인 `as const`를 생길 때 발생할 수 있는 문제이다. 100 | ```typescript 101 | const loc = [10, 20, 30] as const; 102 | panTo(loc); 103 | ``` 104 | `loc`이라는 변수는 `where`가 가지는 타입을 위해서 선언한 ‘의도’를 가지고 있다. 하지만 이러한 문제를 선언시점에 파악해야하지만 실행시점에 파악해야한다는 문제가 발생할 수 있기 때문이다. 105 | 106 | 107 |
108 | 109 | 아래의 두가지 예시가 더 나오는데, 동일한 문제와 해결책을 제시하고 있다. 110 | 111 | #### 2. 객체 사용시 112 | ```typescript 113 | type Language = 'JavaScript' | 'TypeScript' | 'Python'; 114 | interface GovernedLanguage { 115 | language: Language; 116 | organization: string; 117 | } 118 | 119 | function complain(language: GovernedLanguage) { 120 | console.log(language.language); 121 | } 122 | 123 | complain({language : 'JavaScript', organization: 'Microsoft'}); 124 | 125 | const ts = { 126 | language : 'JavaScript', 127 | organization: 'Microsoft' 128 | } 129 | complain(ts); 130 | ``` 131 | 132 | 객체 `ts`는 할당시점에 타입을 가지므로 `{ language: string, organization: string }`으로 타입이 추론된다. 이는 `GovernedLanguage`와 차이가 존재한다. 이와같이 문맥에서 값을 분리하는 경우, 에러가 발생한다. 해결방법은 튜플 사용시와 동일하게 동일하다. 133 | 134 | 1. **타입구문 추가하기** 135 | ```typescript 136 | const ts: GovernedLanguage = { 137 | language : 'JavaScript', 138 | organization: 'Microsoft' 139 | } 140 | ``` 141 |
142 | 143 | 2. **타입 단언문 `as const` 사용하기** 144 | ```typescript 145 | const ts: GovernedLanguage = { 146 | language : 'JavaScript' as const, 147 | organization: 'Microsoft' 148 | } 149 | ``` 150 | - 속성에 대한 타입 단언문을 추가하여 값의 집합이 아닌 값을 타입으로 가지도록 타입 넓히기를 제한하였다. 151 | 152 |
153 | #### 3. 콜백 사용시 154 | 155 | ```typescript 156 | function callWithRandomNumbers(fn: (n1: number, n2: number) => void) { 157 | fn(Math.random(), Math.random()); 158 | } 159 | 160 | callWithRandomNumbers((a, b) => { 161 | console.log(a + b); // a, b type은 number로 추론된다. 162 | }) 163 | 164 | const func = (a, b) => { 165 | console.log(a + b); // a, b type은 any로 추론된다. 166 | } 167 | 168 | callWithRandomNumbers(func); 169 | ``` 170 | 171 | 위의 경우와 같이 `func` 함수를 변수로 선언하게 되면, 인자에 대한 타입 추론이 할당 시점에 발생하므로 모두 `any`가 된다. 이를 위해 타입 구문을 추가하여 해결한다. 172 | 173 | 1. **타입구문 추가하기** 174 | ```typescript 175 | const func = (a: number, b: number) => { 176 | console.log(a + b); 177 | } 178 | 179 | type callback = (a: number, b: number) => void; 180 | const func2: callback = (a, b) => { 181 | console.log(a + b); 182 | } 183 | ``` 184 | 185 | - 콜백 함수에 대한 타입을 정의해두는 것도 하나의 방법이다. 실제로 라이브러리의 경우, 콜백 함수의 타입을 미리 구현해둔다. 186 | 187 | 188 |
189 | 190 | ### 정리하자면, 191 | 192 | #### '문맥'으로부터 '값'을 분리하는 경우, 타입을 올바르게 지정하기 위해 사용하는 방법을 기억하자. 193 | 194 | 195 | 196 | 197 | 198 | -------------------------------------------------------------------------------- /ch03_타입_추론/item27_혁주.md: -------------------------------------------------------------------------------- 1 | # 함수형 기법과 라이브러리로 타입 흐름 유지하기 2 | 3 | 라이브러리를 타입스크립트와 조합하여 사용하면 효과적입니다. 4 | 5 | 그 이유는 타입 정보가 그대로 유지되면서 타입 흐름이 계속 전달되도록 하기 때문입니다. 6 | 7 | 예를 들어 직접 루프를 구현한다면 타입 체크에 대한 관리도 직접 해야 합니다. 8 | 9 | 어떤 CSV 데이터를 파싱한다고 가정하겠습니다. 순수 자바스크립트에서는 절차형 프로그래밍 형태로 구현할 수 있습니다. 10 | 11 | ```typescript 12 | const csvData = '...'; 13 | const rawRows = csvData.split('\n'); 14 | const headers = rawRows[0].split(','); 15 | 16 | const rows = rawRows.slice(1).map((rowStr) => { 17 | const row = {}; 18 | rowStr.split(',').forEach((val, j) => { 19 | row[headers[j]] = val; 20 | }); 21 | return row; 22 | }); 23 | ``` 24 | 25 | 함수형 마인드를 조금이라도 가진 자바스크립트 개발자라면 Reduce를 사용해서 행 객체를 만드는 법을 선호할 수도 있습니다. 26 | 27 | ```typescript 28 | const rows = rawRows 29 | .slice(1) 30 | .map((rowStr) => 31 | rowStr 32 | .split(',') 33 | .reduce((row, val, i) => ((row[headers[i]] = val), row), {}) 34 | ); 35 | ``` 36 | 37 | 이 코드는 절차형 코드에 비해 20개의 글자를 절약했지만 보는 사람에 따라 더 복잡하게 느껴질 수도 있습니다. 38 | 39 | 키와 값 배열로 취합해서 객체로 만들어 주는, 로대시의 zipObject 함수를 이용하면 코드를 더욱 짧게 만들 수 있습니다. 40 | 41 | ```typescript 42 | import _ from 'lodash'; 43 | const rows = rawRows 44 | .slice(1) 45 | .map((rowStr) => _.zipObject(headers, rowStr.split(','))); 46 | // 동일한 결과 얻을 수 있다. 47 | ``` 48 | 49 | 코드가 매우 짧아졌습니다. 그런데 자바스크립트에서는 프로젝트에 서드파티 라이브러리 종속성을 추가할 때 신중해야 합니다. 만약 코드를 짧게 줄이는 데 시간이 많이 든다면, 서드파티 라이브러리를 사용하지 않는 게 낫기 때문입니다. 50 | 51 | 그러나 같은 코드를 타입스크립트로 작성하면 서드파티 라이브러리를 사용하는 것이 무조건(무조건이라니...) 유리합니다. 타입 정보를 참고하며 작업할 수 있기 때문에 서드파티 라이브러리 기반으로 바꾸는 데 시간이 훨씬 단축됩니다. 52 | 53 | 한편, CSV 파서의 절차형 버전과 함수형 버전 모두 같은 오류를 발생시킵니다. 54 | 55 | ```typescript 56 | const csvData = '...'; 57 | const rawRows = csvData.split('\n'); 58 | const headers = rawRows[0].split(','); 59 | 60 | const rowsA = rawRows.slice(1).map((rowStr) => { 61 | const row = {}; 62 | rowStr.split(',').forEach((val, j) => { 63 | row[headers[j]] = val; 64 | // ~ '{}' 형식에서 'string' 형식의 매개변수가 포함된 인덱스 시그니처를 찾을 수 없습니다. 65 | }); 66 | return row; 67 | }); 68 | 69 | const rowsB = rawRows.slice(1).map( 70 | (rowStr) => 71 | rowStr 72 | .split(',') 73 | .reduce((row, val, i) => row[((headers[i] = val), row)], {}) 74 | // ~ '{}' 형식에서 'string' 형식의 매개변수가 포함된 인덱스 시그니처를 찾을 수 없습니다. 75 | ); 76 | ``` 77 | 78 | 두 버전 모두 {}의 타입으로 {[column: string]: string} 또는 Record을 제공하면 오류가 해결됩니다. 79 | 반면 로대시 버전은 별도의 수정 없이도 타입 체커를 통과합니다. 80 | 81 | ```typescript 82 | const rows = rawRows 83 | .slice(1) 84 | .map((rowStr) => _.zipObject(headers, rowStr.split(','))); 85 | // 타입이 _.Dictionary[] 86 | ``` 87 | 88 | Dictionary는 로대시의 타입 별칭입니다. `Dictionary`은 `{[key: string]: string}` 또는 `Record`과 동일합니다. 여기서 중요한 점은 타입 구문이 없어도 rows의 타입이 정확하다는 것입니다. 89 | 90 | 데이터의 가공이 정교해질수록 이러한 장점은 더욱 분명해집니다. 예를 들어, 모든 MBA 팀의 선수 명단을 가지고 있다고 가정해 보겠습니다. 91 | 92 | ```typescript 93 | interface BasketballPlayer { 94 | name: string; 95 | team: string; 96 | salary: number; 97 | } 98 | declare const rosters: { [team: string]: BasketballPlayer[] }; 99 | ``` 100 | 101 | 루프를 사용해 단순 선수 명단을 만들려면 배열에 concat을 사용해야 합니다. 102 | 다음 코드는 동작은 되지만 타입 체크는 되지 않습니다. 103 | 104 | ```typescript 105 | let allPlayers = []; 106 | // ~ 'allPlayers' 변수는 형식을 확인할 수 없는 경우 일부 위치에서 암시적으로 'any[]' 형식입니다. 107 | for (const players of Object.values(rosters)) { 108 | allPlayers = allPlayers.concat(players); 109 | // ~ 'allPlayers' 변수에는 암시적으로 'any[]' 형식이 포함됩니다. 110 | } 111 | ``` 112 | 113 | 이 오류를 고치려면 allPlayers에 타입 구문을 추가해야 합니다. 114 | 115 | ```typescript 116 | let allPlayers: BasketballPlayer[] = []; 117 | for (const players of Object.values(rosters)) { 118 | allPlayers = allPlayers.concat(players); 119 | } 120 | ``` 121 | 122 | 그러나 더 나은 해법은 Array.prototype.flat을 사용하는 것입니다. 123 | 124 | ```typescript 125 | const allPlayers = Object.values(rosters).flat(); 126 | // 마우스를 올려보면 타입이 BasketballPlayer[] 127 | ``` 128 | 129 | flat 메서드는 다차원 배열을 평탄화해줍니다. 타입 시그니처는 130 | `T[][] => T[]`같은 형태입니다. 이 버전이 가장 간결하고 타입 구문도 필요 없습니다. 131 | 132 | allPlayers를 가지고 각 팀별로 연봉 순으로 정렬해서 최고 연봉 선수의 명단을 만든다고 가정해 보겠습니다. 133 | 134 | 로대시 없는 버전은 다음과 같습니다. 함수형 기법을 쓰지 않은 부분은 타입 구문이 필요합니다. 135 | 136 | ```typescript 137 | const teamToPlayers: { [team: string]: BasketballPlayer[] } = {}; 138 | for (const player of allPlayers) { 139 | const { team } = player; 140 | teamToPlayers[team] = teamToPlayers[team] || []; 141 | teamToPlayers[team].push(player); 142 | } 143 | 144 | for (const players of Object.values(teamToPlayers)) { 145 | players.sort((a, b) => b.salary - a.salary); 146 | } 147 | 148 | const bestPaid = Object.values(teamToPlayers).map((players) => players[0]); 149 | bestPaid.sort((playerA, playerB) => playerA.salary - playerB.salary); 150 | console.log(bestPaid); 151 | ``` 152 | 153 | 결과는 다음과 같습니다. 154 | 155 | ```typescript 156 | [ 157 | {team:'GSW',salary:37457154,name:'Stephen Curry'}, 158 | {team:'HOU',salary:35654150,name:'Chris Paul'}, 159 | {team:'LAL',salary:35654150,name:'LeBron James'}, 160 | {team:'OKC',salary:35654150,name:'Russell Westbrook'}, 161 | {team:'DET',salary:32088932,name:'Blake Griffin'}, 162 | ... 163 | ] 164 | ``` 165 | 166 | 로대시를 사용해서 동일한 작업을 하는 코드를 구현하면 다음과 같습니다. 167 | 168 | ```typescript 169 | const bestPaid = _(allPlayers) // 타입이 BasketballPalyer[] 170 | .groupBy((player) => player.team) 171 | .mapValues((players) => _.maxBy(players, (p) => p.salary)!) 172 | .values() 173 | .sortBy((p) => -p.salary) 174 | .value(); 175 | ``` 176 | 177 | - 길이 절반으로 감소 178 | - 보기에 깔-끔 179 | - Non-null assertion operator(null 아님 연산자) 한 번만 사용 180 | - 타입 체커는 \_.maxBy로 전달된 players 배열이 비어 있지 않은지 알 수 없음 181 | - 로대시와 언더스코어의 개념인 체인을 사용하여 자연스러운 순서로 연산을 작성 182 | 183 | 만약 체인을 사용하지 않는다면 다음 예제처럼 뒤에서부터 연산이 수행됩니다. 184 | 185 | ```typescript 186 | _.c(_.b(_.a(v))); 187 | ``` 188 | 189 | 체인을 사용한다면 다음처럼 연산자의 등장 순서와 실행 순서가 동일하게 됩니다. 190 | 191 | ```typescript 192 | _(v).a().b().c().value(); 193 | ``` 194 | 195 | \_(v)는 값을 래핑(wrap)하고, .value()는 언래핑(unwrap)합니다. 196 | 197 | 그런데 내장된 Array.prototype.map 대신 \_.map을 사용하려는 이유가 무엇일까요? 198 | 한 가지 이유는 콜백을 전달하는 대신 속성의 이름을 전달할 수 있기 때문입니다. 199 | 200 | 예를 들어 다음 세 가지 종류의 호출은 모두 같은 결과를 냅니다. 201 | 202 | ```typescript 203 | const namesA = allPlayers.map((player) => player.name); // 타입이 string[] 204 | const namesB = _.map(allPlayers, (player) => player.name); // 타입이 string[] 205 | const namesC = _.map(allPlayers, 'name'); // 타입이 string[] 206 | ``` 207 | 208 | 타입스크립트 타입 시스템이 정교하기 때문에 앞의 예제처럼 다양한 동작을 정확히 모델링할 수 있습니다. 209 | 210 | 내장된 함수형 기법들과 로대시 같은 라이브러리에 타입 정보가 잘 유지되는 것은 우연이 아닙니다. 함수 호출 시 전달된 매개변수 값을 건드리지 않고 매번 새로운 값을 반환함으로써, 새로운 타입으로 안전하게 반환할 수 있습니다. 211 | 212 | 넓게 보면, 타입스크립트의 많은 부분이 자바스크립트 라이브러리의 동작을 정확히 모델링하기 위해서 개발되었습니다. 그러므로 라이브러리를 사용할 때 타입 정보가 잘 유지되는 점을 십분 활용해야 타입스크립트의 원래 목적을 달성할 수 있습니다. 213 | 214 | --- 215 | 216 | ## 요약 217 | 218 | 타입 흐름을 개선하고, 가독성을 높이고, 명시적인 타입 구문의 필요성을 줄이기 위해 직접 구현하기보다는 내장된 함수형 기법과 로대시 같은 유틸리티 라이브러리를 사용하는 것이 좋습니다. 219 | -------------------------------------------------------------------------------- /ch04_타입_설계/item28_jungho.md: -------------------------------------------------------------------------------- 1 | # 아이템 28 유효한 상태만 표현하는 타입을 지향하기 2 | 3 | > 유효한 상태만 표현한다는 것은 특정 상태를 관리할 때 **사용되는(유효한) 속성**들만 갖도록 표현하라는 것이다. 4 | 상태에 대한 타입 설계를 명확히 하지 않으면 분기문이 복잡해지거나, 분기문 내에서 유효한 상태를 놓치는 경우가 생길 수 있기 때문이다. 5 | > 6 | - 유효한 상태와 무효한 상태 모두 표현하는 타입은 예측 가능성이 떨어지고 오류를 초래하기 쉽다. 7 | - 유효한 상태만 표현하는 타입을 지향하면 코드가 길어지거나 표현이 어려울 수는 있지만 결론적으로 유지보수를 용이하게 한다. 8 | 9 |
10 | 11 | 만약 응답에 대한 **로딩상태, 에러, 성공**을 관리하는 상태가 있다고 가정한다면 아래와 같은 형태일 것이다. 12 | 13 | ```tsx 14 | interface state { 15 | isLoading: boolean; 16 | error: boolean; 17 | data: string; 18 | } 19 | ``` 20 | 21 | 위와 같은 형태도 어색하지는 않다 말 그대로 상태를 관리하는 것이니 로딩 성공 여부와 에러 발생 여부 성공 데이터가 담겨있는 형태이기 때문에 추상화는 잘못되었다고 표현할 수 없다. 22 | 23 | 하지만 이렇게 되면 케이스 분기 처리가 복잡해질 수 있다. 24 | 25 | 때문에 아래와 같이 작성한다면 상태를 정의하는 코드는 길어지겠지만 분기 처리가 용이하다. 26 | 27 | ```tsx 28 | // 이렇게만 상태를 나누고 이에 따른 함수를 작성하기보다. 29 | interface State { 30 | pageText: string; 31 | isLoading: boolean; 32 | error?: string; 33 | } 34 | 35 | function renderpage(state: State) { 36 | if (state.error) { 37 | return `Error! Unable to load ${currentpage}: ${state.error}`; 38 | } else if (state.isLoading) { 39 | return `Loading ${currentPage}...`; 40 | } 41 | return `

${currentPage}

\n${state.pageText}`; 42 | } // 분기조건이 명확하지 않다. error값이 존재하면서 로딩중인 상태일 수도 있음. 43 | 44 | // 이렇게 무효한 상태를 허용하지 않도록 코드가 길어지더라도 명시적으로 모델링하는 것이 좋다. 45 | // 그 이후 이에따른 함수 작성 46 | interface RequestPending { 47 | state: 'pending'; 48 | } 49 | 50 | interface RequestError { 51 | state: 'error'; 52 | error: string; 53 | } 54 | 55 | interface RequestSuccess { 56 | state: 'ok'; 57 | data: string; 58 | } 59 | 60 | type RequestState = RequestPending | RequestError | RequestSuccess; 61 | 62 | interface State { 63 | currentPage: string; 64 | requests: { [page: string]: RequestState }; 65 | } 66 | 67 | function renderPage(state: State) { 68 | const { currentPage } = state; 69 | const requestState = state.requests[currentPage]; 70 | 71 | switch (requestState.state) { 72 | case 'pending': 73 | return `Loading ${currentPage}...`; 74 | case 'error': 75 | return `Error! Unable to load ${currentPage}: ${requestState.error}`; 76 | case 'ok': 77 | return `

${currentPage}

\n${requestState.data}`; 78 | } 79 | } 80 | ``` -------------------------------------------------------------------------------- /ch04_타입_설계/item29_호찬.md: -------------------------------------------------------------------------------- 1 | # 아이템 29. 사용할 때는 너그럽게, 생성할 때는 엄격하게 2 | 3 | ### 견고성 원칙 (포스텔의 법칙) 4 | 5 | > 자신이 행하는 일은 엄격하게, 남의 것을 받아들일 때는 너그럽게. - 존 포스텔 (Jon Postel) 6 | 7 | 함수의 시그니처에도 비슷한 규칙을 적용해야한다. 8 | 9 | **매개변수 타입의 범위는 넓게 설계하고, 반환 타입의 범위는 좁게 설계하자.** 10 | 11 | 아래 예제에서 `LngLatBounds` 는 총 19가지 (9 + 9 + 1)형태를 지원하고 있다. 19가지 경우의 수를 지원하는 것은 좋은 설계가 아니다. 12 | 13 | ```ts 14 | declare function setCamera(camera: CameraOptions): void; 15 | declare function viewportForBounds(bounds: LngLatBounds): CameraOptions; 16 | 17 | interface CameraOptions { 18 | center?: LngLat; 19 | zoom?: number; 20 | bearing?: number; 21 | pitch?: nunmber; 22 | } 23 | 24 | type LngLat = 25 | { lng: number; lat: number; } | 26 | { lon: number; lat: number; } | 27 | [number, number]; 28 | 29 | type LngLatBounds = 30 | { northeast: LngLat; southwest: LngLat; } | 31 | [ LngLat, LngLat ] | 32 | [ number, number, number, number ] 33 | ``` 34 | 35 | 또 `viewportForBounds`의 반환 타입인 `CameraOptions`가 `setCamera`의 매개변수의 타입으로 사용되고있다. 이럴 경우 매개변수 타입이 엄격해지거나 반환되는 타입이 느슨해질 수 있는 문제가 발생할 수 있다. 36 | 37 | ```ts 38 | function focusOnFeature(f: Feature) { 39 | const bounds = calculateBoundingBox(f); 40 | const camera = viewportForBounds(bounds); 41 | setCamera(camera); 42 | const {center: {lat, lng}, zoom} = camera; 43 | // ... 형식에 'lat' 속성이 없습니다. 44 | // ... 형식에 'lng' 속성이 없습니다. 45 | zoom; // 타입이 number | undefined 46 | } 47 | ``` 48 | 49 | 타입을 정의할 때 `CameraOptions` 타입이 너무 자유로워 위와 같은 문제가 발생한다. 50 | 51 | 우리는 사용하기 편리한(매개변수의 타입은 넓고, 반환값의 타입은 엄격한) 타입으로 개선시키기 위해 **유니온 타입을 각각 코드 상에서 분기 처리할 수 있다.** 52 | 53 | 분기 처리를 위한 방법중 하나로 `LngLat`과 `LngLatLike`으로 유사 타입을 만드는 방법이 있다. 54 | 55 | ```ts 56 | declare function setCamera(camera: CameraOptions): void; // 느슨한 매개변수 타입 57 | declare function viewportForBounds(bounds: LngLatBounds): Camera; // 엄격한 반환값 타입 58 | 59 | interface Camera { // 엄격한 반환값 타입 60 | center: LngLat; 61 | zoom: number; 62 | bearing: number; 63 | pitch: number; 64 | } 65 | 66 | interface CameraOptions { // 느슨한 매개변수 타입 67 | center?: LngLatLike; 68 | zoom?: number; 69 | bearing?: number; 70 | pitch?: number; 71 | } 72 | 73 | interface LngLat { lng: number; lat: number; }; 74 | 75 | type LngLatLike = LngLat | { lon: number; lat: number; } | 76 | [number, number] 77 | ``` 78 | 79 | 다시 돌아와서 아래 에서 모든 문제가 해결됨을 볼 수 있다. 80 | 81 | ```ts 82 | function focusOnFeature(f: Feature) { 83 | const bounds = calculateBoundingBox(f); 84 | const camera = viewportForBounds(bounds); 85 | setCamera(camera); 86 | const {center: {lat, lng}, zoom} = camera; 87 | zoom; // 타입이 number 88 | } 89 | ``` 90 | 91 | ### 결론 92 | 93 | - **매개변수 타입의 범위는 넓게 설계하고, 반환 타입의 범위는 좁게 설계하자.** -------------------------------------------------------------------------------- /ch04_타입_설계/item30_혁주.md: -------------------------------------------------------------------------------- 1 | # 문서에 타입 정보를 쓰지 않기 2 | 3 | > 다음 코드에서 잘못된 부분을 찾아보세요. 4 | 5 | ```typescript 6 | /** 7 | * 전경색(foreground) 문자열을 반환합니다. 8 | * 0개 또는 1개의 매개변수를 받습니다. 9 | * 매개변수가 없을 때는 표준 전경색을 반환합니다. 10 | * 매개변수가 있을 때는 특정 페이지의 전경색을 반환합니다. 11 | */ 12 | function getForegroundColor(page?: string) { 13 | return page === 'login' ? { r: 127, g: 127, b: 127 } : { r: 0, g: 0, b: 0 }; 14 | } 15 | ``` 16 | 17 | 코드와 주석의 정보가 맞지 않습니다. 둘 중에 뭐가 맞는지 판단하기에는 정보가 부족합니다. 18 | 앞의 예제에서 의도된 동작이 코드에 제대로 반영되고 있다고 가정하면, 주석에는 세 가지 문제점이 있습니다. 19 | 20 | 1. 전경색의 문자열을 반환한다고 했는데 실제로는 `{r,g,b}` 객체를 반환합니다. 21 | 2. 주석에는 함수가 0개 또는 1개의 매개변수를 받는다고 설명하고 있지만 타입 시그니처만 봐도 명확히 알 수 있는 부분입니다. 22 | 3. 불필요하게 장황합니다(코드보다 주석이 더 깁니다). 23 | 24 | 함수의 입력과 출력의 타입을 **코드로 표현하는 것**이 주석보다 더 나은 방법입니다. 25 | 26 | - TS의 타입 구문 시스템은 간결하고, 구체적이며, 쉽게 읽을 수 있도록 설계되었습니다. 27 | - 타입 구문은 타입스크립트 컴파일러가 체크해주기 때문에 절대로 구현체와의 정합성이 어긋나지 않습니다. 28 | - getForegroundColor 함수는 과거에 문자열을 반환했지만 추후에 객체를 반환하도록 변경되었나 봅니다. 그리고 주석 수정을 깜빡했겠죠? 29 | 30 | ## 올바른 주석 사용법 31 | 32 |
33 | 34 | 1. 구현체가 변경되어도 바뀌지 않을 포괄적인 주석을 사용하는 방법 35 | 36 | ```typescript 37 | /** 애플리케이션 또는 특정 페이지의 전경색을 가져옵니다. */ 38 | function getForegroundColor(page?: string): Color { 39 | /* ... */ 40 | } 41 | ``` 42 | 43 | 2. JSDoc을 활용하는 방법 44 | 45 | - `@param` 구문을 사용하면 됩니다. (-> 아이템 48) 46 | 47 |
48 | 49 | 3. readonly를 활용하는 방법 50 | 51 | 다음과 같이 값이나 매개변수를 변경하지 않는다고 설명하는 주석도 좋지 않습니다. 52 | 53 | ```typescript 54 | /** nums를 변경하지 않습니다. */ 55 | function sort(nums: number[]) { 56 | /* ... */ 57 | } 58 | ``` 59 | 60 | 그 대신 **readonly**로 선언하여 TS가 규칙을 강제할 수 있게 하면 됩니다. 61 | 62 | ```typescript 63 | function sort(nums: readonly number[]) { 64 | /* ... */ 65 | } 66 | ``` 67 | 68 | ## 변수명 규칙 69 | 70 |
71 | 72 | 주석에 적용한 규칙은 변수명에도 그대로 적용할 수 있습니다. 변수명에 타입 정보를 넣지 않도록 합니다. 73 | 74 | - 변수명을 `ageNum`으로 하는 것보다는 `age`로 하고 그 타입이 `number`임을 명시하는게 더 좋습니다. 75 | 76 | 그러나 단위가 있는 숫자들은 예외! 77 | 78 | - `timeMS`는 `time`보다 훨씬 명확합니다. 79 | - `temperatureC`는 `temperature`보다 훨씬 명확합니다. 80 | 81 | ## 요약 82 | 83 | - 주석과 변수명에 타입 정보를 적는 것은 피해야 합니다. 84 | - 타입 선언이 중복되는 것으로 끝나면 다행이지만 최악의 경우 타입 정보에 모순이 발생하게 됩니다. 85 | - 타입이 명확하지 않은 경우는 변수명에 타입 정보를 포함하는 것이 좋습니다. 86 | -------------------------------------------------------------------------------- /ch04_타입_설계/item31_jungho.md: -------------------------------------------------------------------------------- 1 | # 아이템31 타입 주변에 null 값 배치하기 2 | 3 | [strictNullChecks](https://www.typescriptlang.org/tsconfig#strictNullChecks) 설정을 활성화 시키면, `null`이나 `undefined` 값에 관련된 오류들이 갑자기 발생한다. 4 | 5 | 따라서 오류를 회피하기 위해 null check하는 `if` 예외 처리 구문이 코드 전반에 추가 되어야 한다고 생각 할 수 있다. 6 | 7 | 왜냐하면 만약 변수가 여러개면서 서로 종속되는 관계가 있는 변수가 있다면, 이러한 관계가 겉으로 드러나는 것이 아니기 때문에 개발자, 타입체커 모두에게 혼란스러워 타입만으로 어떤 변수가 `null`이 될 수 있는지 파악하기 어려워지기 때문이다. 8 | 9 |
10 | 11 | 그러나, 값이 전부 `null`이거나 전부 `null`이 아닌 경우로 분명하게 구별될 수만 있다면 코드 작성이 훨씬 쉽다고 한다. 12 | 13 | 이와 관련하여 타입에 `null`을 추가하는 방식을 사용할 수 있는데, 이 장에선 이를 어떻게 모델링하고 어떻게 사용했을 때 편리해 지는지 소개하고 있다. 14 | 15 |
16 | 17 | 다음은 숫자들의 최소값과 최대값을 게산하는 extent 함수 예제이다. 18 | 19 | ```tsx 20 | // 버그가 발생할 수 있는 코드 21 | function extent(nums: number[]){ 22 | let min, max; 23 | for (const num of nums){ 24 | if(!min) { 25 | min = num; 26 | max = num; 27 | } 28 | else { 29 | min = Math.min(min, num); 30 | max = Math.max(max, num); 31 | // strictNullChecks 옵션을 키면 'number | undefined' 형식의 인수는 'number'형식에 할당할 수 없음을 알려줌 32 | } 33 | } 34 | return [min, max]; // (number | undefined)[] 35 | } 36 | // 배열에 0이 포함된다면, 의도치 않게 if(!min) 조건이 성립하고 값이 덧씌워져 버림 37 | // nums 빈 배열이면 [undefined, undefined] 배열이 반환 38 | // undefined를 포함하는 객체는 다루기 어렵기 때문에 권장하지 않음 39 | 40 | // 해법 41 | // min, max를 한 객체에 넣고 null 타입 체크 42 | function extentFix(nums: number[]) { 43 | // 타입 주변에 null 값 배치 44 | let result: [number, number] | null = null;  45 | for(const num of nums) { 46 | if(!result){ 47 | result = [num, num]; 48 | } else { 49 | result = [Math.min(num, result[0]), Math.max(num, result[1])]; 50 | } 51 | } 52 | return result; 53 | } 54 | // 이제 반환 타입이 [number, number] | null 55 | // null 단언 또는 if문을 통한 예외처리로 min, max값 추출 가능 56 | ``` 57 | 58 |
59 | 60 | `null`과 `null`이 아닌 값을 혼재하여 사용하면 클래스에서도 문제가 발생한다. 61 | 62 | 예를 들어, 사용자와 그 사용자의 포럼 게시글을 나타내는 클래스가 있다고 가정해보자. 63 | 64 | ```tsx 65 | class UserPosts { 66 | user: UserInfo | null; 67 | posts: Post[] | null; 68 | 69 | constructor(){ 70 | this.user = null; 71 | this.posts = null; 72 | } 73 | 74 | async init(userId: string) { 75 | return Promise.all([ // 프로미스가 완료되지 않은 상태에서 user와 Post 상태는 null이게 된다. 76 | async () => this.user = await fetchUser(userId), 77 | async () => this.posts = await fetchPostUser(userId) 78 | ]); 79 | } 80 | 81 | getUser(){ 82 | // ... 83 | } 84 | } 85 | ``` 86 | 87 | 시점에 따라 총 4가지 상태를 가질 수 있다. (`user`, `posts` 둘 다 `null`이거나 모두 `null`이 아니거나, 둘 중 하나만 `null`인 두 가지 상태) 88 | 89 | 속성 값의 불확실성이 클래스의 모든 메서드에 나쁜 영향을 미칠수 있다. 90 | 91 | 이를 방지하기 위해 `null` 체크가 난무하게 되고 가독성이 떨어지며 버그가 양산될 가능성을 높아진다. 92 | 93 |
94 | 95 | 이를 개선하게 되면, 96 | 97 | ```tsx 98 | class UserPostsFix { 99 | user: UserInfo; 100 | posts: Post[]; 101 | 102 | constructor(user: UserInfo, posts: Post[]){ 103 | this.user = user; 104 | this.posts = posts; 105 | } 106 | 107 | static async init(userId: string) : Promise { 108 | const [user, posts] = await Promise.all([ 109 | fetchUser(userId), 110 | fetchPostsForUser(userId) 111 | ]); 112 | 113 | return new UserPostsFix(user, posts); 114 | } 115 | 116 | getUserName() { 117 | return this.user.name; 118 | } 119 | } 120 | ``` 121 | 122 | 이제 UserPosts 클래스내 속성은 완전히 `null`이 아니게 되어, 메서드를 작성할 때 불확실성을 줄일 수 있다. 123 | 124 | 물론 이 또한, 데이터가 부분적으로 fetch되었을 때 무언가 작업해야 한다면, `null` 또는 `null`이 아닌 상태로 다루어야 하긴 한다. 125 | 126 |
127 | 128 | **요약** 129 | 130 | - 한 값의 `null` 여부가 다른 값의 `null` 여부에 암시적으로 관련되도록 설계하는 것은 피해야한다. 131 | - API 작성 시에는 반환 타입을 큰 객체로 만들고 반환 타입 전체가 `null`이거나 `null`이 아니게 만들어야 한다. 132 | - 클래스를 만들 때는 필요한 모든 값이 준비되었을 때 생성하여 `null`이 존재하지 않도록 하는 것이 좋다. 133 | - `strictNullChecks`를 설정하면 타입 오류가 많이 표시되겠지만, `null`값과 관련된 문제점 발견에 도움이 되므로 반드시 필요하다. -------------------------------------------------------------------------------- /ch04_타입_설계/item32_dami.md: -------------------------------------------------------------------------------- 1 | # 아이템 32. 유니온의 인터페이스보다는 인터페이스의 유니온 사용하기 2 | 3 | 4 | 5 | ```typescript 6 | interface Layer { 7 | layout: FillLayout | LineLayout | PointLayout; 8 | paint: FillPaint | LinePaint | PointPaint; 9 | } 10 | ``` 11 | 12 | 위 예제에서 `layout`과 `paint`의 타입은 짝이 맞아야 하기에 `layout`이 `LineLayout` 타입이며 `paint`가 `FillPaint` 타입일 수 없습니다. 이런 방식을 허용한다면 오류가 발생하기 쉽고, 인터페이스 다루기도 어려울 것입니다. 13 | 14 | `Layer` 인터페이스를 각각 타입 계층을 분리하여 인터페이스로 두어 더 나은 모델링을 할 수 있습니다. 15 | 16 | ```typescript 17 | interface FillLayer { 18 | layout: FillLayout; 19 | paint: FillPaint; 20 | } 21 | interface LineLayuer { 22 | layout: LineLayout; 23 | paint: LinePaint; 24 | } 25 | interface PointLayer { 26 | layout: PointLayout; 27 | paint: PointPaint; 28 | } 29 | type Layer = FillLayer | LineLayer | PointLayer; 30 | ``` 31 | 32 | 위와 같은 형태는 `layout`과 `point` 속성 조합이 잘못되는 것을 방지할 수 있습니다. 이는 아이템 28의 조언인 '유효한 상태만을 표현' 하는 타입 정의 방식입니다. 이 방식을 `태그된 유니온(Tagged Union)`이라 하는데, `태그된 유니온`을 이용하여 속성간의 관계를 명확히 할 수 있습니다. 33 | 34 | > **태그된 유니온 (Tagged Union)** 이란 35 | > 상황에 따라 인터페이스를 분리한 후 해당 **인터페이스들을 `type`을 이용하여 유니온으로 사용한 것** 36 | > **태그**는 `type`에 주어진 개별 속성이며 런타임 시 **타입의 범위를 줄일 수 있도록 도와준다** 37 | 38 | 39 | 40 | ```typescript 41 | interface FillLayer { 42 | type: 'fill'; 43 | layout: FillLayout; 44 | paint: FillPaint; 45 | } 46 | interface LineLayuer { 47 | type: 'line'; 48 | layout: LineLayout; 49 | paint: LinePaint; 50 | } 51 | interface PointLayer { 52 | type: 'paint'; 53 | layout: PointLayout; 54 | paint: PointPaint; 55 | } 56 | type Layer = FillLayer | LineLayer | PointLayer; 57 | ``` 58 | 59 | 각 `interface`의 `type` 속성은 '태그'이며 런타임에 어떤 타입의 `Layer`가 사용되는지 판단하는 데 쓰입니다. 타입스크립트 이 태그를 참고하여 아래 예제처럼 Layer 타입의 범위를 좁힐 수도 있습니다. 60 | 61 | 62 | 63 | ```typescript 64 | function drawLayer(layer: Layer) { 65 | if(layer.type === 'fill') { 66 | const { paint, layout } === layer //타입이 FillPaint, FillLayout 67 | } 68 | .. 69 | .. 70 | } 71 | ``` 72 | 73 | 이처럼 각 타입 속성 간의 관계를 제대로 모델링하면, 타입스크립트가 코드의 정확성을 체크하는 데 도움이 됩니다. 어떤 데이터 타입을 태그된 유니온으로 표현할 수 있다면, 보통 그렇게 하는 것이 좋습니다. 또는 여러 개의 선택적 필드가 동시에 값이 있거나 동시에 undefined인 경우도 태그된 유니온 패턴이 잘 맞습니다. 74 | 75 | 76 | 77 | ### 문제 : 속성 간의 관계가 잘 표현되지 않음 78 | 79 | ```typescript 80 | interface Person { 81 | name: string; 82 | // 아래 속성은 둘 다 있거나, 둘 다 없음 83 | placeOfBirth?: string; 84 | dateOfBirth?: Date; 85 | } 86 | ``` 87 | 88 | 인터페이스 내 타입 정보를 담고 있는 주석은 문제가 될 소지가 있기에 지양하는 것이 좋습니다. (아이템 30) 위 인터페이스 내 `placeOfBirth` 와 `dateOfBirth` 필드는 실제로 관련되어 있지만, 어떠한 관계도 표현되지 않았습니다. 89 | 90 | 91 | 92 | ### 해결첵: optional 속성을 하나의 객체로 모으기 93 | 94 | 이때는 두 속성을 하나의 객체로 모으는 것이 더 나은 설계입니다. 이는 `null` 값을 경계로 두는 아이템 31의 방법과 비슷합니다. 95 | 96 | ```typescript 97 | interface Person { 98 | name: string; 99 | // 아래 속성은 둘 다 있거나, 둘 다 없음 100 | birth?: { 101 | placeOfBirth: string; 102 | dateOfBirth: Date; 103 | } 104 | } 105 | ``` 106 | 107 | ### 결과 108 | 109 | - `place`만 있고 `date`가 없는 경우에 오류가 발생합니다. 110 | 111 | - `Person` 객체를 매개변수로 받는 경우에는 `birth` 하나만 체크해주면 됩니다. 112 | 113 | ```typescript 114 | function foo (p: Person) { 115 | const { birth } = p; 116 | if (birth) { 117 | ~~ 118 | } 119 | } 120 | ``` 121 | 122 | 123 | 124 | 타입의 구조를 손 댈 수 없는 상황(예를 들어 API의 결과)이면, 앞서 다룬 인터페이스의 유니온을 사용해서 속성 사이의 관계를 모델링 할 수 있습니다. 125 | 126 | ```typescript 127 | interface Name { 128 | name : string; 129 | } 130 | 131 | interface PersonWithBirth extends Name { 132 | placeOfBirth: string; 133 | dateOfBirth: Date; 134 | } 135 | 136 | type Person = Name | PersonWithBirth; 137 | ``` 138 | 139 | 이제 중첩된 객체에서도 동일한 효과를 볼 수 있습니다. 140 | 141 | ```typescript 142 | function foo(p: Person) { 143 | if ('placeOfBirth' in p) { 144 | p // PersonWithBirth 타입 145 | const { dateOfBirth } = p; // Date 타입 146 | } 147 | } 148 | ``` 149 | 150 | 151 | 152 | ## 결론 153 | 154 | - 유니온 타입의 속성을 여러개 가지는 인터페이스는 속성간의 관계가 분명하지 않아 실수가 발생할 수 있다. 155 | - 타입 정확도 : 유니온의 인터페이스 < 인터페이스의 유니온 156 | - 태그된 유니온은 타입스크립트와 잘 맞는다. 태그된 유니온을 사용해서 타입스크립트가 제어 흐름을 분석할 수 있도록 하자. 157 | -------------------------------------------------------------------------------- /ch04_타입_설계/item33_chanu.md: -------------------------------------------------------------------------------- 1 | # string 타입보다 구체적인 타입 사용하기 2 | 3 | string 타입의 경우, 값의 범위가 매우 크기 때문에 any와 유사한 문제가 발생할 수 있다. 단순히 string 타입을 정의하는 것 보다 명확한 의미와 범위가 존재한다면 값의 유니온 타입으로 정의하는 것이 더 좋다. 4 | 5 | ```typescript 6 | interface AlbumPre { 7 | artist: string; 8 | title: string; 9 | releaseDate: string; 10 | recordingType: string; 11 | } 12 | ``` 13 | - AlbumPre의 모든 속성의 타입은 string이다. 14 | - releaseDate 속성의 경우, 날짜의 의미를 담은 string 타입으로 'YYYY-MM-DD', 'YYMMDD'등으로 문자열의 형식이 제한되어있지 않기 때문에 유효성 검사가 쉽지 않다. 15 | - recordingType 속성의 경우, 실제 해당 속성의 의도는 'studio' 'live' 두 개의 값으로 표현이 가능하지만 string을 사용하고 있다. 16 | 17 | 18 | ```typescript 19 | type RecordingType = 'studio' | 'live'; 20 | 21 | interface Album { 22 | artist: string; 23 | title: string; 24 | releaseDate: Date; 25 | recordingType: RecordingType; 26 | } 27 | ``` 28 | - releaseDate 속성이 표현할 수 문자열 형식과 무관하게 날짜의 의미를 가질 수 있도록 Date 객체 타입으로 변경하였다. 29 | - recordingType 속성이 가질 수 있는 문자열 값을 유니온을 통해 타입의 범위를 줄였다. 30 | 31 | ### 구체적인 타입을 통해서 타입의 범위를 좁힐 때의 장점 32 | 33 | 34 | #### 1. 객체의 속성을 독립적으로 사용할 때 그 의미가 명확하게 전달된다. 35 | 36 | ```typescript 37 | type getAlbumsOfType = (recordingType: string) => Album[]; 38 | 39 | type RecordingType = 'studio' | 'live'; 40 | type getAlbumsOfType = (recordingType: RecordingType) => Album[]; 41 | ``` 42 | - 위 함수의 경우, 전달하는 값이 Album 객체의 recordingType 속성으로 '녹음 유형'이라는 의미를 가지지만 string 타입이므로 해당 함수 시그니처로는 단순히 문자열이 전달된다는 의미만 해석할 수 있다. 43 | - 아래의 함수의 경우, 인자가 RecordingType 타입으로 '녹음 유형'이라는 의미를 명확하게 전달함으로써 'studio' 또는 'live' 문자열만을 가질 수 있다는 의미를 추가적으로 전달할 수 있다. 44 | 45 |
46 | 47 | #### 2. 타입에 대한 주석을 편집기를 통해 확인할 수 있다. 48 | 49 | ```typescript 50 | /* 이 녹음은 어떤 환경에서 이루어졌는가? */ 51 | type RecordingType = 'studio' | 'live'; 52 | 53 | type getAlbumsOfType = (recordingType: RecordingType) => Album[]; 54 | ``` 55 | 56 | - 편집기를 통해서 함수 시그니처에서 RecordingType에 대한 설명, 이 녹음은 어떤 환경에서 이루어졌는가?'를 확인할 수 있다. 57 | 58 |
59 | 60 | #### 3. keyof 연산자로 더욱 세밀하게 객체의 속성 체크가 가능하다. 61 | 62 | 객체의 속성을 사용하는 경우, 발생하는 문제를 조금 더 명확한 타입을 정의함으로써 해결할 수 있다. 예제를 위해 객체를 인자로 받아 해당 객체의 속성을 사용하는 함수를 구현하였다. 63 | 64 | ```typescript 65 | function pluck(records: T[], key: string): any[] { 66 | return records.map(r => r[key]); 67 | } 68 | ``` 69 | 70 | - key 인자는 T객체의 속성명으로 사용하려고 하였으나, `r[key]`에서 속성명의 타입보다 string의 타입이 더 범위가 넓기 때문에 오류가 발생한다. 예를 들어 위의 Album 객체를 사용한다고 하였을 때, 가질 수 있는 속성명은 `'artist'`, `'title'`, `'releaseDate'`, `'recordingType'` 총 네가지의 문자열이지만 string 타입은 이 값 이외의 모든 문자열이 가능하다. 71 | - 이를 명확하게 하기 위해 string 타입 대신 `keyof`를 사용하여 타입의 범위를 줄인다. 72 | 73 | ```typescript 74 | function pluck(records: T[], key: keyof T):T[keyof T][] { 75 | return records.map(r => r[key]); 76 | } 77 | 78 | const albums: Album[] = [ 79 | { 80 | artist: 'chanwoo', 81 | title:'sea', 82 | releaseDate: new Date('2022-03-02'), 83 | recordingType: 'studio' 84 | } 85 | ] 86 | 87 | const releaseDates = pluck(albums, 'releaseDate'); // (string | Date)[] 88 | ``` 89 | 90 | - key 속성의 타입을 `keyof T`를 통해서 `'artist' | 'title' | 'releaseDate' | 'recordingType'` 으로 타입의 범위를 제한하였다. 91 | - 하지만 반환값의 타입이 `T[keyof T]`은 T의 모든 속성값이므로 `string | Date`가 된다. 이는 단일 속성의 값의 배열을 가져야하는 반환값의 의도와는 차이가 존재한다. 92 | - `K extends keyof T`를 통해서 `keyof T`의 유니온 타입에서 단 하나의 타입만을 가지도록 K를 추출해 사용한다. 93 | 94 | ```typescript 95 | function pluck(records: T[], key: K): T[K][] { 96 | return records.map(r => r[key]); 97 | } 98 | ``` 99 |
100 | 101 | ### 정리하자면, 102 | 103 | #### 1. 단순히 `string` 타입을 사용하는 것이 아니라 유니온 타입을 통해 의도에 맞게 타입의 범위를 제한하여 사용한다. 104 | 105 | #### 2. 객체의 속성을 인자로 사용할 때는 `string`이 아닌 `typeof`를 사용하여 의도에 맞게 타입의 범위를 제한하여 사용한다. 106 | 107 | -------------------------------------------------------------------------------- /ch04_타입_설계/item34_혁주.md: -------------------------------------------------------------------------------- 1 | # 부정확한 타입보다는 미완성 타입을 사용하기 2 | 3 | 타입 선언을 작성할 때, 일반적으로 타입이 구체적일수록 버그를 더 많이 잡고 타입스크립트가 제공하는 도구를 활용할 수 있습니다. 4 | 5 | 그러나 타입 선언의 정밀도를 높이는 일에는 주의를 기울여야 합니다. 잘못된 타입은 차라리 타입이 없는것 보다 못할 수 있기 때문입니다. 6 | 7 | GeoJSON 형식의 타입 선언을 작성한다고 가정해보겠습니다. 8 | 9 | ```typescript 10 | interface Point { 11 | type: 'Point'; 12 | coordinates: number[]; 13 | } 14 | 15 | interface LineString{ 16 | type: 'LineString'; 17 | coordinates: number[][]; 18 | } 19 | 20 | interface Polygon{ 21 | type: 'Polygon'; 22 | coordinates: number[][][]; 23 | } 24 | 25 | type Geometry = Point | LineString | Polygon; 26 | ``` 27 | 28 | Point 인터페이스의 `number[]`가 추상적이라고 생각하여 튜플 타입으로 선언한다고 해봅시다. 29 | 30 | ```typescript 31 | type GeoPosition = [number,number]; 32 | interface Point{ 33 | type: 'Point'; 34 | coordinates: GeoPosition; 35 | } 36 | ``` 37 | 38 | 더 구체적인 코드이지만, GeoJSON의 위치 정보에는 세 번째 요소가 있을 수도 있고, 또 다른 정보가 들어갈 수도 있습니다. 39 | > 결과적으로 타입 선언을 세밀하게 만들고자 했지만 시도가 너무 과했고 오히려 타입이 부정확해졌습니다. 40 | 41 |
42 | 43 | ### JSON으로 정의된 Lisp와 비슷한 언어의 타입 선언을 작성할 경우 44 | 45 | ```JSON 46 | 12 47 | "red" 48 | ["+",1,2] // 3 49 | ["/",20,2] // 10 50 | ["case",[">",20,10],"red","blue"] 51 | ["rgb",255,0,127] 52 | ``` 53 | 54 | 이런 동작을 모델링해 볼 수 있는 입력값의 전체 종류를 살펴보겠습니다. 55 | 1. 모두 허용 56 | 2. 문자열, 숫자, 배열 허용 57 | 3. 문자열, 숫자, 알려진 함수 이름으로 시작하는 배열 허용 58 | 4. 각 함수가 받는 매개변수의 개수가 정확한지 확인 59 | 5. 각 함수가 받는 매개변수의 타입이 정확한지 확인 60 | 61 | ```typescript 62 | type Expression1 = any; // 1. 모두 허용 63 | type Expression2 = number | string | any[]; // 2. 문자열, 숫자, 배열 허용 64 | ``` 65 | 66 | 정밀도를 더 끌어올리기 위해서 튜플의 첫 번째 요소에 문자열 리터럴 요소를 추가합니다. 67 | 68 | ```typescript 69 | type FnName = '+' | '-' | '*' | '/' | '>' | '<' | 'case' | 'rgb'; 70 | type CallExpression = [FnName, ...any[]]; 71 | type Expression3 = number | string | CallExpression; 72 | 73 | const tests: Expression3[] = [ 74 | 10, 75 | "red", 76 | true, // ~~ true 형식은 'Expression3' 형식에 할당할 수 없습니다. 77 | ["+",10,5], 78 | ["case",[">",20,10],"red","blue","green"], 79 | ["**",2,31], // ~~ "**" 형식은 'FnName' 형식에 할당할 수 없습니다. 80 | ["rgb",255,128,64] 81 | ] 82 | ``` 83 | 84 | 더 정밀하게 바꿔보도록 하겠습니다. 85 | 86 | ```typescript 87 | type Expression4 = number | string | CallExpression; 88 | type CallExpression = MathCall | CaseCall | RGBCall; 89 | 90 | interface MathCall{ 91 | 0: '+' | '-' | '/' | '*' | '>' | '<'; 92 | 1: Expression4; 93 | 2: Expression4; 94 | length: 3; 95 | } 96 | 97 | interface CaseCall { 98 | 0: 'case'; 99 | 1: Expression4; 100 | 2: Expression4; 101 | 3: Expression4; 102 | length: 4 | 6 | 8 | 10 | 12 | 14 | 16 // 등등... 103 | } 104 | 105 | interface RGBCall{ 106 | 0: 'rgb'; 107 | 1: Expression4; 108 | 2: Expression4; 109 | 3: Expression4; 110 | length: 4; 111 | } 112 | 113 | const test4: Expression4[] = [ 114 | 10, 115 | "red", 116 | true, // ~~ true 형식은 'Expression4' 형식에 할당할 수 없습니다. 117 | ["+",10,5], 118 | ["case",[">",20,10],"red","blue","green"],// ~~ type "case"는 type "rgb"에 할당할 수 없습니다. 119 | ["**",2,31], // ~~ type "**"는 type "rgb"에 할당할 수 없습니다. 120 | ["rgb",255,128,64], 121 | ["rgb",255,128,64,73],// 길이가 5라서 안됨~ 122 | ] 123 | ``` 124 | 125 | 이렇게 하면 무효한 표현식에서 전부 오류가 발생합니다. 그러나 오류가 나면 엉뚱한 메세지를 출력하며, **에 대한 오류는 오히려 이전 버전보다 메시지가 부정확해집니다. 126 | 타입 정보가 더 정밀해졌지만 결과적으로 이전 버전보다 개선되었다고 보기는 어렵습니다. 127 | 128 | 타입 선언의 복잡성으로 인해 버그가 발생할 가능성도 높아졌습니다. 129 | (잘못된 오류를 표시하는 것을 볼 수 있습니다) 130 | 131 | 스크린샷 2022-11-09 오전 12 08 34 132 | 133 | 코드를 더 정밀하게 만들려던 시도가 너무 과했고 그로 인해 코드가 오히려 더 부정확해졌습니다. 134 | 타입을 정제할 때, 불쾌한 골짜기 은유를 생각해 보면 도움이 될 수 있습니다. 135 | 136 | - 불쾌한 골짜기? 137 | 138 | ![불쾌한골짜기](https://user-images.githubusercontent.com/76726411/200603218-d8fdccd8-bf72-4176-9b02-7d9f4bfa156c.jpeg) 139 | 140 | 일반적으로 any 같은 매우 추상적인 타입은 정제하는 것이 좋습니다. 그러나 타입이 구체적으로 정제된다고 해서 정확도가 무조건 올라가지는 않습니다. 141 | 142 | ## 요약 143 | - 타입 안전성에서 불쾌한 골짜기는 피해야 합니다. 타입이 없는 것보다 잘못된 게 더 나쁩니다. 144 | - 정확하게 타입을 모델링할 수 없다면, 부정확하게 모델링하지 말아야 합니다. 145 | -------------------------------------------------------------------------------- /ch04_타입_설계/item35_dami.md: -------------------------------------------------------------------------------- 1 | # 아이템 35. 데이터가 아닌, API 명세를 보고 타입 만들기 2 | 3 | ## API 명세를 바탕으로 타입을 생성하자 4 | 5 | API 명세를 참고해 타입을 생성하면 타입스크립트는 사용자가 실수를 줄일 수 있게 도와줍니다. 6 | 7 | 반면에 예시 데이터를 참고해 타입을 생성하면 눈앞에 있는 데이터들만 고려하게 되므로 예기치 않은 곳에서 오류가 발생할 수 있습니다. 8 |
9 | 10 | ```typescript 11 | // requires node modules: @types/geojson 12 | 13 | interface BoundingBox { 14 | lat: [number, number]; 15 | lng: [number, number]; 16 | } 17 | import { Feature } from "geojson"; 18 | 19 | function calculateBoundingBox(f: Feature): BoundingBox | null { 20 | let box: BoundingBox | null = null; 21 | 22 | const helper = (coords: any[]) => { 23 | // ... 24 | }; 25 | 26 | const { geometry } = f; 27 | if (geometry) { 28 | helper(geometry.coordinates); 29 | // ~~~~~~~~~~~ 30 | // 'Geometry' 형식에 'coordinates' 속성이 없습니다. 31 | // 'GeometryCollection' 형식에 'coordinates' 속성이 없습니다. 32 | } 33 | 34 | return box; 35 | } 36 | ``` 37 | 38 | 위 예제에서는 geometry에 coordinates 속성이 있다고 가정한 것이 문제입니다. 다른 도형과 다르게 GeometryCollection에는 coordinates 속성이 없기 때문입니다. 이 오류는 GeometryCollection을 명시적으로 차단하여 해결할 수 있습니다. 39 | 40 |
41 | 42 | ```typescript 43 | function helper(coordinates: any[]) {} 44 | const { geometry } = f; 45 | if (geometry) { 46 | if (geometry.type === "GeometryCollection") { 47 | throw new Error("GeometryCollections are not supported."); 48 | } 49 | helper(geometry.coordinates); // OK 50 | } 51 | ``` 52 | 53 | 하지만 위와 같이 GeometryCollection 타입을 차단하기 보다는 모든 타입을 지원하는 것이 더 좋은 방법이기 때문에 조건을 분기해서 헬퍼 함수를 호출하면 모든 타입을 지원할 수 있습니다. 54 | 55 |
56 | 57 | ```typescript 58 | function helper(coordinates: any[]) {} 59 | const geometryHelper = (g: Geometry) => { 60 | if (geometry.type === "GeometryCollection") { 61 | geometry.geometries.forEach(geometryHelper); 62 | } else { 63 | helper(geometry.coordinates); // OK 64 | } 65 | }; 66 | 67 | const { geometry } = f; 68 | if (geometry) { 69 | geometryHelper(geometry); 70 | } 71 | ``` 72 | 73 | 이렇듯 직접 작성한 타입 선언에는 GeometryCollection 같은 예외 상황이 포함되지 않고, 완벽할 수도 없습니다. 74 | 75 | API 명세를 기반으로 타입을 작성한다면 현재까지 경험한 데이터 뿐만 아니라 사용 가능한 모든 값에 대해 작동한다는 확신을 가질 수 있습니다. 76 | 77 |
78 | 79 | ## GraphQL - 자체적으로 타입 정의 가능 80 | 81 | GraphQL의 장점은 특정 쿼리에 대해 타입스크립트 타입을 생성할 수 있다는 것입니다. 82 | 83 | 타입은 GraphQL의 스키마로 부터 생성되기 때문에 스키마,쿼리가 바뀐다면 결과에 대한 타입도 자동으로 바뀌어 타입과 실제 값이 항상 일치합니다. 84 | 85 | 하지만 GraphQL와 같은 기술을 사용하지 않는다면 데이터로부터 타입을 생성해야 합니다. quicktype과 같은 도구를 쓸 수 있는데, 이렇게 생성된 데이터가 실제 데이터와 항상 일치하지 않을 수 있습니다. 86 | 87 |
88 | 89 | ## 결론 90 | 91 | - 코드의 안정성을 위해선 API 명세 기반으로 타입을 작성하고, 코드를 생성하는 것이 좋다. 92 | -------------------------------------------------------------------------------- /ch04_타입_설계/item36_chanu.md: -------------------------------------------------------------------------------- 1 | # 해당 분야의 용어로 타입 이름 짓기 2 | 3 | > 엄선된 타입, 속성, 변수의 이름은 의도를 명확히하고 코드와 타입의 추상화 수준을 높여줍니다. 4 | 5 |
6 | 7 | ```typescript 8 | interface Animal { 9 | name: string; 10 | endangered: boolean; 11 | habitat: string; 12 | } 13 | ``` 14 | 15 | - 동물을 표현하는데 있어서 `name`은 동물의 학명이 될 수도, 일반적인 명칭이 될 수 있기 때문에 해당 속성명으로는 의미가 모호하다. 16 | - `endangered`는 멸종위기를 표현하려고 하였으나, 실제 멸종된 동물에 대해서는 표현할 수 없기 때문에 그 의미가 모호하다. 17 | - `habitat`을 string 타입으로 두게되면, 기후의 종류와 표현하는 방식이 다양하기 때문에 모호하다. 18 | 19 |
20 | 21 | ```typescript 22 | interface Animal { 23 | commonName: string; 24 | genus: string; 25 | species: string; 26 | staus: ConservationStatus; 27 | climates: KoppenClimate[]; 28 | } 29 | ``` 30 | - `name`을 구체적으로 일반적인 명칭과 동물의 학명을 각각 속성으로 나누어 표현하였다. 31 | - `endangered` 단순 멸종위기 여부가 아닌, 동물 보호 등급이라는 표준 표기법을 사용하도록 변경하였다. 32 | - `habitat` 값의 범위가 너무나도 넓기 때문에 쾨펜 기후 분류라는 표준 표기법을 사용하도록 변경하였다. 33 | 34 |
35 | 36 | > 표현하고자 하는 분야에 존재하는 전문 용어를 사용하면 모호함으로 인해 이전 개발자나 다른 사람들에게 추가적인 정보를 제공받을 필요가 없게됩니다. 37 | 38 | #### 타입, 속성, 변수에 이름을 붙일 때, 명심해야 할 세 가지 규칙 39 | 40 | 1. 동일한 의미를 표현할 때 사용하는 용어는 반드시 통일해야한다. 41 | 2. `data`, `info`, `thing`, `item`, `object`, `entity`와 같이 의미가 없지만 단순히 접미사로는 사용하지 않는다. 42 | 3. 계산 방식이 아닌 데이터에 집중한다. `INodeList` -> `Directory` 43 | 44 |
45 | 46 | ### 정리하자면, 47 | 48 | #### 변수, 타입, 속성의 이름을 사용할 때는 구현하고자 하는 분야에 전문적인 용어가 존재하는지 우선적으로 확인하고 가급적 사용한다. -------------------------------------------------------------------------------- /ch04_타입_설계/item37_호찬.md: -------------------------------------------------------------------------------- 1 | # 아이템 37. 공식 명칭에는 상표 붙이기 2 | 3 | 구조적 타이핑의 특성 때문에 이상한 결과를 도출 할 수 있다. 4 | 5 | ```typescript 6 | interface Vector2D { 7 | x: number; 8 | y: number; 9 | } 10 | 11 | function calculateNorm(p: Vector2D) { 12 | return Math.sqrt(p.x * p.x + p.y * p.y); 13 | } 14 | 15 | calculateNorm({x: 3, y: 4}); // 정상, 결과는 5 16 | const vec3D = {x: 3, y: 4, z: 1}; 17 | calculateNorm(vec3D); // 정상! 결과는 동일하게 5 18 | ``` 19 | 20 | 이 코드는 구조적 타이핑 관점에서는 문제가 없지만, 수학적으로 따지면 2차원 백터를 사용해야 이치에 맞다. 21 | 22 | `calculateNorm` 함수가 3차원 백터를 허용하지 않게 하려면 **공식 명칭(nominal typing)을** 사용하면 된다. **공식 명칭을 사용하는 것은, 타입이 아니라 값의 관점에서 `Vector2D`라고 말하는 것이다.** 23 | 24 | 공식 명칭 개념을 타입스크립트에서는 **상표(brand)를** 붙여서 흉내낼 수 있다. 25 | 26 | ```typescript 27 | interface Vector2D { 28 | _brand: '2d'; 29 | x: number; 30 | y: number; 31 | } 32 | 33 | function vec2D(x: number, y: number): Vector2D { 34 | return {x, y, _brand: '2d'}; 35 | } 36 | 37 | function calculateNorm(p: Vector2D) { 38 | return Math.sqrt(p.x * p.x + p.y * p.y); 39 | } 40 | 41 | calculateNorm({x: 3, y: 4}); // 정상, 결과는 5 42 | const vec3D = {x: 3, y: 4, z: 1}; 43 | calculateNorm(vec3D); // '_brand' 속성이 ... 형식에 없습니다. 44 | ``` 45 | 46 | `_brand`를 사용해서 `calculateNorm` 함수가 `Vector2D` 타입만 받는 것을 보장할 수 있다. 47 | 48 | **상표 기법은 타입 시스템에서 동작하지만 런타임에 상표를 검사하는 것과 동일한 효과를 얻을 수 있다.** 타입 시스템이기 때문에 런타임 오버헤드를 없앨 수 있고 추가 속성을 붙일 수 없는 `string`이나 `number` 같은 내장 타입도 상표화 가능하다. 49 | 50 | ### 내장 타입도 상표화 51 | 52 | ```typescript 53 | type AbsolutePath = string & {_brand: 'abs'}; 54 | 55 | function listAbsolutePath(path: AbsolutePath) { 56 | //... 57 | } 58 | 59 | function isAbsolutePath(path: string): path is AbsolutePath { 60 | return path.startWith('/'); 61 | } 62 | ``` 63 | 64 | `string` 타입이면서 `_brand` 속성을 가지는 객체를 만들 순 없다. 따라서 `AbsolutePath` 는 온전히 타입 시스템의 영역이다. 65 | 66 | ```typescript 67 | function f(path: string) { 68 | if (isAbsolutePath(path)) { 69 | listAbsolutePath(path); 70 | } 71 | listAbsolutePath(path); // 'string' 형식의 인수는 'AbsolutePath' 형식의 72 | // 매개변수에 할당될 수 없습니다. 73 | } 74 | ``` 75 | 76 | **단언문을 쓰지 않고 `AbsolutePath` 타입을 얻기 위한 유일한 방법은 `AbsolutePath` 타입을 매개변수로 받거나 타입이 맞는지 체크하는 것뿐이다.** 77 | 78 | ### 타입 시스템 내에서 표현할 수 없는 속성 모델링 79 | 80 | 이진 검색은 이미 정렬된 상태를 가정하기 떄문에, 목록이 이미 정렬되어 있어야한다. **타입스크립트의 타입 시스템에서는 목록이 정렬되어 있다는 의도를 표현하기가 어려운데 상표 기법을 통해서 해결 할 수 있다.** 81 | 82 | ```typescript 83 | type SortedList = T[] & {_brand: 'sorted'}; 84 | 85 | function isSorted(xs: T[]): xs is SortedList { 86 | //... 87 | return false; 88 | //... 89 | return true; 90 | } 91 | 92 | function binarySearch(xs: SortedList, x: T): boolean { 93 | //... 94 | } 95 | ``` 96 | 97 | 이 코드에서 `binarySearch`를 호출하려면, 정렬되었다는 상표가 붙은 `SortedList` 타입의 값을 사용하거나 `isSorted`를 호출하여 정렬되었음을 증명해야 한다. 98 | 99 | ### 결론 100 | 101 | - 타입스크립트는 구조적 타이핑(덕 타이핑)을 사용하기 때문에, 값을 세밀하게 구분하지 못하는 경우가 있다. 값을 구분하기 위해 공식 명칭이 필요하다면 상표를 붙이는 것을 고려해야 한다. 102 | - 상표 기법은 타입 시스템에서 동작하지만 런타임에 상표를 검사하는 것과 동일한 효과를 얻을 수 있다. -------------------------------------------------------------------------------- /ch05_any_다루기/item38_jungho.md: -------------------------------------------------------------------------------- 1 | # 아이템38 any 타입은 가능한 한 좁은 범위에서만 사용하기 2 | 3 | 타입스크립트의 점진적인 특성을 가지는데, 이런 특성에 중요한 역할을 하는 것이 `any`타입이다. 4 | 5 | `any`타입은 프로그램의 일부에만 타입체크를 비활성화 시킬 수 있기 때문에 점진적인 마이그레이션을 진행하는데 도움이 되기 때문이다. 6 | 7 | 하지만 강력한 만큼 남용할 소지가 높기 때문에 장점을 살리고 단점을 줄이는 방법들이 필요하며, 이 아이템에서는 `any` 타입을 좁은 범위에서 사용할 것을 권장한다. 8 | 9 |
10 | 11 | ```tsx 12 | interface Foo { foo: string; } 13 | interface Bar { bar: string; } 14 | declare function expressionReturningFoo(): Foo; 15 | ``` 16 | 17 | 위의 타입 조건이 있고 `x`라는 변수가 있을 때, `x`가 동시에 `Foo` 타입과 `Bar` 타입에 할당 가능하다면, 어떻게 오류를 제거할 수 있을까? 18 | 19 |
20 | 21 | 오류를 제거하는 방법으로 아래와 같이 두 가지 방법을 제시할 수 있다. 22 | 23 | ```tsx 24 | function f1() { 25 | const x: any = expressionReturningFoo(); // 이거보다 26 | processBar(x); 27 | } 28 | 29 | function f2() { 30 | const x = expressionReturningFoo(); 31 | processBar(x as any); // 이게 낫다 32 | } 33 | ``` 34 | 35 | 여기서 두 번째 방법이 권장되는데, 이유는 `any` 타입이 `processBar` 함수의 매개변수에서만 사용되는 표현식이므로 ***다른 코드에는 영향을 주지 않기 때문이다.*** 36 | 37 |
38 | 39 | 위 경우, 40 | 41 | `f1`은 함수가 종료될 때까지 x의 타입이 `any` 로 유지되지만, 42 | 43 | `f2`에서는 `processBar` 호출 이후 x가 그대로 `foo` 타입이 된다. 44 | 45 | 만약 `f1`함수가 `x`를 반환할 경우 문제가 더욱 커진다. 46 | 47 | ```tsx 48 | function f1() { 49 | const x: any = expressionReturningFoo(); 50 | processBar(x); 51 | return x; 52 | } 53 | 54 | function g() { 55 | const foo = f1(); // 타입이 any 56 | foo.fooMethod(); // 메서드가 체크되지 않는다 57 | } 58 | ``` 59 | 60 | `g`함수 내에서 `f1`이 사용되므로, `f1`의 반환 타입이 `foo`의 타입에 영향을 미친다. 61 | 62 | 이처럼 함수에서 `any` 를 반환할 경우 프로젝트 전반에 걸쳐서 영향을 미칠 수 있어 주의해야 한다. 63 | 64 |
65 | 66 | 반면 `any` 의 사용 범위를 좁게 제한하는 `f2`함수의 경우 `any` 타입이 함수 바깥으로 영향을 미치지 않는다. 67 | 68 |
69 | 70 | 추가로 타입스크립트가 함수의 반환 타입을 추론할 수 있는 경우에도 함수의 반환 타입을 명시하는 것이 좋다. 71 | 72 | 함수의 반환 타입을 명시하면 `any` 타입이 함수 바깥으로 영향을 미치는 것을 방지할 수 있기 때문이다. 73 | 74 |
75 | 76 | `any`를 사용하지 않고 타입 오류를 제거하는 방법도 존재한다. 77 | 78 | `@ts-ignore` 를 사용하면 `any` 를 사용하지 않고 오류를 제거할 수 있다. 79 | 80 | 하지만 근본적인 오류를 해결한 것이 아니기 때문에 다른 곳에서 문제가 발생할 수 있다. 81 | 82 | ```tsx 83 | function f1() { 84 | const x = expressionReturningFoo(); 85 | // @ts-ignore 86 | processBar(x); 87 | return x; 88 | } 89 | ``` 90 | 91 |
92 | 93 | 객체와 관련한 `any` 의 사용법 94 | 95 | ```tsx 96 | const config: Config = { 97 | a: 1, 98 | b: 2, 99 | c: { 100 | key: value 101 | // 해당 속성이 타입 오류를 가지는 상황이라 가정 102 | } 103 | }; 104 | ``` 105 | 106 | 단순히 config 객체 전체를 `as any` 로 선언해서 오류를 제거할 수 있지만, 이는 좋지 않은 방법이다. 107 | 108 | 객체 전체를 `any` 로 단언하면, 다른 속성들(a, b)까지 타입 체크가 되지 않는 부작용이 발생한다. 109 | 110 |
111 | 112 | 그러므로 아래처럼 최소한의 범위에만 `any` 를 사용하는 것이 좋다. 113 | 114 | ```tsx 115 | // X 116 | const config: Config = { 117 | a: 1, 118 | b: 2, 119 | c: { 120 | key: value 121 | } 122 | } as any; // 이렇게 하면 a,b의 체크를 놓치게 됨 123 | 124 | // O 125 | const config: Config = { 126 | a: 1, 127 | b: 2, 128 | c: { 129 | key: value as any // 이렇게 하면 a,b는 여전히 체크된다 130 | } 131 | }; 132 | ``` 133 | 134 | ### 요약 135 | 136 | - 의도치 않은 타입 안전성의 손실을 피하기 위해 `any` 의 사용 범위를 최소한으로 좁혀야 한다. 137 | - 함수의 반환 타입이 `any` 인 경우 타입 안정성이 나빠진다. 138 | - 강제로 타입 오류를 제거하려면, `any` 대신 `@ts-ignore` 를 사용하는 것이 좋다. -------------------------------------------------------------------------------- /ch05_any_다루기/item39_호찬.md: -------------------------------------------------------------------------------- 1 | # 아이템 39. any를 구체적으로 변형해서 사용하기 2 | 3 | `any`는 자바스크립트에서 표현할 수 있는 모든 값을 아우르는 매우 큰 범위의 타입이다. 반대로 생각하면, 일반적인 상황에서는 `any`보다 **구체적으로 표현할 수 있는 타입이 존재할 가능성이 높기 떄문에 더 구체적인 타입을 찾아 타입 안전성을 높여야 한다.** 4 | 5 | ```typescript 6 | function getLengthBad(array: any) { // bad 7 | return array.length; 8 | } 9 | 10 | function getLength(array: any[]) { 11 | return array.length; 12 | } 13 | ``` 14 | 15 | `getLength`는 아래 3가지 이점을 갖는다. 16 | 17 | - 함수 내의 array.length 타입이 체크된다. 18 | - 함수의 반환 타입이 any 대신 number로 추론된다. 19 | - 함수 호출될 때 매개변수가 배열인지 체크된다. 20 | 21 | ### 값을 알 수 없는 객체 22 | 23 | 함수의 매개변수가 객체이긴하지만 값을 알 수 없을때는 `{[key: string]: any}`처럼 선언하자. 24 | 25 | ```typescript 26 | function hasTweleveLetterKey(o: {[key: string]: any}) { 27 | for (const key in o) { 28 | if (key.length === 12) { 29 | return true; 30 | } 31 | } 32 | return false; 33 | } 34 | ``` 35 | 36 | 객체의 값을 알 필요가 없다면, 모든 비기본형(non-primitive) 타입을 포함하는 `object` 타입을 사용할 수 있다. **`object` 타입을 객체의 키를 열거할 수 있지만, 속성에 접근할 수 없다는 점에서 `{[key: string]: any}`와 차이가 있다.** 37 | 38 | ```typescript 39 | function hasTweleveLetterKey(o: object) { 40 | for (const key in o) { 41 | if (key.length === 12) { 42 | console.log(key, o[key]); // '{}' 형식에 인덱스 시그니처가 없으므로 요소에 43 | // 요소에 암시적으로 'any' 형식이 있습니다. 44 | } 45 | } 46 | return false; 47 | } 48 | ``` 49 | 50 | ### 함수의 타입 51 | 52 | 함수의 타입에도 `any`를 사용하기보다는 아래처럼 조금이라도 구체화를 시키자. 53 | 54 | ```typescript 55 | type Fn0 = () => any; // 매개변수 없이 호출 가능한 모든 함수 56 | type Fn1 = (arg: any) => any; // 매개변수 1개 57 | type Fn2 = (...args: any[]) => any; // 모든 개수의 매개변수, "Function" 타입과 동일 58 | ``` 59 | 60 | 적어도 `any`보다는 구체적이다. 61 | 62 | ```typescript 63 | const numArgsBad = (...args: any) => args.length; // any를 반환 64 | const numArgsGood = (...args: any[]) => args.length; // number를 반환 65 | ``` 66 | 67 | ### 결론 68 | 69 | - `any`를 사용할 때는 정말로 모든 값이 허용되어야 하는지 면밀히 검토해야한다. 70 | - `any`보다 더 정확히 모델링 할 수 있도록 `any[]`, `{[id: string]: any}` 또는 `() => any` 처럼 구체적인 형태를 사용하자. -------------------------------------------------------------------------------- /ch05_any_다루기/item41_dami.md: -------------------------------------------------------------------------------- 1 | # 아이템 41. any의 진화를 이해하기 2 | 3 | 타입스크립트에서 변수의 타입은 변수를 선언할 때 결정되고, 이후 `null` 값 체크 등을 통해 정제될 수 있으나, 타입을 확장할 수는 없습니다. 4 | 하지만 `any` 타입과 관련해서 예외인 경우가 존재합니다. 5 | 6 | ## 배열에서의 타입 변화 예시 7 | 8 | ```typescript 9 | function range(start: number, limit: number) { 10 | const out = []; // 타입이 any[] 11 | for (let i = start; i < limit; i++) { 12 | out.push(i); // Out의 타입이 any[] 13 | } 14 | return out; // 타입이 number[] 15 | } 16 | ``` 17 | 18 | 위 예제에서는 처음엔 `any` 타입 배열로 초기화 되었으나, 마지막에는 `number[]`로 추론되고 있습니다. 19 | 20 | out의 타입은 `any[]`로 선언되었지만 number타입의 값을 넣는 순간부터 타입은 `number[]`로 진화합니다. 21 | 22 |
23 | 24 | ## 분기에 따른 타입 변화 예시 25 | 26 | ```typescript 27 | let val; // 타입이 any 28 | if (Math.random() < 0.5) { 29 | val = /hello/; 30 | val; // 타입이 RegExp 31 | } else { 32 | val = 12; 33 | val; // 타입이 number 34 | } 35 | val; // 타입이 number | RegExp 36 | ``` 37 | 38 | 위 예제에서는 분기에 따라 타입이 변화합니다. 39 | 40 |
41 | 42 | ## try/catch 블록 내 타입 변화 예시 43 | 44 | ```typescript 45 | function somethingDangerous() {} 46 | let val = null; // 타입이 any 47 | try { 48 | somethingDangerous(); 49 | val = 12; 50 | val; // 타입이 number 51 | } catch (e) { 52 | console.warn("alas!"); 53 | } 54 | val; // 타입이 number | null 55 | ``` 56 | 57 | 변수의 초깃값이 `null`인 경우도 `any`의 진화가 일어나는데, 보통은 `try/catch` 블록 안에서 변수를 할당하는 경우에 나타납니다. 58 | 59 |
60 | 61 | ## any가 유지되는 경우 62 | 63 | `any`는 `noImplictAny`가 설정된 상태에서 변수의 타입이 암시적으로 `any`인 경우에만 일어나지만, 다음처럼 명시적으로 `any`를 선언하면 타입이 그대로 유지됩니다. 64 | 65 | ```typescript 66 | function range(start: number, limit: number) { 67 | const out = []; // 타입이 any[] 68 | for (let i = start; i < limit; i++) { 69 | out.push(i); // 타입이 any[] 70 | } 71 | return out; // 타입이 number[] 72 | } 73 | ``` 74 | 75 |
76 | 77 | ## 암시적 any 오류 78 | 79 | ```typescript 80 | function range(start: number, limit: number) { 81 | const out = []; 82 | // ~~~'out' 변수는 형식을 확인할 수 없는 경우 일부 위치에서 암시적으로 'any[]' 형식입니다. 83 | if (start === limit) { 84 | return out; 85 | // ~~~ 'out' 변수에는 암시적으로 'any[]' 형식이 포함됩니다. 86 | } 87 | for (let i = start; i < limit; i++) { 88 | out.push(i); 89 | } 90 | return out; 91 | } 92 | ``` 93 | 94 | ### 암시적 any 타입은 함수 호출을 거쳐도 진화하지 않는다. 95 | 96 | ```typescript 97 | function makeSquares(start: number, limit: number) { 98 | const out = []; 99 | // 'out' 변수는 일부 위치에서 암시적으로 'any[]' 형식입니다. 100 | range(start, limit).forEach((i) => { 101 | out.push(i * i); 102 | }); 103 | return out; 104 | // ~~~ 'out' 변수에는 암시적으로 'any[]' 형식이 포함됩니다. 105 | } 106 | ``` 107 | 108 | 위 예제에서 `forEach`안의 화살표 함수는 추론에 영향을 미치지 않습니다. 루프를 순회하는 대신 배열의 `map`과 `filter` 메서드를 통해 단일 구문으로 배열을 생성하여 `any` 전체를 진화시키는 방법을 생각해 볼 수 있습니다. 109 | 110 | `any`가 진화하는 방식은 일반적인 변수가 추론되는 원리와 동일합니다. 따라서 암시적으로 any를 진화시키는 것 보다 명시적 타입 구문을 사용하는 것이 더 좋은 설계입니다. 111 | 112 |
113 | 114 | # 결론 115 | 116 | - 암시적 any, any[] 타입은 진화할 수 있다는 걸 알고 작성하자. 117 | - any를 진화시키는 것보다 명시적 타입 구문을 쓰는 것이 좋다. 118 | -------------------------------------------------------------------------------- /ch05_any_다루기/item42_혁주.md: -------------------------------------------------------------------------------- 1 | # 모르는 타입의 값에는 any 대신 unknown을 사용하기 2 | 3 | #### unknown 4 | 5 | 1. 함수의 반환값과 관련된 형태 6 | 2. 변수 선언과 관련된 형태 7 | 3. 단언문과 관련된 형태 8 | 9 | ## 1. 함수의 반환값과 관련된 unknown 10 | 11 | YAML 파서인 parseYAML 함수를 작성한다고 가정해 보겠습니다. 12 | 13 | ```typescript 14 | function parseYAML(yaml: string): any {} 15 | ``` 16 | 17 | 함수의 반환 타입으로 any를 사용하는 것은 좋지 않은 설계입니다. 18 | 대신 parseYAML을 호출한 곳에서 반환값을 원하는 타입으로 할당하는 것이 이상적입니다. 19 | 20 | ```typescript 21 | interface Book { 22 | name: string; 23 | author: string; 24 | } 25 | 26 | const book: Book = parseYAML(` 27 | name: 11st 28 | author: Dcron 29 | `); 30 | ``` 31 | 32 | 그러나 함수의 반환값에 타입 선언을 강제할 수 없기 때문에, **호출한 곳에서 타입 선언을 생략하게 되면** book 변수는 암시적 any 타입이 되고, 사용되는 곳마다 타입 오류가 발생하게 됩니다. 33 | 34 | ```typescript 35 | const book = parseYAML(` 36 | name: 11st 37 | author: Bad 38 | `); 39 | 40 | alert(book.title); // 오류가 발생해야 하는데, 발생하지 않음 -> 런타임에 undefined 경고 41 | book('read'); // 오류가 발생해야 하는데, 발생하지 않음 -> 런타임에 TypeError 발생 42 | ``` 43 | 44 | 대신 parseYAML이 unknown 타입을 반환하게 만드는 것이 더 안전합니다. 45 | 46 | ```typescript 47 | function safeParseYAML(yaml: string): unknown { 48 | return parseYAML(yaml); 49 | } 50 | 51 | const book = safeParseYAML(` 52 | name: The Tenant of Wildfell Hall 53 | author: Anne 54 | `); 55 | 56 | alert(book.title); // ~~ 객체가 unknown 타입입니다. 57 | book('read'); // ~~ 객체가 unknown 타입입니다. 58 | ``` 59 | 60 | unknown을 이해하기 위해 **할당 가능성의 관점**에서 any를 생각해 볼 필요가 있습니다. 61 | 62 | any가 위험한 이유는 무엇일까요? 63 | 64 | 1. 어떠한 타입이든 any 타입에 할당 가능하다. 65 | 2. any 타입은 어떠한 타입으로도 할당 가능하다. (never 타입은 예외) 66 | 67 | 한 집합은 다른 모든 집합의 부분 집합이면서 동시에 상위집합이 될 수 없기 때문에, any는 타입 시스템과 상충되는 면을 가지고 있습니다. 68 | 타입 체커는 집합 기반이기 때문에 any를 사용하면 타입 체커가 무용지물이 되어버립니다. 69 | 70 | `unknown`은 any 대신 쓸 수 있는 타입 시스템에 부합하는 타입입니다. 71 | 72 | - unknown 타입은 어떠한 타입이든 unknown에 할당 가능(any의 1번 속성 만족) 73 | - unknown은 오직 unknown과 any에만 할당 가능(any의 2번 속성 만족 x) 74 | 75 | `never` 타입은 unknown과 정반대입니다. 76 | 77 | - 어떠한 속성도 never에 할당할 수 없음(any의 1번 속성 만족 x) 78 | - 어떠한 타입으로도 할당 가능(any의 2번 속성 만족) 79 | 80 | unknown 타입인 채로 값을 사용하면 오류가 발생합니다. 따라서 적절한 타입으로 변환하도록 강제할 수 있습니다. 81 | 82 | ```typescript 83 | const book = safeParseYAML(` 84 | name: grit 85 | author: flow 86 | `) as Book; 87 | 88 | alert(book.title); // ~~ title이 Book type안에 없다. 89 | book('read'); // ~~ 이 식은 호출할 수 없다. 90 | ``` 91 | 92 | 애초에 반환값이 Book 이라고 기대하며 함수를 호출하기 때문에 단언문은 문제가 되지 않습니다. 93 | Book 타입 기준으로 타입 체크가 되기 때문에, unknown 타입 기준으로 오류를 표시했던 예제보다 오류의 정보가 더 정확해집니다. 94 | 95 | ## 2. 변수 선언과 관련된 unknown 96 | 97 | 어떠한 값이 있지만 그 타입을 모르는 경우에 unknown을 사용합니다. 98 | 99 | ```typescript 100 | interface Feature { 101 | id?: string | number; 102 | geometry: Geometry; 103 | properties: unknown; // 잡동사니 주머니 느낌 104 | } 105 | ``` 106 | 107 | unknown을 원하는 타입으로 변경하는 방법에는 타입 단언문 말고도 다른 방법이 있습니다. 108 | instanceof를 체크한 후 unknown에서 원하는 타입으로 변환할 수 있습니다. 109 | 110 | ```typescript 111 | function processValue(val: unknown) { 112 | if (val instanceof Date) { 113 | val; // type이 Date 114 | } 115 | } 116 | ``` 117 | 118 | 타입 가드를 사용할 수도 있습니다. 119 | 120 | ```typescript 121 | function isBook(val: unknown): val is Book { 122 | return ( 123 | typeof val === 'object' && val !== null && 'name' in val && 'author' in val // null === 'object'임 주의 124 | ); 125 | } 126 | 127 | function processValue(val: unknown) { 128 | if (isBook(val)) { 129 | val; // type 이 Book 130 | } 131 | } 132 | ``` 133 | 134 | 가끔 unknown 대신 제네릭 매개변수가 사용되는 경우도 있습니다. 제너릭을 사용하기 위해 다음 코드처럼 safeParseYAML 함수를 선언할 수 있습니다. 135 | 136 | ```typescript 137 | function safeParseYAML(yaml: string): T { 138 | return parseYAML(yaml); 139 | } 140 | ``` 141 | 142 | 그러나 일반적으로 타입스크립트에서 좋지 않은 스타일입니다. 143 | 제너릭을 사용한 스타일은 타입 단언문과 생김새는 달라보이지만 기능적으로는 동일합니다. 144 | 제너릭보다 unknown을 반환하고 사용자가 직접 단언문을 사용하거나 원하는대로 타입을 좁히도록 강제하는 것이 좋습니다. 145 | 146 | ## 3. 단언문과 관련된 unknown 147 | 148 | 이중 단언문에서 any 대신 unknown을 사용할 수도 있습니다. 149 | 150 | ```typescript 151 | declare const foo: Foo; 152 | let barAny = foo as any as Bar; 153 | let barUnk = foo as unknown as Bar; 154 | ``` 155 | 156 | 기능적으로는 동일하지만, 나중에 두 개의 단언문을 분리하는 리팩터링을 한다면 unknown 형태가 더 안전합니다. 157 | any의 경우 분리되는 순간 그 영향력이 전염병처럼 퍼지게 됩니다. 158 | unknown의 경우 분리되는 즉시 오류가 발생하여 더 안전합니다. 159 | 160 | ### unknown과 유사한 타입들 161 | 162 | `object` 또는 `{}`를 사용하는 코드들도 존재합니다. 163 | 164 | - `{}` 타입은 `null`과 `undefined`를 제외한 모든 값을 포함합니다. 165 | - `object` 타입은 모든 비기본형(`non-primitive`) 타입으로 이루어집니다. 여기에는 `true` 또는 `12` 또는 `'foo'`가 포함되지 않지만 `객체`, `배열`은 포함됩니다. 166 | 167 | `unknown` 타입이 도입되기 이전에는 `{}`가 일반적으로 많이 쓰였지만, 최근에는 `{}`를 사용하는 경우가 드뭅니다. 168 | 정말로 `null`과 `undefined`가 불가능하다고 판단되는 경우에만 `unknown` 대신 `{}`를 사용하면 됩니다. 169 | 170 | ## 요약 171 | 172 | - `unknown` 타입은 `any` 대신 사용할 수 있는 안전한 타입입니다. 어떠한 값이 있지만 그 타입을 알지 못하는 경우 사용합니다. 173 | 사용자가 타입 단언문이나 타입 체크를 사용하도록 강제하려면 `unknown`을 사용하면 됩니다. 174 | - `{}`, `object`, `unknown`의 차이점을 이해해야 합니다. 175 | -------------------------------------------------------------------------------- /ch05_any_다루기/item43_chanu.md: -------------------------------------------------------------------------------- 1 | # 몽키패치보다는 안전한 타입을 사용하기 2 | 3 | > JS는 객체와 클래스에 임의의 속성을 추가할 수 있을 만큼 유연하다는 것입니다. 4 | 5 | JS에서 사용하는 전역객체인 `window`와 `document`에 속성을 추가하여 전역에서 사용하는 방식으로 구현하기도 한다. 이렇게 런타임 시점에 클래스가 변경되는 것을 몽키패치라고 한다. 6 | 7 | ```typescript 8 | window.monkey = 'Tamarin'; 9 | document.monkey = 'Howler'; 10 | ``` 11 | 12 |
13 | 14 | > 심지어 내장된 객체의 프로토타입에도 속성을 추가할 수 있습니다. 15 | 16 | ```typescript 17 | RegExp.prototype.monkey = 'Capuchin'; 18 | 19 | console.log(/123/.monkey); 20 | ``` 21 | - `/123/`의 속성값을 사용하기 위해 wrapper객체가 생성되고, `RegExp`클래스의 프로토타입을 거쳐 `monkey` 속성값을 반환하게 된다. 22 | - 전역객체, 또는 내장객체의 프로토타입에 속성을 추가할 수 있으나, 이러한 경우 반드시 의도하지 않은 side effect가 발생할 수 있으므로 주의해야한다. 23 | 24 |
25 | 26 | > TS에서 문제는 추가된 속성에 대해서는 타입체커가 알지못한다는 것입니다. 27 | 28 | ```typescript 29 | window.monkey = 'Tamarin'; // 'Window' 타입에 `monkey` 속성이 없습니다. 30 | ``` 31 | 32 | - any 단언문을 사용할 수 있으나, 이 것은 매우 좋지 않은 방법. 33 | 34 |
35 | 36 | #### 1. 보강 37 | 38 | ```typescript 39 | interface Document { 40 | monkey: string; 41 | } 42 | 43 | document.monkey = 'Tamarin'; 44 | ``` 45 | 46 | - 기존에 구현되어있는 전역객체의 타입인 `Document` 인터페이스에 보강기법을 통해 `monkey` 타입을 선언할 수 있다. 47 | - 모듈 관점에서 전역으로 사용되기 위해서는 `declare global`을 추가해야한다. 48 | - 보강은 전역으로 적용되기 때문에 `document`객체를 사용하는 모든 영역에 적용되는 문제점이 존재한다. 전역객체를 사용하되, 우리가 원하는 영역에 대해서만 적용할 수 없을까? 49 | 50 |
51 | 52 | #### 2. 구체적인 타입 단언문 53 | 54 | ```typescript 55 | interface MonkeyDocument extends Document { 56 | monkey: string; 57 | } 58 | 59 | (document as MonkeyDocument).monkey = 'Macaque'; 60 | ``` 61 | 62 | - `Document` 타입을 건드리지 않고 확장하여 사용할 수 있다. 63 | - 단언문을 사용하므로, 전역객체를 사용하되 모듈단위의 적용이 가능하다. 64 | 65 |
66 | 67 | ### 정리하자면, 68 | 69 | #### 1. 전역객체를 사용하지 않는 것이 좋다. 70 | 71 | #### 2. 써야만 한다면, 구체적인 타입 단언문을 사용하자. -------------------------------------------------------------------------------- /ch05_any_다루기/item44_혁주.md: -------------------------------------------------------------------------------- 1 | # 타입 커버리지를 추적하여 타입 안전성 유지하기 2 | 3 | `noImplicitAny`를 설정하고 모든 암시적 any대신 명시적 타입 구문을 추가해도 any 타입과 관련된 문제들로부터 안전하다고 할 수 없습니다. 4 | 5 | any 타입이 여전히 프로그램 내에 존재할 수 있는 두 가지 경우가 있습니다. 6 | 7 | 1. 명시적 any 타입 8 | 9 | - any 타입의 범위를 좁히고 구체적으로 만들어도 여전히 any 타입입니다. 10 | 11 | 특히 any[]와 {[key: string]: any}같은 타입은 인덱스를 생성하면 단순 any 타입이 되고 코드 전반에 영향을 미칩니다. 12 | 13 | 2. 서드파티 타입 선언 14 | 15 | - 이 경우는 @types 선언 파일로부터 any 타입이 전파되기 때문에 특별히 조심해야 합니다. 16 | 17 | `noImplicitAny`를 설정하고 절대 any를 사용하지 않았다 하더라도 여전히 any 타입은 코드 전반에 영향을 미칩니다. 18 | 19 | any 타입은 타입 안전성과 생산성에 부정적인 영향을 미칠 수 있으므로 프로젝트에서 any의 개수를 추적하는 것이 좋습니다. 20 | 21 | npm의 type-coverage 패키지를 활용해서 any를 추적할 수 있습니다. 22 | 23 | (참고: https://www.npmjs.com/package/type-coverage) 24 | 25 | ## any 가 등장하는 몇 가지 문제와 그 해결책 26 | 27 | 1. 함수의 반환으로 any가 남아 있는 경우 28 | 29 | 표 형태의 데이터에서 어떤 종류의 열(column) 정보를 반환하는 함수를 만든다고 가정해봅시다. 30 | 31 | ```typescript 32 | function getColumnInfo(name: string): any { 33 | return utils.buildColumnInfo(appState.dataSchema, name); // any를 반환합니다. 34 | } 35 | ``` 36 | 37 | `utils.buildColumnInfo` 호출은 any를 반환합니다. 따라서 `getColumnInfo` 함수의 반환에는 주석과 함께 명시적으로 :any 구문을 추가했습니다. 38 | 39 | 이후에 타입 정보를 추가하기 위해 `ColumnInfo` 타입을 정의하고 `utils.buildColumnInfo`가 `any` 대신 `ColumnInfo`를 반환하도록 개선해도 `getColumnInfo` 함수의 반환문에 있는 `any` 타입이 모든 타입 정보를 날려 버리게 됩니다. 40 | 41 | > `getColumnInfo`에 남아 있는 `any`까지 제거해야 문제가 해결됩니다. 42 | 43 | 2. 서드파티 라이브러리로부터 비롯되는 any 44 | 45 | #### 2-1. 전체 모듈에 any 타입을 부여하는 경우(극단적) 46 | 47 | ```typescript 48 | declare module 'my-module'; 49 | ``` 50 | 51 | 앞의 선언으로 인해 my-module에서 어떤 것이든 오류 없이 임포트할 수 있습니다. 임포트안 모든 심벌은 any 타입이고, 임포트한 값이 사용되는 곳마다 any 타입을 양산하게 됩니다. 52 | 53 | ```typescript 54 | import { someMethod, someSymbol } from 'my-module'; 55 | const pt1 = { 56 | x: 1, 57 | y: 2, 58 | }; 59 | const pt2 = someMethod(pt1, someSymbol); // type이 any 60 | ``` 61 | 62 | 일반적인 모듈의 사용법과 동일하기 때문에, 타입 정보가 모두 제거됐다는 것을 간과할 수 있습니다. 63 | 64 | > 가끔 모듈들을 점검해야 합니다. 65 | 66 | image 67 | 68 | 69 | #### 2-2. 타입에 버그가 있는 경우 70 | 예를 들어 아이템 29의 조언(값을 생성할 때는 엄격하게 타입을 적용하라)를 무시한 채로, 함수가 유니온 타입을 반환하도록 선언하고 실제로는 유니온 타입보다 훨씬 더 특정된 값을 반환하는 경우입니다. 71 | 72 | 선언된 타입과 실제 반환된 타입이 맞지 않는다면 어쩔 수 없이 any 단언문을 사용해야 합니다. 73 | >그러나 나중에 라이브러리가 업데이트되어 함수의 선언문이 제대로 수정된다면 any를 제거해야 합니다. 74 | 75 | 76 | 3. 기타 경우 77 | - any 타입이 사용되는 코드가 실제로는 더 이상 실행되지 않는 코드일 수 있습니다. 78 | - 어쩔 수 없이 any를 사용했던 부분이 개선되어 제대로 된 타입으로 바뀌었다면 any가 더 이상 필요 없을 수 있습니다. 79 | - 버그가 있는 타입 선언문이 업데이트되어 제대로 타입 정보를 가질 수도 있습니다. 80 | > 타입 커버리지를 추적하면 이러한 부분들을 쉽게 발견할 수 있기 때문에 코드를 꾸준히 점검할 수 있게 해줍니다. 81 | 82 | ## 요약 83 | - `noImplicitAny`가 설정되어 있어도, 명시적 any 또는 서드파티 타입 선언(@types)을 통해 any 타입은 코드 내에 여전히 존재할 수 있음을 주의해야 합니다. 84 | - 작성한 프로그램의 타입이 얼마나 잘 선언되었는지 추적해야 합니다. 추적함으로써 any의 사용을 줄여 나갈 수 있고 타입 안전성을 꾸준히 높일 수 있습니다. -------------------------------------------------------------------------------- /ch06_타입_선언과_@types/item45_호찬.md: -------------------------------------------------------------------------------- 1 | # 아이템 45. devDependencies에 typescript와 @types 추가하기 2 | 3 | ### package.json 4 | 5 | - `dependencies` 6 | - : 프로젝트를 npm에 공개하여 다른 사용자가 해당 프로젝트를 설치한다면, `dependencies`의 라이브러리가 같이 설치 7 | - `devDependencies` 8 | - : 프로젝트를 npm에 공개하여 다른 사용자가 해당 프로젝트를 설치한다면, `devDependencies`의 라이브러리는 제외하고 설치 9 | - `peerDependencies` 10 | - : 런타임에 필요하긴 하지만, 의존성을 직접 관리하지 않는 라이브러리만 포함 11 | 12 | 타입스크립트 프로젝트에서 공통적으로 고려해야 할 사항 두가지는 아래와 같다. 13 | 14 | #### 타입스크립트 자체 의존성을 고려해야 한다. 15 | 16 | 팀원들 모두가 항상 동일한 버전을 설치한다는 보장이 있어야 한다. 따라서 `typescript`를 시스템 레밸에서 사용하는 것보다는 `devDependencies`에 넣는 것이 적합하다. 또, `npx`를 사용해서 `devDependencies`를 통해 설치된 타입스크립트 컴파일러를 실행할 수 있다. 17 | 18 | ```bash 19 | npx tsc 20 | ``` 21 | 22 | #### 타입 의존성(@types)을 고려해야 한다. 23 | 24 | 라이브러리에 타입 선언이 포함되엉 있지 않더라도, DefinitelyTyped에서 타입 정보를 얻을 수 있다. 25 | 26 | ```bash 27 | npm install react 28 | npm install --save-dev @types/react 29 | ``` 30 | 31 | `package.json` 32 | 33 | ```json 34 | { 35 | "devDependencies": { 36 | "@types/react": "^16.8.19", 37 | "typescript": "^3.5.3", 38 | }, 39 | "dependencies": { 40 | "react": "^16.8.6", 41 | } 42 | } 43 | ``` 44 | 45 | ### 요약 46 | 47 | - 타입스크립트를 시스템 레밸로 설치하는 것보다, 프로젝트의 devDependencies에 포함시키고 팀원 모두가 동일한 버전을 사용하도록 하자. 48 | - @types 의존성은 devDependencies에 포함시켜야한다. 런타입에 @types가 필요한 경우라면 별도의 작업이 필요할 수 있다. 49 | -------------------------------------------------------------------------------- /ch06_타입_선언과_@types/item46_dami.md: -------------------------------------------------------------------------------- 1 | # 아이템 46. 타입 선언과 관련된 세 가지 버전 이해하기 2 | 3 | 타입스크립트를 사용하면 아래 세가지 사항을 고려해야 하기에 의존성 관리를 더 복잡하게 만듭니다. 4 | 5 | - 라이브러리의 버전 6 | - 타입 선언(@types)의 버전 7 | - 타입스크립트의 버전 8 | 9 | 세 가지 버전 중 하나라도 맞지 않으면, 의존성과 상관없어 보이는 곳에서 오류가 발생할 수 있습니다. 10 | 타입스크립트에서 의존성을 사용하기 위해 특정 라이브러리를 dependencies로 설치하고, 타입 정보는 devDependencies로 설치합니다. (아이템 45) 11 | 12 | ```typescript 13 | $ npm install react 14 | + react@16.8.6 15 | 16 | $ npm install --save-dev @types/react 17 | + @types/react@16.8.19 18 | ``` 19 | 20 | 위 예제에서는 메이저 버전과 마이너 버전(16.8)이 일치하지만 패치 버전(.6 vs .19)은 일치하지 않습니다. 21 | `@types/react@16.8.19` 는 타입 선언들이 리액트 16.8 버전의 API를 나타낸다는 것을 의미합니다. 22 | 23 | 리액트 모듈이 시맨틱(semantic) 버전 규칙을 제대로 지킨다고 했을때, 패치 버전들(16.8.1, 16.8.2...)의 경우 공개 API의 사양을 변경하지 않기에 타입 선언을 업데이트할 필요가 없습니다. 그러나 타입 선언 자체도 버그, 누락이 존재할 수 있습니다. 24 | 25 | > MAJPR.MINOR.PATCH 26 | > 27 | > - MAJOR: API 호환성이 깨질만한 변경사항 28 | > 29 | > - MINOR: 하위 호환성 지키면서 API 기능이 추가된 것 30 | > 31 | > - PATCH: 하위 호환성 지키는 범위 내에서 버그가 수정된 것 32 | 33 | @types 모듈의 패치 버전은 버그나 누락으로 인한 수정, 추가에 따른 것입니다. 34 | 35 | ## 타입 정보 버전이 별도로 관리되는 것의 4가지 문제점 36 | 37 | ### 1. 라이브러리는 업데이트했지만 타입 선언은 업데이트 하지 않은 경우 38 | 39 | - 라이브러리 업데이트와 관련된 새로운 기능을 사용하려 할 때마다 타입 오류가 발생합니다. 40 | - 하위 호환성이 깨지는 변경이 있다면, 타입 체커는 통과해도 런타임에 오류가 발생할 수 있습니다. 41 | 42 | #### 해결책 43 | 44 | 1. 타입 선언도 라이브러리 버전에 맞게 업데이트 한다. 45 | 2. 사용하려는 새 함수와 메서드의 타입 정보를 프로젝트 자체에 추가한다. 46 | 3. 타입 선언 업데이트를 직접 작성해서 커뮤니티에 기여한다. 47 | 48 | ### 2. 라이브러리보다 타입 선언의 버전이 최신인 경우 49 | 50 | 보통 타입 없이 라이브러리를 상용하다가 타입 선언을 설치하려고 할 때 발생합니다. 51 | 52 | 타입 체커는 최신 API를 기준으로 코드를 검사하게 되지만 런타임에 실제로 쓰이는 것은 과거 버전입니다. 53 | 54 | #### 해결책 55 | 56 | 라이브러리와 타입 선언 버전이 맞도록 57 | 58 | 1. 라이브러리 버전을 올린다. 59 | 2. 타입 버전을 내린다. 60 | 61 | ### 3. 프로젝트 타입스크립트 버전보다 라이브러리 타입스크립트 버전이 최신인 경우 62 | 63 | 현재 프로젝트보다 라이브러리에게 필요한 타입스크립트 버전이 높은 상황이라면, @types 선언 자체에서 타입오류가 발생하게 됩니다. 64 | 65 | #### 해결책 66 | 67 | 1. 라이브러리 타입 선언의 버전을 원래대로 내린다. 68 | 2. declare module을 선언해서 라이브러리의 타입 정보를 없앤다 69 | 70 | ### 4. @types 의존성이 중복되는 경우 71 | 72 | @types/foo이 @types/bar에 의존한다면, npm은 중첩된 폴더에 별도로 해당 버전을 설치해 문제를 해결하려 할 것입니다. 73 | 74 | ``` 75 | node_modules/ 76 | @types/ 77 | foo/ 78 | index.d.ts @1.2.3 79 | bar/ 80 | index.d.ts 81 | node_modules/ 82 | @types/ 83 | foo/ 84 | index.d.ts @2.3.4 85 | ``` 86 | 87 | 런타입에 사용되는 모듈이 아니라 전역 네임스페이스(name-space)에 있는 타입 선언 모듈이라면 중복된 선언, 선언이 병합될 수 없다는 오류로 나타나게 됩니다. 88 | 89 | #### 해결책 90 | 91 | npm ls @types/foo 를 실행해서 어디서 타입 선언이 중복으로 발생했는지 추적한 뒤 92 | 93 | 1. @types/foo를 업데이트 하거나, @types/bar를 업데이트해서 서로 호환되게 한다. 94 | 95 | ## 번들된 타입이 문제가 되는 상황 96 | 97 | ### 1. 번들된 타입에서는 @types 버전 선택이 불가능 98 | 99 | ### 2. 프로젝트 내 타입 선언이 다른 라이브러리의 타입 선언에 의존한 경우 100 | 101 | - 작성자가 의존성을 devDpendencies로 한 경우 다른 사용자가 설치했을 때 devDpendencies 내용은 설치 되지가 않아 타입 오류가 발생하게 됩니다. 102 | 103 | ### 3. 프로젝트의 과거 버전에 있는 타입 선언에 문제가 있는 경우 104 | 105 | - 이 경우엔 과거 버전으로 돌아가서 패치 업데이트를 해야합니다. 106 | 107 | ### 4. 타입 선언의 패치 업데이트를 자주하기 어렵다. 108 | 109 | - 라이브러리 자체보다 타입 선언에 대한 패치 업데이트가 세 배나 더 많았고, 이 패치 업데이트를 자주하기 어렵습니다. 110 | 111 | ## 결론 112 | 113 | - @types 의존성과 관련된 3가지 버전이 있다. 114 | - 라이브러리 버전 115 | - @types 버전 116 | - 타입스크립트 버전 117 | - 라이브러리를 업데이트 하는 경우 해당 @types 역시 업데이트 해야한다. 118 | - 타입스크립트로 작성된 라이브러리 -> 타입 선언을 자체적으로 포함하자. 119 | - 자바스크립트로 작성된 라이브러리 -> 타입 선언을 DefinitelyTyped에 공개하자. 120 | -------------------------------------------------------------------------------- /ch06_타입_선언과_@types/item48/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/11st-corp/effective-typescript/21099a52aa554a17a888ca5bde4b3c90d9fb0a1c/ch06_타입_선언과_@types/item48/img.png -------------------------------------------------------------------------------- /ch06_타입_선언과_@types/item48/img_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/11st-corp/effective-typescript/21099a52aa554a17a888ca5bde4b3c90d9fb0a1c/ch06_타입_선언과_@types/item48/img_1.png -------------------------------------------------------------------------------- /ch06_타입_선언과_@types/item48_chanu.md: -------------------------------------------------------------------------------- 1 | # API 주석에 TSDoc 사용하기 2 | 3 | ```typescript 4 | // 이 함수는 완전 개꿀입니다. 5 | function gaekkul(): string { 6 | return 'wow'; 7 | } 8 | 9 | 10 | /* 이 함수는 완전 개꿀입니다. */ 11 | function gaekkul2(): string { 12 | return 'wow'; 13 | } 14 | ``` 15 | - 인라인 형태의 주석이나 JSDoc 스타일 주석 모두 커서를 통해 편집기의 툴팁에 의해 보여진다 16 | 17 |
18 | 19 | #### 함수의 경우, JSDoc의 스타일과 같이 `@param`, `@returns`의 데코레이터를 활용하여 인자와 반환값에 대한 가이드를 작성할 수 있다. 20 | 21 | ```typescript 22 | /** 23 | * 인사말을 생성한다. 24 | * @param name 이름 25 | * @param title 그사람의 칭호 26 | * @returns 인사말 27 | */ 28 | function greetFullTSDoc(name: string, title: string) { 29 | return `Hello ${title} ${name}`; 30 | } 31 | ``` 32 | 33 | - 함수 위에서 `/**`만 작성하고 `Enter`를 누르면 intellij가 자동으로 인자와 반환값에 대한 JSDoc을 생성하여 준다. 34 | 35 |
36 | 37 | #### 타입을 정의할 때에도 각 속성에 대해서 주석을 입력할 수 있다. 38 | 39 | ```typescript 40 | interface Measure { 41 | /* 어디서 측정 되었는가? */ 42 | position: string; 43 | time: number; 44 | /* 측정된 운동량 */ 45 | momentum: string; 46 | } 47 | 48 | const m: Measure = { 49 | position: 'home', 50 | time: 2022, 51 | momentum: 'heavy' 52 | } 53 | ``` 54 | 55 | - 아래와 같이 각 속성에 대한 주석이 편집기의 툴팁에 의해 보여진다. 56 | 57 | 58 | 59 | 60 |
61 |
62 | 63 | #### markdown 형식을 사용하므로 문법을 그대로 사용할 수 있다. 64 | 65 | ```typescript 66 | /** 67 | * 이 *인터페이스*는 **세가지 속성**을 가집니다. 68 | * 1. x 69 | * 2. y 70 | * 3. z 71 | */ 72 | interface Vector3D { 73 | x: number; 74 | y: number; 75 | z: number; 76 | } 77 | ``` 78 | 79 | 80 | 81 |
82 |
83 | 84 | 85 | 86 | ### 정리하자면, 87 | 88 | #### 1. 함수, 클래스, 타입에 대해서 주석을 달 때에는 TSDoc 형식을 사용하여 편집기의 툴팁에 의해 주석이 보여지도록 하자. 89 | 90 | #### 2. 타입에 대한 명시는 코드로 제공되기 때문에 주석에는 타입에 대한 정보는 명시해서는 안된다. 91 | 92 | -------------------------------------------------------------------------------- /ch06_타입_선언과_@types/item49_혁주.md: -------------------------------------------------------------------------------- 1 | # 콜백에서 this에 대한 타입 제공하기 2 | 3 | 일반적인 let, const는 어디서 선언했는지에 따라 상위 스코프를 결정합니다. 이것을 렉시컬 스코프라고 합니다. 4 | 5 | 그러나 자바스크립트에서 this는 다이나믹 스코프를 따릅니다. 이는 함수가 호출된 형태에 따라 상위 스코프가 정해지는 것을 의미합니다. 6 | 7 | ### this가 사용되는 예시 8 | 9 | ```javascript 10 | class C { 11 | vals = [1, 2, 3]; 12 | logSquares() { 13 | for (const val of this.vals) { 14 | console.log(val * val); 15 | } 16 | } 17 | } 18 | 19 | const c = new C(); 20 | c.logSquares(); 21 | ``` 22 | 23 | 코드를 실행하면 다음과 같은 값이 출력됩니다. 24 | 25 | ```js 26 | 1; 27 | 4; 28 | 9; 29 | ``` 30 | 31 | 이제 앞선 예제를 살짝 변경하여 logSquare를 외부에 넣고 호출하면 어떻게 될까요? 32 | 33 | ```javascript 34 | class C { 35 | vals = [1, 2, 3]; 36 | logSquares() { 37 | for (const val of this.vals) { 38 | console.log(val * val); 39 | } 40 | } 41 | } 42 | 43 | const c = new C(); 44 | const method = c.logSquares; 45 | method(); // ~~ TypeError: Cannot read property 'vals' of undefined 46 | ``` 47 | 48 | c.logSqaures()가 실제로는 두 가지 작업을 수행합니다. 49 | 50 | 1. C.prototype.logSquares를 호출 51 | 2. this의 값을 c로 바인딩 52 | 53 | 앞의 코드에서는 logSquares의 참조 변수를 사용함으로써 두 가지 작업을 분리했고, 2번 작업이 없어졌기 때문에 this의 값이 undefined가 됩니다. 54 | 55 | this 바인딩을 어떻게 제어할 수 있을까요? 56 | `call` (`apply`, `bind`)을 사용하면 됩니다. 57 | 58 | ```javascript 59 | const c = new C(); 60 | const method = c.logSquares; 61 | method.call(c); // 1, 4, 9 정상 출력 62 | ``` 63 | 64 | (call은 method 함수를 호출하면서 첫 번째 인수로 전달한 c를 호출함 함수의 this에 바인딩합니다.) 65 | 66 | this가 반드시 C의 인스턴스에 바인딩 되어야 하는 것은 아니며, 어느 것이든 심지어 DOM에서도 this를 바인딩할 수 있습니다. 67 | 68 | ### 이벤트 핸들러의 this 69 | 70 | 예를 들어보겠습니다. 71 | 72 | ```javascript 73 | document.querySelector('input')!.addEventListener('change',function(e){ 74 | console.log(this); // 이벤트가 발생한 input 엘리먼트를 출력합니다. 75 | }) 76 | ``` 77 | 78 | ### 콜백 함수의 this 79 | 80 | this 바인딩은 종종 콜백 함수에서 쓰입니다. 예를 들어, 클래스 내에 onClick 핸들러를 정의한다면 81 | 82 | ```javascript 83 | class ResetButton { 84 | render() { 85 | return makeButton({ text: 'Reset', onClick: this.onClick }); 86 | } 87 | onClick() { 88 | alert(`Reset ${this}`); 89 | } 90 | } 91 | ``` 92 | 93 | 그러나 ResetButton에서 onClick을 호출하면, this 바인딩 문제로 인해 "Reset이 정의되지 않았습니다."라는 경고가 뜹니다. 94 | 95 | 일반적으로 생성자에서 메서드에 this를 바인딩시켜 해결합니다. 96 | 97 | ```javascript 98 | class ResetButton { 99 | constructor() { 100 | this.onClick = this.onClick.bind(this); 101 | } 102 | render() { 103 | return makeButton({ text: 'Reset', onClick: this.onClick }); 104 | } 105 | onClick() { 106 | alert(`Reset ${this}`); 107 | } 108 | } 109 | ``` 110 | 111 | `onclick(){}`은 `ResetButton.prototype`의 속성을 정의합니다. 그러므로 ResetButton의 모든 인스턴스에서 공유됩니다. 112 | 113 | 그러나 생성자에서 위와 같이 바인딩하면 onClick 속성에 this가 바인딩되어 해당 인스턴스에 생성됩니다. 114 | 115 | 속성 탐색 순서에서 onClick 인스턴스 속성은 onClick 프로토타입 앞에 놓이므로, render() 메서드의 `this.onClick`은 바인딩된 함수를 참조하게 됩니다. 116 | 117 | 더 간단한 방법을 사용해보겠습니다. 118 | 119 | ```javascript 120 | class ResetButton { 121 | render() { 122 | return makeButton({ text: 'Reset', onClick: this.onClick }); 123 | } 124 | onClick = () => { 125 | alert(`Reset ${this}`); 126 | }; 127 | } 128 | ``` 129 | 130 | onClick을 화살표함수로 바꿨습니다. 이렇게 되면 ResetButton이 생성될 때마다 제대로 바인딩된 this를 가지는 새 함수를 생성하게 됩니다. 131 | 132 | 왜냐하면 화살표 함수 내부의 this는 상위 스코프의 this를 가리키기 때문입니다. 133 | 134 | (화살표 함수는 함수 자체의 this 바인딩을 갖지 않아서 스코프 체인 상에서 가장 가까운 상위 스코프의 this를 참조) 135 | 136 | 자바스크립트가 실제로 생성한 코드는 다음과 같습니다. 137 | 138 | ```javascript 139 | class ResetButton { 140 | constructor() { 141 | var _this = this; 142 | this.onClick = function () { 143 | alert('Reset' + _this); 144 | }; 145 | } 146 | render() { 147 | return makeButton({ text: 'Reset', onClick: this.onClick }); 148 | } 149 | } 150 | ``` 151 | 152 | ### this를 사용하는 콜백 함수 153 | 154 | this 바인딩은 자바스크립트의 동작이기 때문에, 타입스크립트 역시 this 바인딩을 그대로 모델링합니다. 155 | 156 | 만약 작성중인 라이브러리에 this를 사용하는 콜백 함수가 있다면, this 바인딩 문제를 고려해야 합니다. 157 | 158 | 이 문제는 콜백 함수의 매개변수에 this를 추가하고, 콜백 함수를 call로 호출해서 해결할 수 있습니다. 159 | 160 | ```typescript 161 | function addkeyListener( 162 | el: HTMLElement, 163 | fn: (this: HTMLElement, e: KeyboardEvent) => void 164 | ) { 165 | el.addEventListener('keydown', (e) => { 166 | fn.call(el, e); 167 | }); 168 | } 169 | ``` 170 | 171 | 콜백 함수의 매개변수에 this를 추가하면 this 바인딩이 체크되기 때문에 실수를 방지할 수 있습니다. 172 | 173 | ```typescript 174 | function addkeyListener( 175 | el: HTMLElement, 176 | fn: (this: HTMLElement, e: KeyboardEvent) => void 177 | ) { 178 | el.addEventListener('keydown', (e) => { 179 | fn(e); // ~~ 'void'형식의 'this' context를 180 | // 메서드의 'HTMLElement' 형식 'this'에 할당 할 수 없습니다. 181 | }); 182 | } 183 | ``` 184 | 185 | 또한 라이브러리 사용자의 콜백 함수에서 this를 참조할 수 있고 완전한 타입 안전성도 얻을 수 있습니다. 186 | 187 | ```typescript 188 | declare let el: HTMLElement; 189 | addkeyListener(el, function (e) { 190 | this.innerHTML; // 정상, 'this'는 HTMLElement 타입 191 | }); 192 | ``` 193 | 194 | 만약 라이브러라 사용자가 콜백을 화살표 함수로 작성하고 this를 참조하려고 하면 타입스크립트가 문제를 잡아냅니다. 195 | 196 | ```typescript 197 | class Foo { 198 | registerHandler(el: HTMLElement) { 199 | addkeyListener(el, (e) => { 200 | this.innerHTML; // ~~ 'Foo'유형에 'innerHTML' 속성이 없습니다. 201 | }); 202 | } 203 | } 204 | ``` 205 | 206 | this의 사용법을 반드시 기억해야 합니다. 콜백 함수에서 this 값을 사용해야 한다면 this는 API의 일부가 되는 것이기 때문에 반드시 타입 선언에 포함해야 합니다. 207 | 208 | ## 요약 209 | 210 | - this 바인딩이 동작하는 원리를 이해해야 합니다. 211 | - 콜백 함수에서 this를 사용한다면, 타입 정보를 명시해야 합니다. 212 | -------------------------------------------------------------------------------- /ch06_타입_선언과_@types/item50_jungho.md: -------------------------------------------------------------------------------- 1 | # 아이템50 오버로딩 타입보다는 조건부 타입을 사용하기 2 | 3 | ```tsx 4 | function double(x) { 5 | return x + x; 6 | } 7 | ``` 8 | 9 | 위의 double 함수에는 number 또는 string이 올 수 있으며 함수 return 값도 number 나 string을 기대할 수 있다. 10 | 11 | 이 double 함수에 타입 정보를 추가하고자 한다면, 아래와 같은 방법들을 떠올릴 수 있다. 12 | 13 | ```tsx 14 | /** 15 | * 유니온 타입 16 | */ 17 | function double(x: number|string): number|string 18 | // param: number => return: string이 되는 경우도 포함되어 버림 19 | 20 | /** 21 | * 제네릭 22 | */ 23 | function double(x: T): T; 24 | // 리터럴 문자열 'x'가 param으로 들어오면, return타입이 'x'가 되어 버림 25 | 26 | /** 27 | * 오버로딩 28 | */ 29 | function double(x: number): number 30 | function double(x: string): string; 31 | // param으로 유니온 타입이 오는 경우 타입 오류가 발생 32 | // string|number 타입을 추가로 오버로딩하면 되지만, 좋지 않은 방법 33 | ``` 34 | 35 | 그렇다면 가장 좋은 해결 방법은 무엇일까? 36 | 37 | 조건부 타입(conditional type)을 사용하는 것이다. 38 | 39 | ```tsx 40 | function double( 41 | x: T 42 | ): T extends string ? string : number; 43 | 44 | function double(x: any) { return x + x; } 45 | ``` 46 | 47 | 조건부 타입은 자바스크립트의 삼항 연산자(`? :`) 문법과 같이 사용하면 되므로 받아들이기 쉽다. 48 | 49 | 이렇게 타입을 작성하게 되면 앞선 모든 예제의 문제점을 보완할 수 있다. 50 | 51 | 오버로딩 타입이 작성하기는 쉽지만, 조건부 타입이 타입을 더 정확히 하는데 도움을 줄 수 있다. 52 | 53 | 오버로딩 타입을 작성하는 상황에서 조건부 타입이 필요한 상황이 주로 발생하므로, 오버로딩 타입을 작성 중이라면 조건부 타입 사용을 검토해 보는 것이 좋다. -------------------------------------------------------------------------------- /ch06_타입_선언과_@types/item51_chanu.md: -------------------------------------------------------------------------------- 1 | # 의존성 분리를 위해 미러 타입 사용하기 2 | 3 | node js를 사용하여 구현한 `parseCSV` 함수 4 | ```typescript 5 | function parseCSV(contents: string | Buffer): {[column: string]: string} { 6 | if(typeof contents === 'object') { 7 | return pasrseCSV(contents.toString('utf-8')); 8 | } 9 | } 10 | ``` 11 | 12 | - 해당 함수의 인자로 사용되는 타입 `Buffer`는 node js 라이브러리에서 제공하는 타입 선언을 devDependencies에 의존성을 추가해야한다. 13 | `npm install --save-dev @types/node ` 14 | 15 | - `Buffer` 타입을 위해 의존성을 추가하게 되면, typescript를 사용하지 않는 개발자, Nodejs를 사용하지 않고 단순히 해당 함수만을 사용하는 개발자 모두 필요하지 않은 타입 선언들이다. 16 | **즉, 쓸데없이 너무 많이 들고 온다는 것이다.** 17 | 18 |
19 | 20 | ### 구조적 타이핑을 적용, 실제 필요한 타입만을 선언하여 사용하는 방식으로 구현한다. 21 | 22 | > 구조적 타이핑 - 객체 내의 속성을 모두 가지고 있다면, 해당 객체의 타입을 만족한다. 즉, 속성의 유무가 타입의 동일성을 판단하는 기준이 된다. 23 | 24 | ```typescript 25 | interface Vector2D { 26 | x: number; 27 | y: number; 28 | } 29 | 30 | function length(v: Vector2D) { 31 | return v.x * v.y; 32 | } 33 | 34 | 35 | interface NamedVector { 36 | name: string; 37 | x: number; 38 | y: number; 39 | } 40 | 41 | interface Vector3D { 42 | x: number; 43 | y: number; 44 | z: number; 45 | } 46 | ``` 47 | 48 | - `NamedVector`, `Vector3D` 모두 `Vector2D`의 속성을 모두 포함하기 때문에 둘 모두 `Vector2D`에 포함되며, 타입이 될 수 있다. 49 | - `NamedVector`, `Vector3D` 타입의 객체 모두 `length` 함수를 사용할 수 있다. 50 | 51 |
52 | 53 | ### 이러한 특성을 사용하여 기존의 라이브러리에서 제공하는 타입인 `Buffer`를 포함하는 **새로운 미러 타입**을 선언하여 사용한다. 54 | 55 | ```typescript 56 | interface CsvBuffer { 57 | toString(encoding: string) : string; 58 | } 59 | 60 | function parseCSV(contents: string | CsvBuffer): {[column: string]: string} { 61 | if(typeof contents === 'object') { 62 | return pasrseCSV(contents.toString('utf-8')); 63 | } 64 | } 65 | ``` 66 | - `Buffer` 대신 우리가 새롭게 선언한 `CsvBuffer`를 사용하였다. 67 | - 추후에 node js 개발자가 `Buffer`를 사용하는 경우, `Buffer` 타입 또한 `toString` 속성을 가지고 있으므로 `CsvBuffer` 타입에 포함되며 마찬가지로 `parseCSV` 함수를 사용할 수 있게 된다. 68 | ```parseCSV(new Buffer('wowowowo'))``` 69 | 70 | 71 |
72 | 73 | ### 정리하자면, 74 | 75 | #### 1. 라이브러리의 타입 선언 전체가 필요하지 않는 경우, 필요한 선언부만 추출하여 작성 중인 라이브러리에 넣는 것(미러링)을 고려해보는 것이 좋다. 76 | 77 | #### 2. 라이브러리의 타입 선언 대부분을 사용한다면 그냥 의존성을 추가하자. 78 | -------------------------------------------------------------------------------- /ch06_타입_선언과_@types/item52_dami.md: -------------------------------------------------------------------------------- 1 | # 아이템 52. 테스팅 타입의 함정에 주의하기 2 | 3 | 프로젝트를 공개하려면 테스트 코드를 작성하는 것이 필수이며, 타입 선언도 테스트를 거쳐야 하나 타입 선언을 테스트하는 것은 어렵습니다. 4 | 5 | 타입 선언에 대한 테스트 코드를 작성할 때 단언문으로 때우는 경우가 많지만 문제를 야기할 수 있기에, dtslint 또는 타입 시스템 외부에서 타입을 검사하는 유사한 도구를 사용하는 것이 더 안전하고 간단합니다. 6 | 7 |
8 | 9 | ## 반환타입을 체크하지 않는 테스트 예시 10 | 11 | ### 유틸리티 라이브러리(`lodash`, `underscore`) `map` 예시 12 | 13 | ```typescript 14 | declare function map(array: U[], fn: (u: U) => V): V[]; 15 | map(["2017", "2018", "2019"], (v) => Number(v)); 16 | ``` 17 | 18 | `map`의 타입 선언이 예상한 타입으로 결과를 내는지 체크하기 위해 `map` 함수를 호출한 코드입니다. 19 | 20 | 위 예제는 반환값에 대한 체크가 누락되어 있기 때문에 완전한 테스트라고 할 수 없습니다. 21 | 22 | ### 함수 런타임 동작 테스트 예시 23 | 24 | ```typescript 25 | const square = (x: number) => x * x; 26 | test("square a number", () => { 27 | square(1); 28 | square(2); 29 | }); 30 | ``` 31 | 32 | 위 예제 역시 함수 실행에서는 오류가 발생하지 않지만 체크할 뿐 반환값은 체크하지 않아 `square`의 구현이 잘못되어도 테스트는 통과하게 됩니다. 33 | 함수 실행만 하는 테스트 코드가 의미 없는 것은 아니지만, 실제로 반환 타입을 체크하는 것이 더 좋은 테스트 코드 입니다. 34 | 35 | 반환 값을 특정 타입의 변수에 할당하여 반환 타입을 체크할 수 있는 방법을 알아봅시다. 36 | 37 | ```typescript 38 | const lengths: number[] = map(["john", "paul"], (name) => name.length); 39 | ``` 40 | 41 | 위 예제는 **불필요한 타입 선언(아이템19)** 에 해당하나, 테스트 코드 관점에서는 중요한 역할을 하고 있습니다. (반환 타입을 `number[]`로 보장) 42 | 그러나 테스팅을 위해 할당을 사용하는 방법에는 두가지 근본적인 문제가 있습니다. 43 | 44 |
45 | 46 | ## 테스팅을 위해 할당을 사용하는 것의 2가지 문제점 47 | 48 | ### 1. 불필요한 변수 생성 49 | 50 | 반환값을 할당할 불필요한 변수를 생성하기 보다는 헬퍼 함수를 정의해서 문제를 해결할 수 있습니다. 51 | 52 | ```typescript 53 | declare function map(array: U[], fn: (u: U) => V): V[]; 54 | function assertType(x: T) {} 55 | 56 | assertType(map(["john", "paul"], (name) => name.length)); 57 | ``` 58 | 59 | 위 예제는 불필요한 변수 문제를 해결하지만, 두 타입이 동일한지 체크하는 것이 아니고 할당 가능성을 체크하고 있다는 문제가 있습니다. 60 | 61 | ### 2. 할당 가능성 체크 62 | 63 | ```typescript 64 | function assertType(x: T) {} 65 | const n = 12; 66 | assertType(n); // 정상 67 | ``` 68 | 69 | 위 예제에서는 잘 동작합니다. n 심벌의 타입은 실제로 숫자 리터럴 타입 12입니다. 12는 number의 서브타입이기에 할당 가능성 체크를 통과하게 됩니다. 70 | 즉, 타입이 동일한지 여부를 판단하는 것이 아니라 할당이 가능한지를 체크하는 것에 그칩니다. 71 | 72 |
73 | 74 | ## assertType 제대로 사용하기 (`Parameters`, `ReturnType` 제너릭 타입) 75 | 76 | `Parameters`와 `ReturnType` 제너릭 타입을 이용해 함수의 매개변수 타입과 반환 타입만 분리하여 테스트할 수 있습니다. 77 | 78 | ### Parameters 79 | 80 | 함수 타입 Type의 매개변수에 사용된 타입에서 튜플 타입을 생성합니다. ([참고](https://www.typescriptlang.org/ko/docs/handbook/utility-types.html#parameterstype)) 81 | 82 | ### ReturnType 83 | 84 | 함수 Type의 반환 타입으로 구성된 타입을 생성합니다. ([참고](https://www.typescriptlang.org/ko/docs/handbook/utility-types.html#returntypetype)) 85 | 86 | ```typescript 87 | const double = (x: number) => 2 * x; 88 | let p: Parameters = null!; 89 | assertType<[number, number]>(p); 90 | // ~ '[number]' 형식의 인수는 '[number, number]' 91 | // 형식의 매개변수에 할당될 수 없습니다. 92 | let r: ReturnType = null!; 93 | assertType(r); // 정상 94 | ``` 95 | 96 |
97 | 98 | ## 콜백함수가 지닌 문제점 (this 관련) 99 | 100 | 예제에 사용된 `map`의 콜백함수는 화살표 함수(항상 상위 스코프의 `this`를 가리킴)가 아니기에 `this`의 타입이 테스트 대상이 됩니다. 101 | 102 | ```typescript 103 | const beatles = ["john", "paul", "george", "ringo"]; 104 | assertType( 105 | map(beatles, function (name, i, array) { 106 | // ~~~~~~~ '(name: any, i: any, array: any) => any' 형식의 인수는 107 | // '(u: string) => any' 형식의 매개변수에 할당될 수 없습니다. 108 | assertType(name); 109 | assertType(i); 110 | assertType(array); 111 | assertType(this); 112 | // ~~~~ 'this'에는 암시적으로 'any' 형식이 포함됩니다. 113 | return name.length; 114 | }) 115 | ); 116 | ``` 117 | 118 | 위 예제의 문제는 아래 코드의 선언을 사용하면 타입 체크를 통과하게 됩니다. 119 | 120 | ```typescript 121 | declare function map( 122 | array: U[], 123 | fn: (this: U[], u: U, i: number, array: U[]) => V 124 | ): V[]; 125 | ``` 126 | 127 |
128 | 129 | ## DefinitelyTyped의 타입 선언 도구 - dtslint 130 | 131 | [github 바로가기 microsoft/dtslint](https://github.com/microsoft/dtslint) 132 | 133 | dtslint는 특별한 형태의 주석을 통해 동작합니다. dtslint를 사용하면 beatles 관련 예제 테스트를 다음과 같이 작성할 수 있습니다. 134 | 135 | ```typescript 136 | declare function map( 137 | array: U[], 138 | fn: (this: U[], u: U, i: number, array: U[]) => V 139 | ): V[]; 140 | const beatles = ["john", "paul", "george", "ringo"]; 141 | map( 142 | beatles, 143 | function ( 144 | name, // $ExpectType string 145 | i, // $ExpectType number 146 | array // $ExpectType string[] 147 | ) { 148 | this; // $ExpectType string[] 149 | return name.length; 150 | } 151 | ); // $ExpectType number[] 152 | ``` 153 | 154 | dtslint는 할당 가능성을 체크하는 대신 각 심벌의 타입을 추출하여 글자 자체가 같은지 비교합니다. 155 | 그러다 보니 `number|string` 과 `string|number`는 같은 타입이지만 글자는 다르기 때문에 다른 타입으로 인식합니다. 156 | 157 |
158 | 159 | ## 결론 160 | 161 | - 타입을 테스트 할 때는 특히 함수 타입의 동일성(equality)과 할당 가능성(assignability)의 차이점을 알고 있어야 한다. 162 | - 콜백이 있는 함수를 테스트 할 때, 콜백 매개변수의 추론된 타입을 체크해야 합니다. 163 | - this가 API 일부분이라면 테스해야 한다. 164 | - 엄격한 테스트를 위해 dtslint 같은 도구를 사용할 수도 있다. 165 | -------------------------------------------------------------------------------- /ch07_코드를_작성하고_실행하기/item53_호찬.md: -------------------------------------------------------------------------------- 1 | # 아이템 53. 타입스크립트 기능보다는 ECMAScript 기능을 사용하기 2 | 3 | 타입스크립트가 만들어지던 2010년에 자바스크립트는 결함이 많았고, 개선해야 할 부분이 많은 언어였다. 타입스크립트 초기 버전에는 독립적으로 클래스, 열거형(enum), 모듈 시스템을 포함시켰지만, 시간이 흐르면서 TC39는 부족했던 점들을 내장 기능으로 추가했다. 새로 추가된 기능들은 타입스크립트에서 호환성 문제가 발생했다. 4 | 5 | 따라서, **TC39는 런타임 기능을 발전시키고, 타입스크립트 팀은 타입 기능만 발전시킨다는 명확한 원칙을 세우고 현재까지 지켜오고 있다.** 6 | 7 | 타입 공간(타입스크립트)과 값 공간(자바스크립트)의 경계를 혼란스럽게 만드는 타입스크립트 기능들은 사용하지 않는게 좋다. 8 | 9 | ### 열거형(enum) 10 | 11 | ```typescript 12 | enum Flavor { 13 | VANILLA = 0, 14 | CHOCOLATE = 1, 15 | STRAWBERRY = 2, 16 | } 17 | 18 | let flavor = Flavor.CHOCOLATE; 19 | 20 | Flavor // 자동 완성 추천: VANILLA, CHOCOLATE, STRAWBERRY 21 | Flavor[0] // 값이 "VANILLA" 22 | ``` 23 | 24 | 타입스크립트 열거형은 아래 상황에 따라 다르게 동작한다. 25 | 26 | - 숫자 열거형에 0, 1, 2 외의 다른 숫자가 할당되면 매우 위험하다. 27 | - 상수 열거형은 보통의 열거형과 달리 런타임에서 제거된다. `const enum Flavor`와 같이 const를 붙이면 `Flavor.CHOCOLATE` 가 0으로 바뀌어 버린다. 28 | - `preserveConstEnums` 플래그를 설정하면 상수 열거형은 보통의 열거형처럼 런타임 코드에 열거형 정보를 유지한다. 29 | - 문자형 열거형은 구조적 타이핑이 아닌 명목적 타이핑을 사용한다. (명목적 타이핑은 타입의 이름이 같이야 할당이 허용된다.) 30 | 31 | #### 숫자 열거형에 0, 1, 2 외의 다른 숫자가 할당되면 매우 위험하다. 32 | 33 | ```typescript 34 | enum Alphabet{ 35 | A = -2, // -2 36 | B, // -1 37 | C = 10, // 10 38 | D, // 11 39 | E = 0, // 0 40 | F // 1 41 | } 42 | ``` 43 | 44 | #### 문자형 열거형의 명목적 타이핑 45 | 46 | scoop 함수가 있다고 한다면, Flavor는 런타임때 문자열이기 때문에 자바스크립트에서 아래와 같이 호출된다. 47 | 48 | ```typescript 49 | function scoop(flavor: Flavor){/* ... */} 50 | ``` 51 | 52 | ```javascript 53 | // javascript 54 | scoop('vanilla'); 55 | ``` 56 | 57 | 그러나 타입스크립트에서는 열거형을 임포트하고 대신 사용해야한다. 58 | 59 | ```typescript 60 | // typescript 61 | scoop('vanilla'); // 'vanilla' 형식은 'Flavor' 형식의 매개변수에 할당 될 수 없습니다. 62 | 63 | import {Flavor} from 'ice-cream'; 64 | scoop(Flavor.VANILLA); // 정상 65 | ``` 66 | 67 | 자바스크립트와 타입스크립트의 동작이 다르기 때문에 문자열 열거형은 사용하지 않는게 좋다. 대신 리텉럴 타입과 유니온을 사용해서 해결할 수 있다. 68 | 69 | ```typescript 70 | type Flavor = 'vanilla' | 'chocolate' | 'strawberry'; 71 | 72 | let flavor: Flavor = 'chocolate'; 73 | ``` 74 | 75 | - [Typescript Deep Dive 열거형(Enums)](https://radlohead.gitbook.io/typescript-deep-dive/type-system/enums) 76 | - [TypeScript enum을 사용하지 않는 게 좋은 이유를 Tree-shaking 관점에서 소개합니다.](https://engineering.linecorp.com/ko/blog/typescript-enum-tree-shaking/) 77 | 78 | ### 매개변수 속성 79 | 80 | ```typescript 81 | class Person { 82 | name: string; 83 | constructor(name: string) { 84 | this.name = name; 85 | } 86 | } 87 | ``` 88 | 89 | 타입스크립트는 더 간결하 문법을 제공한다. 90 | 91 | ```typescript 92 | class Person { 93 | constructor(public name: string) {} 94 | } 95 | ``` 96 | 97 | public name은 `매개변수 속성`이라고 부른다. `매개변수 속성`은 아래와 같은 몇가지 문제점이 존재한다. 98 | 99 | - 일반적으로 타입스크립트 컴파일은 타입 제거가 이루어지므로 코드가 줄어들지만, 매개변수 속성은 코드가 늘어나는 문법이다. 100 | - 매개변수 속성이 런타임에는 실제로 사용되지만, 타입스크립트 관점에서는 사용되지 않는 것처럼 보인다. 101 | - 매개변수 속성과 일반 속성을 섞어서 사용하면 클래스의 설계가 혼란스러워 진다. 102 | 103 | ```typescript 104 | class Person { 105 | first: string; 106 | last: string; 107 | constructor(public name: string) { 108 | [this.first, this.last] = name.split(' '); 109 | } 110 | } 111 | ``` 112 | 113 | Person 클래스에는 세가지 속성(first, last, name)이 있지만, name은 매개변수 속성에 있어서 일관성이 없다. 아래와 같은 상황도 일어날 수 있다. 114 | 115 | ```typescript 116 | class Person { 117 | constructor(public name: string) {} 118 | } 119 | 120 | const p: Person = {name: 'Jed Bartlet'}; // 정상 121 | ``` 122 | 123 | 초급자에게는 생소한 문법이다. 따라서 일반 속성과 매개변수 속성 중 한 가지만 사용하자. 124 | 125 | ### 네임스페이스와 트리플 슬래시 임포트 126 | 127 | 타입스크립트는 자체적으로 모듈 시스템을 구축했고, 충돌을 피하기 위해 module과 같은 기능을 하는 namespace 키워드를 추가했다. 128 | 129 | ```typescript 130 | namespace foo { 131 | function bar() {} 132 | } 133 | 134 | /// 135 | foo.bar 136 | ``` 137 | 138 | 트리플 슬래시 임포트와 module 키워드는 호환성을 위해 남아 있을 뿐이며, 이제는 ECMAScript 2015 스타일의 모듈(import와 export)를 사용해야 한다. 139 | 140 | ```typescript 141 | type Flavor = 'vanilla' | 'chocolate' | 'strawberry'; 142 | 143 | export default Flavor 144 | ``` 145 | 146 | ```typescript 147 | import type Flavor from '@/types/flavor' 148 | ``` 149 | 150 | ### 데코레이터 151 | 152 | 데코레이터는 클래스, 메서드, 속성에 애너테이션을 붙이거나 기능을 추가하는데 사용할 수 있다. 153 | 154 | ```typescript 155 | class Greeter { 156 | greeting: string; 157 | constructor(message: string) { 158 | this.greeting = message; 159 | } 160 | 161 | @logged 162 | greet() { 163 | return "Hello, " + this.greeting; 164 | } 165 | } 166 | 167 | function logged(target: any, name: string, descriptor: PropertyDescriptor) { 168 | const fn = target[name]; 169 | descriptor.value = function() { 170 | console.log(`Calling ${name}`); 171 | return fn.apply(this, arguments); 172 | } 173 | } 174 | 175 | console.log(new Greeter('Dave').greet()); 176 | // 출력: 177 | // Calling greet 178 | // Hello, Dave 179 | ``` 180 | 181 | 데코레이터는 처음에 앵귤러 프레임워크를 지원하기 위해 추가되었으며 tsconfig.json에 experimentalDecorators 속성을 설정하고 사용해야 한다. 현재까지도 표준화가 완료되지 않았기 때문에, 사용 중인 데코레이터가 비표준으로 바뀌거나 호환성이 깨질 수 있다. 따라서 사용하지 말자. 182 | 183 | ### 요약 184 | 185 | - 일반적으로 타입스크립트 코드에서 모든 타입 정보를 제거하면 자바스크립트가 되지만, **열거형, 매개변수 속성, 트리플 슬래시 임포트, 데코레이터는 타입 정보를 제거한다고 자바스크립트가 되지 않는다.** 186 | - 타입스크립트의 역확을 명확하게 하려면 **열거형, 매개변수 속성, 트리플 슬래시 임포트, 데코레이터는** 사용하지 말자. 187 | -------------------------------------------------------------------------------- /ch07_코드를_작성하고_실행하기/item55_dami.md: -------------------------------------------------------------------------------- 1 | # 아이템 55. DOM 계층 구조 이해하기 2 | 3 | 타입스크립트에서는 DOM 엘리먼트의 계층 구조를 파악하기 용이합니다. `Element`와 `EventTarget`에 달려 있는 Node의 구체적인 타입을 안다면 타입 오류를 디버깅할 수 있고, 언제 타입 단언을 사용해야 할지 알 수 있습니다. 4 | 5 | 6 | 7 | (출처: https://ko.javascript.info/basic-dom-node-properties) 8 | 9 | `HTMLInputElement`는 `HTMLElement`의 서브타입이고, `HTMLElement`는 `Element`의 서브타입입니다. 또한 `Element`는 `Node`의 서브타입이고, `Node`는 `EventTarget`의 서브타입입니다. 10 | 11 | ## 계층 구조별 타입 12 | 13 | ### EventTarget 14 | 15 | - DOM 타입 중 가장 추상화된 타입입니다. 16 | - 이벤트 리스터를 추가하거나 제거하고, 이벤트를 보내는 것만 할 수 있습니다. 17 | 18 | ```typescript 19 | function handleDrag(eDown: Event) { 20 | const targetEl = eDown.currentTarget; 21 | targetEl.classList.add("dragging"); 22 | // ~~~~~~~ 객체가 'null'인 것 같습니다. 23 | // ~~~~~~~~~ 'EventTarget' 형식에 'classList' 속성이 없습니다. 24 | // ... 25 | } 26 | ``` 27 | 28 | - `currentTarget` 속성의 타입은 `EventTarget | null` 입니다. 29 | - `EventTarget` 타입에 `classList` 속성이 없습니다. 30 | 31 | ### Node 32 | 33 | - `Element`가 아닌 `Node`인 경우 예시 : 텍스트 조각, 주석 34 | 35 | ```html 36 |

37 | And yet it move 38 | 39 |

40 | ``` 41 | 42 | - `

` 는 `HTMLParagraphElement` 이며, `children`과 `childNodes` 속성을 가지고 있습니다. 43 | - `children`은 자식 엘리먼트 `yet` 를 포함하는 `HTMLCollection`입니다. 44 | 45 | ### Element, HTMLElement 46 | 47 | - `SVGElemen`t: `SVG 태그`의 전체 계층 구조를 포함하면서 `HTML`이 아닌 엘리먼트입니다. 48 | - ``은 HTMLhtmlElement, ``는 SVGElement 입니다. 49 | 50 | ### HTMLxxxElement 51 | 52 | - 자신만의 고유한 속성을 가지고 있습니다. 53 | 54 | - `HTMLImageElement`에는 `src` 속성 55 | - `HTMLInputElemnt`에는 `value` 속성 56 | 57 | - 속성에 접근하려면, 타입 정보 역시 실제 엘리먼트 타입이어야 합니다. 58 | - 보통은 HTML 태그 값에 해당하는 'button'같은 리터럴 값을 사용하여 DOM에 대한 정확한 타입을 얻을 수 있습니다. 59 | 60 | ```typescript 61 | document.getElementsByTagName("p")[0]; // HTMLParagraphElement 62 | document.createElement("button"); // HTMLButtonElement 63 | document.querySelector("div"); // HTMLDivElement 64 | ``` 65 | 66 | 하지만 항상 정확한 타입을 얻을 수 있는 것은 아닙니다. 67 | 68 | ```typescript 69 | document.getElementById("my-div"); // HTMLElement 70 | ``` 71 | 72 | 위 예시는 HTMLElement로 추론을 하고 있는데, 이 경우는 우리가 더 정확한 타입을 알고 있는 경우이므로 단언문을 사용해도 됩니다. 73 | 74 | ```typescript 75 | document.getElementById("my-div") as HTMLDivElement; 76 | ``` 77 | 78 | ## Event 타입 79 | 80 | Event는 가장 추상화된 이벤트입니다. 더 구체적인 타입들은 다음과 같습니다. 81 | 82 | - `UIEvent`: 모든 종류의 사용자 인터페이스 이벤트 83 | - `MouseEvnet`: 클릭처럼 마우스로부터 발생되는 이벤트 84 | - `TouchEvent`: 모바일 기기의 터치 이벤트 85 | - `WheelEvent`: 스크롤 휠을 돌려서 발생되는 이벤트 86 | - `KeyboardEvent`: 키 누름 이벤트 87 | 88 | ### Event 관련 오류 예시 89 | 90 | ```typescript 91 | function handleDrag(eDown: Event) { 92 | // ... 93 | const dragStart = [eDown.clientX, eDown.clientY]; 94 | // ~~~~~~~ 'Event'에 'clientX' 속성이 없습니다. 95 | // ~~~~~~~ 'Event'에 'clientY' 속성이 없습니다. 96 | // ... 97 | } 98 | ``` 99 | 100 | `clientX`와 `clientY`에서 발생한 오류는 handleDrag 함수의 매개변수는 `Event`로 선언된 반면 `clientX`와 `clientY`는 구체적인 `MouseEvent` 타입에 있기 때문입니다. 101 | 102 | ### Event 관련 오류 해결 103 | 104 | DOM에 대한 타입 추론은 문맥 정보를 폭넓게 활용합니다.(아이템 26). 105 | 106 | 1. `mousedown` 이벤트 핸들러를 인라인 함수로 만들기 107 | 108 | - 타입스크립트는 더 많은 문맥 정보를 사용하게 되고, 대부분의 오류를 제거할 수 있습니다. 109 | 110 | 2. `Event` 대신 `MouseEvent`로 선언하기 111 | 112 | ```typescript 113 | function addDragHandler(el: HTMLElement) { 114 | el.addEventListener("mousedown", (eDown) => { 115 | const dragStart = [eDown.clientX, eDown.clientY]; 116 | const handleUp = (eUp: MouseEvent) => { 117 | el.classList.remove("dragging"); 118 | el.removeEventListener("mouseup", handleUp); 119 | const dragEnd = [eUp.clientX, eUp.clientY]; 120 | console.log( 121 | "dx, dy = ", 122 | [0, 1].map((i) => dragEnd[i] - dragStart[i]) 123 | ); 124 | }; 125 | el.addEventListener("mouseup", handleUp); 126 | }); 127 | } 128 | 129 | const div = document.getElementById("surface"); 130 | if (div) { 131 | addDragHandler(div); 132 | } 133 | ``` 134 | 135 | - 마지막 `if` 구문은 `#surface` 엘리먼트가 없는 경우를 체크합니다. 이때 해당 엘리먼트가 반드시 존재한다는 것을 알고 있다면, if 구문 대신 단언문(`addDragHandler(div!)`)을 사용할 수 있습니다. 136 | > 타입스크립트에서 변수 앞이 아닌, 뒤에 느낌표(!)를 사용하면 기발한 용도로 사용할 수 있는데, 피연산자가 Nullish(null이나 undefined) 값이 아님을 단언할 수 있다. 단언 연산자(Non-null assertion operator) 또는 확정 할당 어선셜(Definite Assignment Assertions) 라 부른다. 137 | 138 | # 결론 139 | 140 | - DOM 타입은 타입스크립트에서 중요한 정보이다. 141 | - Node, Element, HTMLElement, EventTarget 간의 차이점, Event와 MouseEvent의 차이점을 알아야 한다. 142 | -------------------------------------------------------------------------------- /ch07_코드를_작성하고_실행하기/item56_chanu.md: -------------------------------------------------------------------------------- 1 | # 정보를 감추는 목적으로 `private`를 사용하지 않기 2 | 3 | > JS 내에서는 클래스에 비공개 속성을 만들 수 없습니다. 그 대신, `_`를 속성명의 접두사로 사용하는 것이 관례입니다. 4 | 5 | 6 | ```typescript 7 | class Human { 8 | _name = 'chanwoo'; 9 | } 10 | 11 | const chanwoo = new Human(); 12 | chanwoo._name; 13 | ``` 14 | 15 | - 관례는 관례일뿐, 정보의 은닉화가 제대로 이루어지지 않는다. 16 | 17 |
18 | 19 | #### TS에서는 접근자를 사용하여 편집기에서 접근하지 못하도록 에러를 명시할 수 있다. 20 | ```typescript 21 | class Human { 22 | private name = 'chanwoo'; 23 | } 24 | 25 | const chanwoo = new Human(); 26 | chanwoo._name; 27 | ``` 28 | - `TS2339: Property '_name' does not exist on type 'Human'.` 29 | - `private`를 사용하면 `_name`에 접근하지 못하도록 한다. 하지만 TS는 컴파일시에 사라지기 때문에 위의 코드와 동일하다. 30 | 31 | 32 | ```typescript 33 | class Human { 34 | private name = 'chanwoo'; 35 | } 36 | 37 | const chanwoo = new Human(); 38 | (chanwoo as any)._name; 39 | ``` 40 | - 심지어 any 단언문을 사용하면 `private` 접근 제어자도 소용이 없다. 41 | 42 |
43 | 44 | ### JS에서는 정보의 은닉화를 위해서 클로저(closure)를 사용한다. 45 | 46 | > 클래스에서는 생성자에 클로저를 만들 수 있습니다. 47 | 48 | ```typescript 49 | class PasswordChecker { 50 | checkPassword: (password: string) => boolean; 51 | constructor(passwordHash: number) { 52 | this.checkPassword = (password: string) => { 53 | return hash(password) === passwordHash; 54 | } 55 | } 56 | } 57 | ``` 58 | - `passwordHash`는 생성자에서만 접근이 가능하다. 즉, 해당 변수는 감추어졌다. 59 | - 하지만 해당 변수에 접근하기 위해서는 반드시 생성자 내에서 선언되어야하므로, 메서드 역시 내부에 정의되어야하고, 이렇게 되면 메서드의 복사본이 모든 인스턴스에 생성되기 때문에 메모리가 낭비된다. (클래스의 메서드의 경우, 모든 인스턴스가 공유할 수 있다.) 60 | 61 |
62 | 63 | ### JS에서는 정보의 은닉화를 위해서 또 다른 방식은 비공개 필드 기능이 있다. 64 | ```typescript 65 | class PasswordChecker { 66 | #passwordHash: number; 67 | 68 | constructor(passwordHash: number) { 69 | this.#passwordHash = passwordHash; 70 | } 71 | 72 | checkPassword(password: string){ 73 | return hash(password) === passwordHash; 74 | }; 75 | } 76 | ``` 77 | - 클로저와 동일하게 `passwordHash`는 감추어졌다. 또한 클로저의 문제점이었던 생성자에서의 메서드 선언이 외부에서도 가능해졌다. 78 | - 비공개 필드 기능을 지원하지 않는 경우, `weakMap`으로 구현되어 제공한다고 한다. 79 | 80 | 81 | ### 정리하자면, 82 | 83 | #### 1. `_`, `private` 접근제어자의 경우, 단순히 표기상으로만 정보의 은닉이 이루어진다. 84 | 85 | #### 2. JS에서는 클로저나 비공개 필드 기능을 통해 정보의 은닉을 구현할 수 있다. -------------------------------------------------------------------------------- /ch07_코드를_작성하고_실행하기/item57/img/after-source-map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/11st-corp/effective-typescript/21099a52aa554a17a888ca5bde4b3c90d9fb0a1c/ch07_코드를_작성하고_실행하기/item57/img/after-source-map.png -------------------------------------------------------------------------------- /ch07_코드를_작성하고_실행하기/item57/img/before-source-map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/11st-corp/effective-typescript/21099a52aa554a17a888ca5bde4b3c90d9fb0a1c/ch07_코드를_작성하고_실행하기/item57/img/before-source-map.png -------------------------------------------------------------------------------- /ch07_코드를_작성하고_실행하기/item57/src/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | yarn.lock 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /ch07_코드를_작성하고_실행하기/item57/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /ch07_코드를_작성하고_실행하기/item57/src/index.js: -------------------------------------------------------------------------------- 1 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 2 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 3 | return new (P || (P = Promise))(function (resolve, reject) { 4 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 5 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 6 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 7 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 8 | }); 9 | }; 10 | var __generator = (this && this.__generator) || function (thisArg, body) { 11 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 12 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 13 | function verb(n) { return function (v) { return step([n, v]); }; } 14 | function step(op) { 15 | if (f) throw new TypeError("Generator is already executing."); 16 | while (_) try { 17 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 18 | if (y = 0, t) op = [op[0] & 2, t.value]; 19 | switch (op[0]) { 20 | case 0: case 1: t = op; break; 21 | case 4: _.label++; return { value: op[1], done: false }; 22 | case 5: _.label++; y = op[1]; op = [0]; continue; 23 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 24 | default: 25 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 26 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 27 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 28 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 29 | if (t[2]) _.ops.pop(); 30 | _.trys.pop(); continue; 31 | } 32 | op = body.call(thisArg, _); 33 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 34 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 35 | } 36 | }; 37 | function addCounter(el) { 38 | var _this = this; 39 | var clickCount = 0; 40 | var triviaEl = document.createElement('p'); 41 | var button = document.createElement('button'); 42 | button.textContent = 'Click me'; 43 | button.addEventListener('click', function () { return __awaiter(_this, void 0, void 0, function () { 44 | var response, trivia; 45 | return __generator(this, function (_a) { 46 | switch (_a.label) { 47 | case 0: 48 | clickCount++; 49 | return [4 /*yield*/, fetch("http://numbersapi.com/" + clickCount)]; 50 | case 1: 51 | response = _a.sent(); 52 | return [4 /*yield*/, response.text()]; 53 | case 2: 54 | trivia = _a.sent(); 55 | trivia.textContent = trivia; 56 | button.textContent = "Click me (" + clickCount + ")"; 57 | return [2 /*return*/]; 58 | } 59 | }); 60 | }); }); 61 | el.appendChild(triviaEl); 62 | el.appendChild(button); 63 | } 64 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /ch07_코드를_작성하고_실행하기/item57/src/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,UAAU,CAAC,EAAe;IAAnC,iBAcC;IAbG,IAAI,UAAU,GAAG,CAAC,CAAC;IACnB,IAAM,QAAQ,GAAG,QAAQ,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;IAC7C,IAAM,MAAM,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;IAChD,MAAM,CAAC,WAAW,GAAG,UAAU,CAAC;IAChC,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE;;;;;oBAC7B,UAAU,EAAE,CAAC;oBACI,qBAAM,KAAK,CAAC,2BAAyB,UAAY,CAAC,EAAA;;oBAA7D,QAAQ,GAAG,SAAkD;oBACpD,qBAAM,QAAQ,CAAC,IAAI,EAAE,EAAA;;oBAA9B,MAAM,GAAG,SAAqB;oBACpC,MAAM,CAAC,WAAW,GAAG,MAAM,CAAC;oBAC5B,MAAM,CAAC,WAAW,GAAG,eAAa,UAAU,MAAG,CAAC;;;;SACnD,CAAC,CAAC;IACH,EAAE,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;IACzB,EAAE,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;AAC3B,CAAC"} -------------------------------------------------------------------------------- /ch07_코드를_작성하고_실행하기/item57/src/index.ts: -------------------------------------------------------------------------------- 1 | function addCounter(el: HTMLElement) { 2 | let clickCount = 0; 3 | const triviaEl = document.createElement('p'); 4 | const button = document.createElement('button'); 5 | button.textContent = 'Click me'; 6 | button.addEventListener('click', async () => { 7 | clickCount++; 8 | const response = await fetch(`http://numbersapi.com/${clickCount}`); 9 | const trivia = await response.text(); 10 | trivia.textContent = trivia; 11 | button.textContent = `Click me (${clickCount})`; 12 | }); 13 | el.appendChild(triviaEl); 14 | el.appendChild(button); 15 | } 16 | -------------------------------------------------------------------------------- /ch07_코드를_작성하고_실행하기/item57/src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "item57", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "devDependencies": { 7 | "tsc": "^2.0.4" 8 | }, 9 | "dependencies": { 10 | "typescript": "^4.9.4" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ch07_코드를_작성하고_실행하기/item57_호찬.md: -------------------------------------------------------------------------------- 1 | # 아이템 57. 소스맵을 사용하여 타입스크립트 디버깅하기 2 | 3 | 디버깅은 런타임에 동작한다. 디버깅이 필요한 시점에 타입스크립트가 직접 실행되는 것이 아니다. 따라서 현재 동작하는 코드가 어떤 과정을 거쳐서 만들어진 것인지 알기가 어렵다. **변환된 자바스크립트 코드는 전처리기, 컴파일러, 압축기를 거친 자바스크립트 코드로 복잡해 디버깅하기가 매우 어렵다.** 4 | 5 | **디버깅 문제를 해결해기 위해 브라우저 제조사들은 서로 협력하여 소스맵(source map)이라는 해결책을 내놓았다.** 소스맵은 변환된 코드의 위치와 심벌들을 원본 코드의 원래 위치와 심벌들로 매핑한다. 6 | 7 | 다음 Typescript 코드를 디버깅해보자. 8 | 9 | ```ts 10 | function addCounter(el: HTMLElement) { 11 | let clickCount = 0; 12 | const triviaEl = document.createElement('p'); 13 | const button = document.createElement('button'); 14 | button.textContent = 'Click me'; 15 | button.addEventListener('click', async () => { 16 | clickCount++; 17 | const response = await fetch(`http://numbersapi.com/${clickCount}`); 18 | const trivia = await response.text(); 19 | trivia.textContent = trivia; 20 | button.textContent = `Click me (${clickCount})`; 21 | }); 22 | el.appendChild(triviaEl); 23 | el.appendChild(button); 24 | } 25 | ``` 26 | 27 | 오래된 브라우저에서 async와 await를 지원하기위해 타입스크립트는 이벤트 핸들러를 상태 머신으로 재작성한다. 원본 코드하고 동일하게 동작하지만 이해하기가 어렵다. 28 | 29 | ![before-source-map](./item57/img/before-source-map.png) 30 | 31 | sourcemap 옵션을 켜서 컴파일을 실행하면, .js 와 .js.map 두개의 파일이 생성된다. 32 | 33 | ```sh 34 | tsc index.ts --sourcemap true 35 | ``` 36 | 37 | sourcemap 파일을 생성한다면, 브라우저에서 Typescript로 디버깅이 가능하다. 38 | 39 | ![after-source-map](./item57/img/after-source-map.png) 40 | 41 | 디버거 좌측 파일 목록에서 index.ts가 기울임(이탤릭) 글꼴로 나오는 것을 확인할 수 있다. 기울임 글꼴은 웹 페이지에 포함된 '실제' 파일이 아니라는 것을 뜻한다. **실제로는 소스맵을 통해 타입스크립트처럼 보이는 것뿐이다.** 42 | 43 | ### 주의사항 44 | 45 | - 타입스크립트와 함께 번들러나 압축기를 사용하고 있다면, **번들러나 압축기가 각자의 소스맵을 생성하므로 설정을 한번 더 확인하자.** 46 | - **상용 환경에 소스맵이 유출되고 있는지 확인하자.** 디비거를 열지 않으면 소스맵이 로드되지 않으므로, 실제 사용자에게 **성능 저하는 발생하지 않는다.** 그러나 중요한 인라인 복사본이 포함되어 있다면 공개해서는 안된다. 47 | - **NodeJS 프로그램의 디버깅에도 소스맵을 사용할 수 있다.** 48 | - **타입 체커가 코드를 실행하기 전에 많은 오류를 잡을 수 있지만, 디버거를 대체할 수 없다.** 49 | 50 | 51 | ### 요약 52 | 53 | - 원본 코드가 아닌 변환된 자바스크립트 코드를 디버깅하지 말자. 소스맵을 사용해서 런타임에 타입스크립트 코드릴 디버깅하자. 54 | - 소스맵이 최종적으로 변환된 코드에 완전히 맵핑되었는지 확인하자. 55 | - 소스맵에 원본 코드가 그대로 포함되도록 설정되어 있을 수 있다. 소스맵이 공개되지 않도록 설정을 확인하자. -------------------------------------------------------------------------------- /ch08_타입스크립트로_마이그레이션하기/item59/img/dom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/11st-corp/effective-typescript/21099a52aa554a17a888ca5bde4b3c90d9fb0a1c/ch08_타입스크립트로_마이그레이션하기/item59/img/dom.png -------------------------------------------------------------------------------- /ch08_타입스크립트로_마이그레이션하기/item59/img/global.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/11st-corp/effective-typescript/21099a52aa554a17a888ca5bde4b3c90d9fb0a1c/ch08_타입스크립트로_마이그레이션하기/item59/img/global.png -------------------------------------------------------------------------------- /ch08_타입스크립트로_마이그레이션하기/item59/img/ts-check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/11st-corp/effective-typescript/21099a52aa554a17a888ca5bde4b3c90d9fb0a1c/ch08_타입스크립트로_마이그레이션하기/item59/img/ts-check.png -------------------------------------------------------------------------------- /ch08_타입스크립트로_마이그레이션하기/item59/img/unknown-library.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/11st-corp/effective-typescript/21099a52aa554a17a888ca5bde4b3c90d9fb0a1c/ch08_타입스크립트로_마이그레이션하기/item59/img/unknown-library.png -------------------------------------------------------------------------------- /ch08_타입스크립트로_마이그레이션하기/item59/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const person = {first: 'Grace', last: 'Hopper'}; 3 | 2 * person.first 4 | 5 | console.log(user.firstName) 6 | 7 | /// 8 | console.log(user.firstName) 9 | 10 | $('#graph').css({'width': '100px', 'height': '100px'}); 11 | 12 | const ageEl = /** @type {HTMLInputElement} */ (document.getElementById('age')); 13 | ageEl.value = '12'; 14 | -------------------------------------------------------------------------------- /ch08_타입스크립트로_마이그레이션하기/item59/types.d.ts: -------------------------------------------------------------------------------- 1 | interface UserData { 2 | firstName: string; 3 | lastName: string; 4 | } 5 | declare let user: UserData; 6 | -------------------------------------------------------------------------------- /ch08_타입스크립트로_마이그레이션하기/item59_호찬.md: -------------------------------------------------------------------------------- 1 | # 아이템 59. 타입스크립트 도입 전에 @ts-check와 JSDoc으로 시험해 보기 2 | 3 | ## `@ts-check` 4 | 5 | `@ts-check` 지시자를 사용하면 타입스크립트 전환시에 어떤 문제가 발생하는지 미리 시험해 볼 수 있다. `noImplicityAny` 설정을 해제한 것보다 느슨한 검사를 수행한다. 6 | 7 | ```js 8 | // @ts-check 9 | const person = {first: 'Grace', last: 'Hopper'}; 10 | 2 * person.first 11 | ``` 12 | 13 | 14 | 15 | `person.first`의 타입은 string으로 추론되었고, `2 * person.first`는 타입 불일치 오류가 발생했다. 16 | 17 | ### 선언 되지 않은 전역 변수 18 | 19 | 20 | 21 | 어딘가에 선언되어 있는 전역 변수의 경우 `types.d.ts` 파일에 별도로 정의 해주어야 한다. 22 | 23 | ```ts 24 | interface UserData { 25 | firstName: string; 26 | lastName: string; 27 | } 28 | declare let user: UserData; 29 | ``` 30 | 31 | 이후 '트리플 슬래시' 참조를 사용하여 명시적으로 임포트하면 오류가 해결된다. 32 | 33 | ```js 34 | /// 35 | console.log(user.firstName) 36 | ``` 37 | 38 | ### 알 수 없는 라이브러리 39 | 40 | ```js 41 | // @ts-check 42 | $('#graph').css({'width': '100px', 'height': '100px'}); 43 | ``` 44 | 45 | 46 | 47 | 알 수 없는 라이브러리의 경우 라이브러리에 맞는 타입 선언을 설치하면 설치한 타입 선언을 참조하게 된다. 48 | 49 | ```sh 50 | npm install --save-dev @types/jquery 51 | ``` 52 | 53 | ### DOM 문제 54 | 55 | ```js 56 | const ageEl = document.getElementById('age'); 57 | ageEl.value = '12'; 58 | ``` 59 | 60 | 61 | 62 | DOM 관련 오류의 경우 `JSDoc`을 사용해서 해결할 수 있다. 63 | 64 | ```js 65 | const ageEl = /** @type {HTMLInputElement} */ (document.getElementById('age')); 66 | ageEl.value = '12'; 67 | ``` 68 | 69 | ### 부정확한 JSDoc 70 | 71 | 프로젝트에서 이미 JSDoc 스타일의 주석을 사용중이었다면, `@ts-check` 지시자를 설정하는 순간부터 기존 주석에 타입 체크가 동작하게 되고 갑자기 수많은 오류가 발생하게 된다. 이때는 당황하지 말고 타입 정보를 차근차근 추가해 나가면 된다. 72 | 73 | `@ts-check` 지시자와 JSDoc 주석을 너무 장기간 사용하는 것은 좋지 않다. 주석이 코드 분량을 늘려서 로직을 해석하는데 방해가 될 수 있다. **마이그레이션의 궁극적인 목표는 자바스크립트에 JSDoc 주석이 있는 형태가 아니라 모든 코드가 타입스크립트 기반으로 전환되는 것임을 잊지 말아야 한다.** 74 | 75 | ## 요약 76 | 77 | - 파일 상단에 `// @ts-check`를 추가하면 자바스크립트에서도 타입 체크를 수행할 수 있다. 78 | - 전역 선언과 서드파티 라이브러리의 타입 선언을 추가하는 방법을 익히자. 79 | - JSDoc 주석을 잘 활용하면 자바스크립트 상태에서도 타입 단언과 타입 추론을 할 수 있다. 80 | - JSDoc 주석은 중간 단계이기 떄문에 너무 공들이지 말자. **최종 목표는 .ts로 된 타입스크립트 코드임을 명심하자.** 81 | -------------------------------------------------------------------------------- /ch08_타입스크립트로_마이그레이션하기/item60_혁주.md: -------------------------------------------------------------------------------- 1 | # `allowJs`로 타입스크립트와 자바스크립트 같이 사용하기 2 | 3 | 소규모 프로젝트는 한꺼번에 TS로 전환할 수 있습니다. 4 | 5 | 대규모 프로젝트의 경우 한꺼번에 JS -> TS 작업이 불가능하므로, 마이그레이션 기간 중에 JS와 TS가 동시에 동작할 수 있도록 해야 합니다. 6 | 핵심은 `allowJs` 컴파일러 옵션인데, TS파일과 JS파일을 서로 임포트할 수 있게 해줍니다. 7 | 8 | 자바스크립트 파일은 특별히 건드릴 것이 없습니다. @ts-check 지시자를 추가하기 전까지는 문법 오류 이외에 다른 오류가 발생하지 않습니다. 9 | 타입스크립트는 자바스크립트의 상위집합이기 때문입니다. 10 | 11 | 타입 체크와 관련이 없지만, 기존 **빌드 과정**에 타입스크립트 컴파일러를 추가하기 위해서 `allowJs` 옵션이 필요합니다. 또한 모듈 단위로 TS로 전환하는 과정에서 **테스트를 수행**해야 하기 때문에도 `allowJs`가 필요합니다. 12 | 13 | 번들러에 TS가 통합되어 있거나, 플러그인 방식으로 통합이 가능하다면 `allowJs`를 간단히 적용할 수 있습니다. 예를 들어, `npm install --save-dev tsify`를 실행하고 browserify를 사용하여 플러그인으로 추가해보겠습니다. 14 | 15 | ``` 16 | browserify index.ts -p [ tsify --allowJs ] > bundle.js 17 | ``` 18 | 19 | 대부분의 유닛 테스트 도구에는 동일한 역할을 하는 옵션이 있습니다. 20 | 21 | 예를 들어, jest를 사용할 때 ts-jest를 설치하고 jest.config.js에 전달할 타입스크립트 소스를 지정합니다. 22 | 23 | ```json 24 | module.exports = { 25 | transform:{ 26 | '^.+\\.tsx?$': 'ts-jest', 27 | } 28 | } 29 | ``` 30 | 31 | 만약, 프레임워크 없이 빌드 체인을 직접 구성했다면 복잡한 작업이 필요할 것입니다. 32 | 33 | - 한 가지 방책으로 outDir 옵션을 사용할 수 있습니다. 34 | - outDir 옵션은 TS가 outDir에 지정된 디렉터리에 소스 디렉터리와 비슷한 구조로 JS 코드를 생성하게 되고, outDir로 지정된 디렉터리를 대상으로 기존 빌드 체인을 실행하면 됩니다. 35 | - 참고: 기존 JS 코드에 특별한 규칙이 있었다면, TS가 생성한 코드가 기존 JS 코드의 규칙을 따르도록 출력 옵션을 조정해야 할 수도 있습니다. (ex target, module 옵션) 36 | 37 | 타입스크립트로 마이그레이션하는 동시에 빌드와 테스트가 동작하게 하는 것이 힘들기는 하지만, 제대로 된 점진적 마이그레이션을 시작하기 위해 반드시 필요합니다. 38 | 39 | ### 요약 40 | 41 | - 점진적 마이그레이션을 위해 JS와 TS를 동시에 사용할 수 있게 `allowJs` 컴파일러 옵션을 사용합시다. 42 | - 대규모 마이그레이션 작업을 시작하기 전에, 테스트와 빌드 체인에 TS를 적용해야 합니다. 43 | -------------------------------------------------------------------------------- /ch08_타입스크립트로_마이그레이션하기/item61_dami.md: -------------------------------------------------------------------------------- 1 | # 아이템 61. 의존성 관계에 따라 모듈 단위로 전환하기 2 | 3 | 타입스크립트로 마이그레이션을 할 때는 모듈 단위로 진행하는 것이 이상적입니다. 4 | 5 | 특정 모듈에서 마이그레이션을 진행하는 경우 해당 모듈에 의존성을 가지고 있는 모듈에서 타입 오류가 발생하게 됩니다. 따라서 최하단 모듈부터 작업 후 최상단에 있는 모듈을 마지막으로 완성해야 합니다. 6 | 7 | ## 마이그레이션 과정 8 | 9 | ### @types 모듈 설치하기 10 | 11 | 프로젝트 내 존재하는 모듈은 서드파티 라이브러리에 의존하나 서드파티 라이브러리는 해당 모듈에 의존하지 않기 때문에, 서드파티 라이브러리 타입 모듈을 가장 먼저 설치해야 합니다. 12 | 13 | ### 외부 API의 타입 정보 추가하기 14 | 15 | 외부 API의 타입 정보는 특별한 문맥이 없기 때문에 타입스크립트가 추론하기 어렵습니다. 따라서 API 사양 기반으로 타입 정보를 생성해야 합니다. 16 | 17 | ### 리팩터링 금지 18 | 19 | 마이그레이션할 때는 타입 정보 추가만 하고, 리팩터링을 해서는 안 됩니다. 당장의 목표는 코드 개선이 아니라 타입스크립트로 전환하는 것임을 명심해야 합니다. 20 | 21 | ### 선언되지 않은 클래스 멤버 정의하기 22 | 23 | 자바스크립트는 클래스 멤버 변수를 선언할 필요가 없지만, 타입스크립트에서는 명시적으로 선언해야 합니다. 멤버 변수를 선언하지 않은 클래스가 있는 `.js`파일을 `.ts`로 바꾸면, 참조하는 속성마다 오류가 발생합니다. 24 | 25 | 타입스크립트로 전환하려는 클래스가 너무 많은 속성을 가지고 있어서 충격 받을지도 모릅니다. 자바스크립트 코드를 타입스크립트로 전환하다 보면 잘못된 설계 그래도 타입스크립트로 전환하는 것은 납득하기 어려울 수 있으나 그 과정에서 리팩터링을 하면 안 됩니다. 26 | 27 | ### 타입 단언문으로 임시로 타입 에러 해결하기 28 | 29 | ```typescript 30 | const state = {}; 31 | state.name = "New York"; 32 | // ~~~ '{}' 유형에 'name' 속성이 없습니다. 33 | state.capital = "Albany"; 34 | // ~~~ '{}' 유형에 'capital' 속성이 없습니다. 35 | ``` 36 | 37 | `state` interface 선언 과정에서 임시 방편으로 타입 단언문을 사용할 수 있습니다. 38 | 39 | ```typescript 40 | interface State { 41 | name: string; 42 | capital: string; 43 | } 44 | const state = {} as State; 45 | state.name = "New York"; // 정상 46 | state.capital = "Albany"; // 정상 47 | ``` 48 | 49 | ## JSDoc, @ts-check 사용 시 주의사항 50 | 51 | 자바스크립트 상태에서 `JSDoc`과 `@ts-check`를 사용해 타입 정보를 추가한 상태라면, 타입스크립트로 전환하는 순간 타입 정보가 '무효과' 된다는 것을 주의해야 합니다. 52 | 53 | 이미 존재하는 `JSDoc`의 타입 정보를 활용해 타입 정보를 생성한 후 불필요해진 `JSDoc`을 제거하면 됩니다. 54 | 55 | ## 마지막 단계 - 테스트 코드를 타입스크립트로 전환하기 56 | 57 | 테스트 코드는 항상 의존성 관계도의 최상단에 위치하기에 마이그레이션의 마지막 단계가 되는 것은 자연스러운 일입니다. 58 | 59 | 또한 최하단 모듈부터 타입스크립트로 전환하는 과정에서 코드, 테스트 코드는 변경되지 않았기 때문에 테스트를 수행할 수 있었을 것입니다. 60 | 61 | ## 결론 62 | 63 | - 서드파티 모듈, 외부 API 호출에 대한 `@types` 추가하기 > 최하단 모듈 부터 마이그레이션 64 | - 리팩터링 금지 65 | -------------------------------------------------------------------------------- /ch08_타입스크립트로_마이그레이션하기/item62_jungho.md: -------------------------------------------------------------------------------- 1 | # 아이템62 마이그레이션의 완성을 위해 noImplicitAny 설정하기 2 | 3 | 프로젝트를 ts로 전환 후 마지막 단계로 `noImplicitAny`를 설정해줘야 한다. 4 | 5 | `noImplicitAny`를 설정함으로써 타입 선언에서 비롯되는 실제 오류를 발견할 수 있기 때문이다. 6 | 7 |
8 | 9 | 예를 들어, class를 ts로 전환하며 아이템61에서 다룬 quick fix기능 내 `Add all missing members(누락된 모든 멤버 추가)`를 사용했다고 가정해보자 10 | 11 | ```ts 12 | class Chart { 13 | indices: any; 14 | 15 | // ... 16 | } 17 | ``` 18 | 19 | `indices` 멤버변수의 타입이 추가되었으나, 문맥정보가 `any`타입으로 추론되었다. 20 | 21 | `indices`는 숫자 배열인 것으로 보여 `number[]` 타입으로 수정하고 오류가 사라짐을 확인했다고 가정해보자. 22 | 23 | 하지만 사실 `indices`는 `number[]`의 타입이 아니었다. 24 | 25 |
26 | 27 | 실제로 클래스내 다른 부분에 아래와 같은 함수가 있었고 28 | 29 | ```ts 30 | getRanges() { 31 | for (const r of this.indices) { 32 | const low = r[0]; // 타입이 any 33 | const high = r[1]; // 타입이 any 34 | // ... 35 | } 36 | } 37 | ``` 38 | 39 | 해당 함수를 보면, `indices`는 `number[]`타입이 아닌 `number[][]` 또는 `[number, number][]`의 타입임을 알 수 있다. 40 | 41 | 그러나, `indices`가 `number[]`로 선언되어 있기 때문에, `r`은 `number`타입으로 추론된다. 42 | 43 | `r`이 `number`타입이지만 배열 인덱스 접근에 오류가 발생하지 않는다는 점이 중요하다. 44 | 45 | 이처럼 `noImplicitAny`를 설정하지 않으면, 타입 체크가 **허술**해지는 모습을 볼 수 있다. 46 | 47 |
48 | 49 | `noImplicitAny`를 설정하면 아래와 같이 오류가 발생하게 되고, 실수한 타입 체크를 확인할 수 있게 된다. 50 | 51 | ```ts 52 | getRanges() { 53 | for (const r of this.indices) { 54 | const low = r[0]; // 'Number' 형식에 인덱스 시그니처가 없으므로 요소에 암시적으로 'any'형식이 있습니다. 55 | const high = r[1]; // 'Number' 형식에 인덱스 시그니처가 없으므로 요소에 암시적으로 'any'형식이 있습니다. 56 | // ... 57 | } 58 | } 59 | ``` 60 | 61 | 이처럼 `noImplicitAny`를 사용하면 타입선언과 관련된 실제 오류를 발견할 수 있으나, 처음 마이그레이션을 진행할 때는 `noImplicitAny`를 로컬에만 설정하고 작업하는 것이 좋다. 62 | 63 | 왜냐하면, 원격에도 동일하게 `noImplicitAny`를 설정하고 빌드하게 된다면, 빌드가 실패할 것이기 때문에, 로컬에서만 오류로 인식시키고 오류를 하나씩 수정함으로써, **점진적으로 마이그레이션**을 할 수 있기 때문이다. 64 | 65 |
66 | 67 | 타입체크의 강도를 높이는 설정에는 여러가지가 있는데, `noImplicitAny`는 상당히 엄격한 설정이다. 68 | 69 | `strictNullChecks` 같은 설정을 하지 않더라도 대부분의 타입체크를 적용한 것으로 볼 수 있다. 70 | 71 | 최종적으로 더 강력한 설정도 있는데 `"strict": true`이다. 72 | 73 | 타입 체크의 강도는 팀 내 모든 사람이 타입스크립트에 익숙해진 다음에 조금씩 높이는 것을 권장한다. --------------------------------------------------------------------------------