From 7ebfb945322610a1eb3614f106a6d6f2d0f7469f Mon Sep 17 00:00:00 2001 From: perfumelim Date: Thu, 7 Apr 2022 20:38:33 +0900 Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=93=9D=20docs:=20Write=20down=20the?= =?UTF-8?q?=20contents=20of=20item6=20-=208?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter2/perfume/item6-8.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chapter2/perfume/item6-8.md b/chapter2/perfume/item6-8.md index c47a2ae..7a707ca 100644 --- a/chapter2/perfume/item6-8.md +++ b/chapter2/perfume/item6-8.md @@ -1,4 +1,4 @@ -## 타입스크립트의 타입 시스템 +## ✨ 타입스크립트의 타입 시스템 오늘날 자바스크립트는 어엿한 모던 프로그래밍 언어로 자리 잡았습니다. 자바스크립트 프로그래머들을 고통스럽게 하던 타입 불안정성 문제 역시 타입스크립트라는 강력한 해결책의 등장으로 안정되었습니다. 타입스크립트의 등장이 자바스크립트의 완성도를 높이는 신의 한 수가 된 셈입니다. @@ -6,7 +6,7 @@ 이번 글에서는 타입스크립트의 타입 시스템을 기초부터 살펴보도록 하겠습니다. -### 타입이 값들의 집합이라고 생각하기 +### 💡 타입이 값들의 집합이라고 생각하기 자바스크립트의 런타임에는 단 6개의 타입만 존재합니다. 하지만 타입스크립트에는 무수히 많은 종류의 타입이 있습니다. 게다가 리터럴 타입과 유니온 타입, extends 키워드 같은 낯선 개념이 타입에 대한 이해의 걸림돌이 되기도 합니다. 이를 보다 쉽게 이해하기 위해서, 타입을 값의 집합이라고 생각해봅시다. From b6f6a918dcd23085d0db82f4340073a06311000a Mon Sep 17 00:00:00 2001 From: perfumelim Date: Fri, 8 Apr 2022 23:48:07 +0900 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=93=9D=20docs:=20Write=20down=20the?= =?UTF-8?q?=20contents=20of=20item6=20-=208?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter2/perfume/item6-8.md | 49 ++++++++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/chapter2/perfume/item6-8.md b/chapter2/perfume/item6-8.md index 7a707ca..3a8bf2f 100644 --- a/chapter2/perfume/item6-8.md +++ b/chapter2/perfume/item6-8.md @@ -4,14 +4,57 @@ 그렇다면 타입스크립트가 어떻게 자바스크립트의 타입 불안정성을 해결했을까요? 간단합니다. '타입'스크립트라는 이름처럼, 타입스크립트 컴파일러는 자바스크립트 코드의 타입 오류를 체크합니다. -이번 글에서는 타입스크립트의 타입 시스템을 기초부터 살펴보도록 하겠습니다. +이번 글에서는 타입스크립트의 타입 시스템을 이해하는 방법을 간단하게 요약해서 설명하겠습니다. (자세한 내용은 책 '이펙티브 타입스크립트'를 참고하세요.) ### 💡 타입이 값들의 집합이라고 생각하기 -자바스크립트의 런타임에는 단 6개의 타입만 존재합니다. 하지만 타입스크립트에는 무수히 많은 종류의 타입이 있습니다. 게다가 리터럴 타입과 유니온 타입, extends 키워드 같은 낯선 개념이 타입에 대한 이해의 걸림돌이 되기도 합니다. 이를 보다 쉽게 이해하기 위해서, 타입을 값의 집합이라고 생각해봅시다. +자바스크립트의 런타임에는 단 6개의 타입만 존재합니다. 하지만 타입스크립트에는 무수히 많은 종류의 타입이 있습니다. 게다가 리터럴 타입과 유니온 타입, 타입의 상속(extends) 같은 낯선 개념이 타입에 대한 이해의 걸림돌이 되기도 합니다. 이를 보다 쉽게 이해하기 위해서, 타입을 값의 집합이라고 생각해봅시다. 36.5, 7, 11, 8 등의 숫자는 number라는 집합에 들어갈 수 있습니다. 하지만 'cat'이라는 string 값은 number라는 집합에 들어갈 수 없겠죠. 집합이라고 표현했지만, 타입의 '범위'라고 이해하셔도 좋을 것 같네요. 이와 같은 관점에서 봤을 때, 타입 체커의 주요 역할은 하나의 집합이 다른 집합의 부분 집합인지 검사하는 것이라고 볼 수 있습니다. -타입스크립트의 타입은 엄격한 상속 관게가 아니라, 겹쳐지는 집합으로 표현됩니다. +타입스크립트의 타입은 엄격한 상속 관계가 아니라 겹쳐지는 집합으로 표현됩니다. 두 타입은 서로 서브타입이 아니면서도 겹쳐질 수 있습니다. + +타입 연산 역시 값의 집합(타입의 범위)에 적용됩니다. 또한 추가적인 속성을 가지는 값도 여전히 그 타입에 속합니다. 이 부분에선 간단한 예시 코드를 보겠습니다. + +``` +interface Person { +name: string; +} + +interface Lifespan { +birth: Date; +death?: Date; +} + +type PersonSpan = Person & Lifespan; +``` + +언뜻 봤을 때는 Person과 Lifespan 인터페이스는 공통으로 가지는 속성이 없기 때문에 PersonSpan 타입이 공집합(never 타입)처럼 보일 수 있습니다. 하지만 앞서 말했듯 타입 연산자는 값의 집합에 적용되기 때문에 아래와 같은 코드가 정상적으로 작동합니다. + +``` +const ps: PersonSpan = { +name: 'Hedy Lamarr', +birth: new Date('1914/11/09'), +death: new Date('2000/01/19'). +}; +``` + +앞의 세 가지보다 더 많은 속성을 가지는 값도 PersonSpan 타입에 속합니다. + +타입이 집합이라는 관점은 이외의 다른 개념들도 명확하게 이해할 수 있도록 돕습니다. 'A는 B를 상속', 'A는 B에 할당 가능', 'A는 B의 서브타입' 등의 말은 결국 A는 B의 부분 집합이라는 뜻입니다. + +### 💡 타입 공간과 값 공간의 심벌 구분하기 + +타입스크립트 코드를 읽을 때 타입인지 값인지 구분하는 방법을 터득해야 합니다. 타입스크립트의 심벌(symbol)이 타입 공간이나 값 공간 중 한 곳에 존재하기 때문입니다. 이름이 같더라도 심벌이 속하는 공간에 따라 다른 것을 나타낼 수 있기 때문에 이 부분이 혼란스러울 수 있습니다. + +모든 값은 타입을 가지지만, 타입은 값을 가지지 않습니다. type과 interface 같은 키워드는 타입 공간에만 존재합니다. + +class나 enum 같은 키워드는 타입과 값 두 가지로 사용될 수 있습니다. + +> 💛 조금 다른 이야기인데요, 타입스크립트에서 enum을 되도록 사용하지 않는 것이 좋다고 합니다. 이에 관한 자세한 내용은 [이 글](https://engineering.linecorp.com/ko/blog/typescript-enum-tree-shaking/)을 참조하세요. + +typeof, this 그 외의 여러 연산자들과 키워드들은 타입 공간과 값 공간에서 다른 목적으로 사용될 수 있습니다. + +그래서 타입스크립트 코드가 잘 동작하지 않는다면 타입 공간과 값 공간을 혼동해서 잘못 작성했을 가능성이 큽니다. 이를 구분하는 방법을 터득하기 위해서 타입스크립트 플레이그라운드를 활용해 개념을 잡으면 좋을 것 같네요! From 47d21020117f8ab3327a45b2c29947ab908a1771 Mon Sep 17 00:00:00 2001 From: perfumelim Date: Sat, 30 Apr 2022 20:06:49 +0900 Subject: [PATCH 3/5] =?UTF-8?q?=F0=9F=93=9D=20docs:=20Write=20down=20the?= =?UTF-8?q?=20contents=20of=20item09=20-=2011?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter2/perfume/item9-11.md | 51 ++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 chapter2/perfume/item9-11.md diff --git a/chapter2/perfume/item9-11.md b/chapter2/perfume/item9-11.md new file mode 100644 index 0000000..8d8fca2 --- /dev/null +++ b/chapter2/perfume/item9-11.md @@ -0,0 +1,51 @@ +### 💡 타입 단언보다는 타입 선언을 사용하기 + +지난 글에서 타입 스크립트를 사용하는 이유는 타입 안정성을 높이기 위해서라고 말한 적이 있습니다. 보다 높은 타입 안정성을 위해, 타입 단언보다는 타입 선언을 사용하는 것이 좋습니다. 왜 그런지 아래 코드를 함께 살펴보며 확인해봅시다. + +``` +interface Cat {name: string}; + +const cherie: Cat = {name: 'Cherie'}; // 타입은 Cat +const homie = {name: 'Homie'} as Cat; // 타입은 Cat +``` + +첫 번째 cherie: Cat은 타입 선언입니다. 변수에 타입 선언을 붙여서 그 값이 선언된 타입임을 명시하는 방법이죠. 두 번째 as Cat은 타입 단언입니다. 말 그대로 타입을 '단언'했기 때문에 타입 스크립트가 추론한 타입이 있더라도 그걸 무시하고 Cat 타입으로 간주합니다. 그래서 아래와 같은 일이 일어납니다. + +``` +const cherie: Cat = {}; +``` + +위 코드를 작성할 경우 'Cat' 유형에 필요한 'name' 속성이 없다는 에러 메시지가 뜹니다. 타입 스크립트가 할당되는 값이 해당 인터페이스를 만족하는지 검사했기 때문입니다. 반면 아래 코드는 에러가 발생하지 않습니다. + +``` +const homie = {} as Cat; +``` + +강제로 타입을 지정했기 때문에 타입 체커가 오류를 무시한 것입니다. 이러한 이유로 타입스크립트보다 타입 정보를 더 잘 알고 있는 상황에만 타입 단언문을 사용하는 것이 좋습니다. + +#### ➕ 화살표 함수의 반환 타입 선언 + +화살표 함수의 타입 선언이 다소 까다롭기 때문에 따로 다뤄보겠습니다. 화살표 함수 안에서 타입과 함께 변수를 선언하는 것이 가장 직관적입니다. + +``` +const cats = ['cherie', 'homie', 'honey'].map(name=> { +const cat:Cat = {name}; +return cat +}); +``` + +그러나 이 방식은 조금 번잡해보인다는 단점이 있습니다. 코드를 좀 더 간결하게 만들어보겠습니다. + +``` +const cats:Cat[] = ['cherie', 'homie', 'honey'].map( +(name):Cat -> ({name}) +); +``` + +💥 **Boom!** 우리가 원하는 타입을 직접 명시하고, 타입스크립트가 할당문의 유효성을 검사하게 만들었습니다. + +### 💡 잉여 속성 체크의 한계 인지하기 + +타입이 명시된 변수에 객체 리터럴을 할당할 때 타입스크립트는 해당 타입의 속성이 있는지, 그리고 '그 외의 속성은 없는지' 확인합니다. 이를 잉여 속성 체크라고 부르는데요, 잉여 속성 체크 역시 조건에 따라 동작하지 않는다는 한계가 있고, 일반적인 할당 가능 검사와 함께 쓰이면 구조적 타이핑이 무엇인지 혼란스러워질 수 있습니다. **잉여 속성 체크는 할당 가능 검사와는 별도의 과정이라는 것**을 기억하세요. + +잉여 속성 체크는 구조적 타이핑 시스템에서 허용되는 속성 이름의 오타 같은 실수를 잡는 데 효과적인 방법입니다. 하지만 적용 범위가 매우 제한적이며 오직 객체 리터럴에만 적용됩니다. 임시 변수를 도입하면 잉여 속성 체크를 건너뛸 수 있다는 점을 기억해야 합니다. From bbc9cecf6e1b69f285b1549b17376bf1b6a328e7 Mon Sep 17 00:00:00 2001 From: perfumelim Date: Sat, 30 Apr 2022 21:47:04 +0900 Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=93=9D=20docs:=20Write=20down=20the?= =?UTF-8?q?=20contents=20of=20items=2012-14?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter2/perfume/item12-14.md | 132 ++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 chapter2/perfume/item12-14.md diff --git a/chapter2/perfume/item12-14.md b/chapter2/perfume/item12-14.md new file mode 100644 index 0000000..c811b6f --- /dev/null +++ b/chapter2/perfume/item12-14.md @@ -0,0 +1,132 @@ +## 💡 함수 표현식에 타입 적용하기 + +자바스크립트와 타입스크립트에서는 함수 '문장(statement)'과 함수 '표현식(expression)'을 다르게 인식합니다. + +``` +function rollDice1(sides:number): nimber {} +``` + +위 코드는 문장(statement)입니다. 반면 아래 코드들은 함수 표현식입니다. + +``` +const rollDice2 = function(sides: number): number {}; +const rollDice3 = (sides: number) : number => {}; +``` + +타입스크립트에서는 함수 표현식을 사용하는 것이 좋습니다. 함수의 매개변수부터 반환값까지 전체를 함수 타입으로 선언하여 함수 표현식에 재사용할 수 있기 때문입니다. + +type DiceRollFn = (sides: number) => number; +const rollDice: DiceRollFn = sides => {}; + +sides에 마우스를 올려 보면 이미 타입스크립트가 sides의 타입을 number로 인식하고 있다는 걸 알 수 있습니다. + +함수 타입의 선언은 불필요한 코드의 반복을 줄입니다. + +``` +function add(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}; +function div(a:number, b: number) {return a / b}; + +``` + +위의 코드처럼 반복되는 함수 시그니처를 일일이 적어줄 필요 없이 하나의 함수 타입으로 통합할 수도 있습니다. + +``` +type BinaryFn = (a:number, b: number) => number; +const add: BinaryFn = (a,b) => a+b; +const sub: BinaryFn = (a,b) => a-b; +const mul: BinaryFn = (a,b) => a*b; +const div: BinaryFn = (a,b) => a/b; +``` + +이처럼 함수의 매개변수에 타입 선언을 하는 것보다 함수 표현식 전체 타입을 정의하는 것이 코드도 간결하고 안전합니다. 다른 함수의 시그니처와 동일한 타입을 가지는 새 함수를 작성하거나, 동일한 타입 시그니처를 가지는 여러 개의 함수를 작성할 때는 매개변수의 타입과 반환 타입을 반복해서 작성하지 말고 함수 전체의 타입 선언을 적용해야 합니다. + +## 💡타입과 인터페이스의 차이점 알기 + +타입스크립트에서 명명된 타입(named type)을 정의하는 두 가지 방법이 있습니다. + +1. type + +``` +type TState = { +name: string; +capital: string; +} +``` + +2. interface + +``` +interface Istate { +name: string; +capital: string; +} +``` + +대부분의 경우 두 가지 중 어느 것을 사용해도 상관 없습니다. 하지만 둘의 차이를 분명하게 알고, 같은 상황에서는 동일한 방법으로 명명된 타입을 정의해 일관성을 유지해야 합니다. + +그럼 둘의 차이가 뭘까요? + +인터페이스는 유니온 타입 같은 복잡한 타입을 확장하지는 못한다는 것입니다. 복잡한 타입을 확장하고 싶다면 타입과 &을 사용해야 합니다. 유니온 타입은 있지만, 유니온 인터페이스라는 개념은 존재하지 않죠. 그래서 type 키워드는 일반적으로 interface보다 쓰임새가 많습니다. type 키워드는 유니온이 될 수도 있고, 매핑된 타입 또는 조건부 타입 같은 고급 기능에 활용되기도 합니다. + +대신 인터페이스에는 타입에 없는 몇 가지 기능이 있습니다. 그중 하나가 바로 보강(augment)이 가능하다는 것입니다. 아까 interface를 설명할 때 등장했던 예제에 population 필드를 추가할 때 보강 기법을 사용할 수 있습니다. + +``` +interface Istate { +name: string; +capital: string; +} + +interface Istate { +population: number; +} + +const wyoming: IState = { +name: 'Wyoming', +capital: 'Cheyenne', +population: 500_000 +}; + +``` + +이 예제처럼 속성을 확장하는 것을 '선언 병합(declaration merging)'이라고 합니다. 선언 병합을 지원하기 위해서는 반드시 인터페이스를 사용해야 합니다. + +자, 그럼 타입과 인터페이스 중 어느 것을 사용하는 게 좋을까요? + +복잡한 타입이라면 고민할 것도 없이 타입 키워드를 사용하면 됩니다. 그러나 두 가지 방법으로 모두 표현할 수 있는 간단한 객체 타입이라면 일관성과 보강의 관점에서 고려해 봐야 합니다. 기존의 코드베이스에서 사용하는 키워드를 쓰는 것이 좋습니다. + +## 💡 타입 연산과 제너릭 사용으로 반복 줄이기 + +같은 코드를 반복하지 말라는 DRY(don't repeat yourself) 원칙이 있습니다. 이 원칙은 타입에도 적용됩니다. 같은 타입을 반복해서 적어줄 필요는 없겠죠! 반복을 줄이는 가장 간단한 방법은 타입에 이름을 붙이는 것입니다. 예를 들어, 아래 코드처럼 몇 개의 함수가 같은 타입 시그니처를 공유하고 있다고 해 보겠습니다. + +``` +function get(url:string, opts: Options) : Promise {} +function post(url:string, opts: Options) : Promise {} +``` + +위와 같은 경우 해당 시그니처를 명명된 타입으로 분리할 수 있습니다. + +``` +type HTTPFunction = (url:string, opts: Options) => Promise; +const get: HTTPFunction = (url, opts) => {}; +const post: HTTPFunction = (url, opts) => {}; + +``` + +한 인터페이스가 다른 인터페이스를 확장하게 해서 반복을 제거할 수도 있습니다. + +``` +interface Person { +firstName: string; +lastName: string; +} + +interface PersonWithBirthDate extends Person { +birth: Date; +} +``` + +이처럼 extends를 사용하면 인터페이스 필드의 반복을 피할 수 있습니다. + +뿐만 아니라 우리에게는 제너릭이 있습니다. 제너릭 타입은 타입을 위한 함수와 같습니다. 그리고 함수는 코드에 대한 DRY 원칙을 지킬 때 유용하게 사용됩니다. 따라서 타입의 반복을 줄이는 핵심에 제너릭이 있습니다. 타입을 반복하는 대신 제너릭 타입을 사용하여 타입들 간에 매핑을 하는 것이 좋습니다. 그런데 함수에서 매개변수로 매핑할 수 있는 값을 제한하기 위해 타입 시스템을 사용하는 것처럼, 제너릭 타입에서 매개변수를 제한할 수 있는 방법이 필요합니다. 그 방법은 바로 extends입니다. extends를 이용하면 제너릭 매개변수가 특정 타입을 확장한다고 선언할 수 있습니다. From f6b53bfb54ddf1f259bbda20f461d1372c0ec75b Mon Sep 17 00:00:00 2001 From: perfumelim Date: Sun, 1 May 2022 19:53:51 +0900 Subject: [PATCH 5/5] =?UTF-8?q?=F0=9F=93=9D=20docs:=20Write=20down=20the?= =?UTF-8?q?=20contents=20of=20items=2015-18?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter2/perfume/item15-18.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 chapter2/perfume/item15-18.md diff --git a/chapter2/perfume/item15-18.md b/chapter2/perfume/item15-18.md new file mode 100644 index 0000000..337d244 --- /dev/null +++ b/chapter2/perfume/item15-18.md @@ -0,0 +1,25 @@ +## 💡 동적 데이터에 인덱스 시그니처 사용하기 + +런타임 때까지 객체의 속성을 알 수 없을 경우에만 인덱스 시그니처를 사용하는 것이 좋습니다. + +#### 🤔 인덱스 시그니처란? + +인덱스 시그니처는 {[키의 이름: 키의 타입]: 값의 타입} 과 같은 형태를 가진 타입 문법을 말합니다. 유연한 매핑을 표현할 수 있다는 장점을 가지고 있죠. 하지만 유연한만큼 잘못된 키를 포함한 모든 키를 허용한다는 단점이 있습니다. + +그래서 인덱스 시그니처의 값 타입에 undefined를 추가해서 안정성을 높이는 방법을 권장합니다. 하지만 더욱 추천하는 것은, 가능하다면 인터페이스나 Record, 매핑딘 타입 같은 인덱스 시그니처말고 정확한 타입을 사용하는 것입니다. + +## 💡 number 인덱스 시그니처보다는 Array, 튜플, ArrayLike 사용하기 + +배열은 객체이므로 키는 숫자가 아니라 문자열입니다. 자바스크립트는 숫자를 키로 사용하는 것을 허용하지 않습니다. 숫자 인덱스를 사용해도 인덱스들이 문자열로 자동으로 변환되어 사용됩니다. 이런 혼란을 바로잡기 위해 타입스크립트는 숫자 키를 허용합니다. 하지만 인덱스 시그니처로 사용된 number 타입은 버그를 잡기 위한 순수 타입스크립트 코드입니다. 그러니 인덱스 시그니처에 number를 사용하기보다 Array, 튜플, ArrayLike를 사용하기를 권장합니다. + +## 💡 변경 관련된 오류 방지를 위해 readonly 사용하기 + +만약 함수가 매개변수를 수정하지 않는다면 readonly로 선언하는 것이 좋습니다. readonly 매개변수는 인터페이스를 명확하게 하며, 매개변수가 변경되는 것을 방지합니다. 불변을 사랑하는 함수형 개발자들이 readonly를 사랑하는 것은 당연할 수 밖에 없습니다. + +readonly를 사용하게 되면 변경하면서 발생하는 오류를 방지할 수 있고, 변경이 발생하는 코드도 쉽게 찾을 수 있습니다. + +하지만 readonly는 얕게 동작한다는 것도 잊지 마세요! + +## 💡 매핑된 타입을 사용하여 값을 동기화하기 + +매핑된 타입을 사용해 관련된 값과 타입을 동기화하도록 하세요. 새로운 속성이 추가될 때마다 값과 타입을 동기화시키면 타입 체커에게 보다 정확한 정보를 줄 수 있게 됩니다. 또한 매핑된 타입은 한 객체가 또 다른 객체와 정확히 같은 속성을 가지게 할 때 이상적입니다. 매핑된 타입을 이용하면 타입스크립트가 코드에 제약을 강제하도록 할 수 있습니다.