diff --git a/chapter2/penguin/item12-14.md b/chapter2/penguin/item12-14.md new file mode 100644 index 0000000..b1be905 --- /dev/null +++ b/chapter2/penguin/item12-14.md @@ -0,0 +1,433 @@ +# Item 12 - 함수 표현식에 타입 적용하기 + +> 함수의 "문장" 과 "표현식" 은 다르다. 타입스크립트에서는 "표현식"을 사용하는 것이 좋다. + +함수 표현식을 사용하면, +함수의 **매개변수** 부터 **반환값** 까지 전체를 함수 타입으로 선언하여 함수 표현식에 재사용이 가능하다. + +``` +type DiceRollFn = (sides:number) => number; + +const rollDice: DiceRollFn = sides =>{...} +``` + +함수 표현식을 사용하여 함수 타입을 선언하면 장점으로 + +1. 함수 타입의 선언은 불필요 코드의 반복을 줄일 수있다. + +가령, + +```JS +function add (a:number,b:number){return a + b;} +function minus (a:number,b:number){return a - b;} +function sub (a:number,b:number){return a * b;} +function mul (a:number,b:number){return a / b;} +``` + +이러한 연산 함수 네 개의 타입은 **하나의 함수 타입으로 통합**할 수있다. + +```JS +type BinaryFn = (a:number,b:number)=>number + +const add:BinaryFn = (a,b )=> a+b; +const minus:BinaryFn = (a,b) =>a-b; +const sub:BinaryFn = (a,b) => a*b; +const mul:BinaryFn = (a,b )=> a/b; +``` + +`라이브러리는 공통 함수 시그니처를 타입으로 제공하기도 한다.` 예로, 리액트는 `MouseEvent` 타입 대신에, 함수의 매개변수에 명시하는 타입 대신 함수 전체에 적용할 수있는 `MouseEventHandler` 타입을 제공한다. ( => 와, 리액트에서 마우스 이벤트 핸들러 타입 다는게 귀찮다고만 생각했는데, 이게 리액트에서 제공되는 시그니처 타입이었구나. 몰랐다.) + +2. 시그니처가 일치하는 다른 함수가 있을 때도, 함수 표현식에 타입을 적용하면 이점이 있다. + 비동기 처리하는 fetch 함수가 있을 때, + +```JS +const responseP = fetch("/quote?by=Mark_Twain"); //타입이 Promise + +async function getQuote(){ + +const response = await fetch("/quote?by=Mark_Twain"); +const quote = await response.json(); +return quote; +} + +``` + +이때 만약 /quote 가 **존재하지 않는 API 라면**, JSON 형식이 아니라 "404 NOT FOUND"가 응답되어 올 수있다. 그러면, reponse 는 JSON 형식이 아니라는 오류메시지와 rejected 프로미스를 반환하게 된다. + +또한 **fetch 가 실패한 경우** 거절된 프로미스를 응답하지 않는다. 따라서 상태를 체크해 줄 checkedFetch +함수를 작성해 보자. + +fetch의 타입 선언 + +```JS +declare function fetch( + input:RequestInfo, init?:RequestInit +): Promise; //타입이 Promise +``` + +```JS +async function checkedFetch(input:RequestInfo,init?:RequestInit){ +const response = await fetch(input,init); +if(!response.ok){ +throw new Error("Request failed:" + response.status); +} +return response; +} +``` + +위의 코드를 더 간결하게 표현식을 사용하면.. +checkedFetch 의 타입은 fetch (_typeof 는 피연산자의 평가 전 자료형을 나타내는 문자열을 반환하는데 이렇게도 사용된 것은 어떤 사용례 일지? fetch 의 자료형을 리턴해준..? [MDN-typeof](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/typeof_) => 찾아보니까 49p에서 JS와 TS 의 typeof 의 다른 기능에 대해 언급함 !) + +```JS +const checkedFetch:typeof fetch = async(input,init)=>{ +const response = await fetch(input,init); +if(!reponse.ok){ + throw new Error("Request failed: "_ response.status); +} +return response; +} +``` + +함수 문장을 함수 표현식으로 바꾸고, 함수 전체에 타입 (typeof fetch) 를 적용했다. `이는 타입스크립트가 input과 init 의 타입을 추론할 수있게 해준다.` 또한 함수 checkedFetch 의 반환타입을 보장해준다. +이는 fetch 와 동일하다. (만약, throw 대신 return 을 했으면 TS 는 오류를 잡아낼것.) + +즉, 요약하면 함수 `매개변수` 가 아닌, 함수 `표현식 전체 타입을 정의` 하는 것이 코드적으로 간결하고 안전하다. + +> 즉, 다른 함수의 시그니처와 동일한 타입을 가지는 새 함수가 있는 경우, (동일 타입 시그니처를 가지는 중복의 함수가 있는 경우) 함수 작성시, 함수 전체의 타입 선언을 적용해야 한다. +> ⭐ 다른 함수의 시그니처를 참조하려면 **typeof fn** 을 사용하면 된다. + +# Item 13 - 타입과 인터페이스의 차이점 + +> TS 에서 타입을 정의하는 방법은 type, interface 두 가지가 있다. 두 방법은 어떤 차이가 있을까? + +(추측 : 타입 사용해서 정의한건,, 그냥 일반적인 타입 정의 방법 ? 인터페이스는 자바에서 본 건데,,음,,,어떤 공용으로 사용되는 타입을 정의할때 쓰는거 아닐까..?) + +```JS +//type 을 사용한 타입 정의 +type TState = { + name: string; + capital: string; +} + +//interface 를 사용한 타입 정의 +interface IState { + name:string; + capital:string; +} +``` + +대부분의 경우, 타입, 인터페이스 둘 중 선택해서 사용해도 된다. 그러나 둘의 차이를 분명하게 알고, +같은 상황에서는 동일한 방법으로 명명된 타입을 정의해 일관성을 유지 하도록 하자. +( \*아, 참고로 위의 예제에서 T와 I 가 접두사로 붙는 것은 C#에서 비롯된 관례이나, 요즘엔 지양해야 할 스타일이다. ) + +먼저 타입과 인터페이스의 **유사점은** 다음과 같다. + +1. [[인덱스 시그니처]]는 인터페이스와 타입에서 모두 사용할 수있습니다 (? 인덱스 시그니처가 뭐지) + +```JS +type TDict = {[key:string]:string}; + +interface IDict{ + [key:string]:string; +} +``` + +2. 함수 타입도 타입, 인터페이스 둘다 정의 가능하다. + +```JS +type TFn = (x:number)=> string; +interface IFn{ + (x:number):string +} + +const toStrT:Tfn = x => '' + x; +const toStrI:Ifn = x => '' + x; +``` + +3. 함수 타입에 추가적인 속성이 있다면 , 타입이나 인터페이스 어떤 것을 선택하든 차이가 없다. + +```JS +type TFnWithProperties = { + (x:number):number; + prop:string; +} + +interface IFnWithProperties{ + (x:number):number; + prop:string; +} +``` + +문법이 낯설지만, 자바스크립트에서 함수는 호출 가능한 `객체` 라는 점을 염두하면 이해할 수있다. + +4. 타입 별칭과 인터페이스는 모두 [[제너릭]]이 가능하다. + +```JS +type TPair={ + first:T; + second:T; +} + +interface IPair{ + first:T; + second:T; +} +``` + +5. 인터페이스는 타입을 확장할 수있으며, 타입은 인터페이스를 확장 할 수있다. + +```JS +interface IStateWithPop extends Tstate{ + population:number; +} +type TStateWithPop = IStateWithPop & {population:number} //책에는 IState로만 되어있는데 IStateWithPop 맞겠지? +``` + +그러나 인터페이스는 유니온 타입과 같은 복잡한 타입은 확장하지 못한다. 복잡한 타입을 확장하고싶다면, 타입과 & 를 사용해야 한다. + +6. 클래스를 구현(implements) 할 때는 타입과, 인터페이스 둘 다 사용이 가능하다. + +```JS +class StateT implements TState{ + name:string=""; + capital:string =""; +} + +class StateI implements IState{ + name:string=""; + capital:string=""; +} +``` + +타입과 인터페이스의 **차이점** 들은다음과 같다. + +1. 유니온 타입은 있지만, 유니온 인터페이스라는 개념은 없다. + +```JS +type AorB = "A" | "B" +``` + +인터페이스 또한 타입을 확장할 수는 있다. 그러나 유니온은 할 수 없다. +즉, + +```JS +type Input={} +type OutPut ={} + +interface VariableMap{ + [name:string] : Input | Output +} +``` + +Input, Output 타입을 하나의 변수명으로 매핑하는 VariableMap 인터페이스 +(name이 string 형이고 input, output, 객체타입 갖는..? TS 코드 구현을 많이 안해봐서 +그런지 코드 이해가 잘 안된다.) + +```JS +type NameVariable = (Input | Output) & { name:string }; +``` + +타입으로 하면, 유니온 타입에다가 name 이라고 하는 속성을 또 붙인 타입을 만들 수있다. + +이러한 표현은 인터페이스로는 불가능한다. 즉, type 으로 타입을 선언하는 것이, 일반적인 경우에서 interface 를 통한 정의보다 쓰임새가 많다. + +> type 사용의 장점 + +1. union 이 가능하고, +2. [[매핑된 타입]] 또는 조건부 타입 같은 고급 기능에 활용할 수있다. +3. [[튜플]]과 [[배열]] 타입도 type 키워드로 더 간결하게 표현이 가능하다. (인터페이스로도 튜플 구현이 가능하나 concat 같은 메서드 사용이 불가능하다. 따라서 type 으로 구현해주는 것이 좋다.) + +```JS +type Pair = [number,number]; +type StringList = string[]; + +type NamedNums = [string,...number[]]; +``` + +위처럼 [[튜플]] 을 type 을 사용하면 간결하게 표현 할 수있는데 반해 +인터페이스로는 다음과 같이 구현해야 한다. 또한 이렇게 구현해도 **concat 과 같은 메서드들은 사용할 수없다.** + +```JS +interface Tuple{ + 0:number; + 1:number; + length:2; +} + +const t:Tuple = [10,20] +``` + +그럼 interface 로 타입정의할 때의 장점은 무엇일까? + +1. 보강(argument)이 가능하다. (타입엔 없는 기능.) + +```JS +interface IState{ + name:string; + capital:string +} + +interface IState{ + population:number; +} + +const penguin:IState ={ + name:"penguin", + capital:"North" + population:500_000 +}; +``` + +위처럼 속성 확장하는 것을 `[[선언 병합(declaration merging)]]` 이라고 한다. 선언 병합을 지원하기 위해서 반드시 인터페이스를 사용해야한다. + +타입 선언할때, `사용자가 채워야 하는 빈틈` 이 있다면 선언 병합을 사용하여 채울 수있다. +(<- 오 이런 점 궁금했었는데 인터페이스로 하면 해결이 되는구나! ) + +타입은 기존 타입에 추가적인 보강이 없는 경우에만 사용해야 한다. + +결론적으로... + +- 복잡한 타입이라면 ? -> [[타입 별칭]]을 사용하면 된다. +- 간단한 객체 타입이라면? -> 일관성과 보강의 관점에서 고려해 봐야 한다. (일관적으로 인터페이스를 사용하는 중이라면 인터페이스를, 타입을 사용중이라면 타입을 사용하면 된다.) + +예를 들어, API 에 대한 타입 선언을 작성해야 한다면, 인터페이스를 사용하는 것이 좋다. +API 변경시 인터페이스를 통해 새로운 필드를 병합 할 수있기 때문임. 그러나 `프로젝트 내부적으로 사용되는 타입에 선언 병합이 생기는 것은 잘못된 설계`이다. 이럴 땐 타입을 사용하자. + +# Item 14 - 타입 연산과 제너릭 사용으로 반복 줄이기 + +함수, 상수 , 루프의 반복을 제거하는 법을 알아보자. + +```JS +console.log("Cylinder 1x1","surface area:",6.2424*1*1+6.2423*1*1, + "Volume:",3.14*1*1*1); + +console.log("Cylinder 1x2","surface area:",6.2424*1*1+6.2423*2*1, + "Volume:",3.14*1*2*1); + +console.log("Cylinder 2x1","surface area:",6.2424*1*1+6.2423*1*1, + "Volume:",3.14*2*2*1); + +``` + +위의 원기둥 높이 표면적 부피를 출력하는 콘솔 코드를 + +반복을 제거하여 다음과 같이 작성할 수있다. + +```JS +const surfaceArea=(r,h)=> 2*Math.PI*r*(r+h); +const volume = (r,h)=>Math.PI*r*r*r*h; + +for(const [r,h] of [[1,1],[1,2],[2,1]]){ + console.log( + `Cylinder ${r}x ${h}`, + `Surface area: ${surfaceArea(r,h)}`, + `Volume: ${volume(r,h)}`); + ) +} +``` + +이렇게 개발자들이 코드의 중복을 제거하는 것 처럼, 타입에서도 또한 중복을 제거하는 것이 바람직하다. + +예를들어, + +```JS +function distance(a:{x:number,y:number},b:{x:number,y:number}){ + return Math.sqrt(Math.pow(a.x-b.x,2))+Math.pow(a.y-b.y,2); +} +``` + +위와 같은 타입 중복은 + +```JS +interface Point2D{ + x:number; + y:number; +} + +function distance(a:Point2D,b:Point2D){...} + +``` + +이런 식으로 반복되는 타입 중복을 줄일 수있다. + +또한 한 인터페이스가 다른 인터페이스를 `확장`하게 함으로써 반복을 제거 할 수있다. + +```JS +interface Person{ + firstName:string; + lastName:string; +} + +interface PersonWithBirthDate extends Person{ + birth:Date; +} +``` + +또한 이미 존재하는 타입 확장의 경우 인터섹션 연산자를(&) 쓸 수도 있다. (일반적이진 않음) + +```JS +type PersonWithBirthDate = Person & {birth:Date}; +``` + +이 방법은 확장할 수없는 유니온 타입에 속성 추가를 하려고 할때 유용하다. + +- ⭐ 더 큰 집합을 인덱싱 하여 속성의 타입에서 중복을 제거하는 법을 알아보자 + +```JS +interface State{ + userId:string; + pageTitle:string; + recentFiles:string[]; + pageContents:string; +} +``` + +```JS +interface TopNavState{ + userId:string; + pageTitle:string; + recentFiles:string[]; +} +``` + +위처럼 State의 타입의 [[부분집합]] 타입을 갖고 있는 TobNavState 가 있을 때, 어떻게 중복을 줄일 수있을까? + +State 를 **인덱싱** 하여 속성의 타입에서 중복을 제거할 수있다. (너무 신기해욘...) + +```JS +type TopNavState = { + userId:State['userId']; + pageTitle:State['pageTitle']; + recentFiles:State['recentFiles']; +} +``` + +이를 더욱 발전시켜보면, **매핑된 타입**을 사용할 수도 있다. + +```JS +type TopNavState = { + [k in 'userId'|'pageTitle'|'recentFiles']:State[k] +} +``` + +[[매핑된 타입]]은 배열 필드를 루프 도는 것과 같은 방식이며, 표준 라이브러리에서도 많이 쓰이는 패턴으로 **Pick** 이라고도 한다. (신기신기) + +```JS +interface SaveAction{ + type:"save"; +} + +interface LoadAction{ + type:"load"; +} + +type Action = SaveAction | LoadAction; + +type ActionType = 'save'|'load'; //타입의 반복. + +// 💡 Action 유니온을 인덱싱 해주면 타입 반복 없이 ActionType을 정의가능하다. + +type ActionRec = Pick //{type:'save'|'load'} + + +``` diff --git a/chapter2/penguin/item15-18.md b/chapter2/penguin/item15-18.md new file mode 100644 index 0000000..2d3535d --- /dev/null +++ b/chapter2/penguin/item15-18.md @@ -0,0 +1,195 @@ +# Item 15 - 동적 데이터에 인덱스 시그니처 사용하기 +> JS 의 장점. 객체를 생성하는 문법이 간단하다. 그래서 인덱스 시그니처도 가능하다..하지만..? + +JS 객체는 문자열 키를 `타입의 값과 관계없이 매핑이 가능`하다. +이러한 속성으로 인해 타입스크립트에서 **인덱스 시그니처** 를 명시하여 유연하게 매핑을 표현할 수 있다. +그러나 인덱스 시그니처를 사용한 타입 체크에는 단점도 존재한다. + +인덱스 시그니처의 예 +```JS +type Rocket = {[property:string]:string}; +``` +- 키의 이름 - property : 키의 이름. 키의 위치만 표시함. +- 키의 타입 - string 이나 number 또는 symbol 의 조합. 보통은 string 사용. +- 값의 타입 - 어떤 것이든 가능 + +인덱스 시그니처 타입의 사용 예 +```JS +const rocket:Rocket ={ +name:'Peng', +species:'bird', +habitat:'antartic' +} +``` + +- 인덱스 시그니처의 단점 + - property에 모든 키가 들어갈 수 있음 (단순히 위치만 표시하는 역할이기 때문에) + - 특정 키가 필요하지 않음. {} 도 유효한 Rocket 타입임. + - 키마다 다른 타입을 갖는 것이 불가능하다. 위의 예시에서 보면 string값만 사용가능함 + - 키가 무엇이든 사용할 수 있기 때문에, 자동 완성 기능이 동작 안함. + + +인덱스 시그니처의 적절한 사용 - 동적인 데이터의 타입을 지정해 줘야 할때 고려해 볼만하다 +- 런타임 때까지 객체의 속성을 알 수없는 경우에 사용하자. +- 인덱스 시그니처는 동적 데이터를 표현할 때 사용하자. 동적인 값이 들어와서 저장해서 사용하고 싶을 때. 인덱스 시그니처를 사용하거나 값 타입에 undefined 를 추가할 수 있다. + +#❓ +> 연관 배열(associative arr)의 경우 인덱스 시그니처 대신 Map타입 사용을 고려 가능. +> 이는 프로토타입 체인과 관련된 문제를 우회한다 <- 이해안가지만..일단 여기까지... + + +인덱스 시그니처의 대안 +- 선택적 필드, 유니온 타입으로 모델링, 인터페이스 +- [Record](https://typescript-kr.github.io/pages/utility-types.html#recordkt) 를 사용 + - Record 는 키 타입에 유연성을 제공하는 제너릭 타입. + - 타입의 프로퍼티들을 다른 타입에 매핑하는데 사용할 수있다. + +- 매핑된 타입을 사용하는 방법. + - 키마다 별도의 타입을 사용 할 수있음 + +```JS +type MappingType ={[K in 'x'|'y'|'z']:number}; + +type ABC = {[k in 'a'|'b'|'c']: k extends 'b'?string:number}; +``` + +**🔹 결론** +- 타입에 필드가 제한 & 지정되어있는 경우 인덱스 시그니처로 타입 모델링 하지 말자. +- 키가 몇개인지 모른다면, **선택적 필드** 혹은 **유니온 타입**으로 모델링 할 것. +- 안전한 접근을 위해 인덱스 시그니처 값 타입에 undefined 추가하는 것 고려하기. + +--- + +# Item 16 - number 인덱스 시그니처보다 Array, 튜플, ArrayLike 사용하기. +> 이상한 자바스크립트.. 암시적 타입 강제같은게 있다니.. +> 이것 때문에 객체 모델에 대한 이해가 필요할 수도있음. (아이템 10) + +자바스크립트에서 배열의 키를 나열해보면 키가 문자열로 출력이됨. +```JS +x = [1,2,3] +Object.keys(x) + +=> 출력 결과 +['1','2','3'] 일줄 알았는데 ㅋ + +키 값이라 +['0', '1', '2'] 이렇게 나오넹 인덱스값? 인듯 + +배열도 객체..(키/밸류) +``` + +타입스크립트에선, 숫자 키를 허용하고 문자열 키와 둘을 구분함. => Array에 대한 타입 선언 + +인덱스에 신경쓰지 않으면 for in 보다 for of 를 사용 하는것이 더 좋다. +타입이 불확실하면 for-in 루프는 for-of , for-루프에 비해 성능이 안좋다. + +⭐ 인덱스 시그니처가 number로 되어있으면 입력값은 number 가 되어야하지만... +실제 JS 엔진이 키를 string 으로 변환하는 식으로 동작함. + +길이를 가지는 배열과 비슷한 형태의 튜플을 사용하고 싶으면, TS 의 ArrayLike 타입을 사용할 것. (물론 ArrayLike 사용해도 키가 여전히 문자열임.) + +**🔹 결론** +- 배열이 객체여서 키가 숫자가 아니라 문자열임. +- 인덱스 시그니처에 number 사용보다는 Array, 튜플, 또는 ArrayLike 타입 사용하는게 좋음. +--- + +# item 17 - 변경 관련된 오류 방지를 위해 readonly 사용하기 + +>자바스크립트의 배열은 내용을 변경할 수있기 때문에, arraySum이 배열을 변경하지 않는다는 접근 제어자 readonly 를 사용하면 어떻게 될까... + +- 삼각수를 출력하는 함수 +```JS +function printTri(n:number){ + +const nums=[]; + +for(let i=0;i void; +} + +const REQUIRES_UPDATE: { [k in keyof ScatterProps]: boolean } = { + xs: true, + ys: true, + xRange: true, + yRange: true, + color: true, + onClick: false, +}; + +function shouldUpdate(oldProps: ScatterProps, newProps: ScatterProps) { + let k: keyof ScatterProps; + for (k in oldProps) { + if (oldProps[k] !== newProps[k] && REQUIRES_UPDATE) { + return true; + } + } + return false; +} +``` + + +**매핑된 타입**은 한 객체가 또 다른 객제와 정확히 같은 속성을 가지게 할 때 이상적이다. **인터페이스에 새로운 속성을 추가할 때, 선택을 강제하도록 매핑된 타입을 사용할 것인지 고려**해야 한다. + +**🔹 결론** +- 매핑된 타입을 사용해서 관련된 값과 타입을 동기화하도록 한다. +- 인터페이스에 새로운 속성을 추가할 때, 선택을 강제하도록 매핑된 타입을 고려하자. + +---