item 6. IDE를 사용하여 타입 시스템 탐색하기
•
타입스크립트를 설치하면 타입스크립트 컴파일러와 단독으로 실행할 수 있는 타입스크립트 서버를 실행할 수 있다.
•
보통의 목적은 컴파일러를 실행하는 것이지만 언어 서비스를 제공한다는 점에서 타입스크립트 서버 또한 중요하다.
•
언어 서비스에는 코드 자동 완성, 명세 검사, 검색, 리팩토링이 포함된다. 보통은 IDE를 통해서 언어 서비스를 사용하는데, 타입스크립트 서버에서 언어 서비스를 제공하도록 하는 것이 좋다.
WebStorm 설정
•
요약
◦
타입스크립트 언어 서비스를 적극 사용하면 어떻게 타입 시스템이 동작하는지 그리고 타입스크립트가 어떻게 타입을 추론하는지 개념을 잡을 수 있다.
◦
타입스크립트가 동작을 어떻게 모델링하는지 알기 위해 타입 선언 파일을 찾아보는 방법을 터득해야 한다.
item 7. 타입이 값들의 집합이라고 생각하기
•
런타임에 모든 변수는 자바스크립트 세상의 값으로부터 정해지는 각자 고유한 값을 가진다. 변수에는 다양한 종류의 값을 할당할 수 있다.
•
반면 코드가 실행되기 전, 즉 타입스크립트가 오류를 체크하는 순간에는 타입을 가지게 된다. 할당 가능한 값들의 집합이 타입이라고 생각하면 된다.
◦
이 집합은 타입의 범위라고 부르기도 한다. 가장 작은 집합은 아무 값도 포함하지 않는 공집합이며 이는 타입스크립트에서 never로 표현된다.
◦
다음으로 가장 작은 집합은 한 가지 값만 포함하는 타입으로 유닛 타입이라고도 불리는 리터럴 타입이다.
▪
두 개 혹은 세 개로 묶으려면 유니온 타입을 사용한다.
▪
유니온 타입은 값 집합들의 합집합을 의미한다.
•
타입스크립트 오류들 중 할당 가능한이라는 문구를 종종 확인 할 수 있는데, 이를 통해 타입 체커의 주요 역할은 하나의 집합이 다른 집합의 부분 집합인지 검사하는 것이라고 볼 수 있다.
•
타입스크립트는 구조적 타이핑을 기반으로 하기 때문에 다음과 같은 객체가 있다고 할 때, 어떤 객체가 string으로 할당 가능한 id 속성을 가지고 있다면 그 객체는 해당 타입이다.
interface Type { id: string; }
Java
복사
•
& 연산자는 피연산 타입의 인터섹션을 계산한다. 언뜻 봤을땐 공통된 속성을 타입으로 가지는 타입이 생성될 것 같다.
◦
그러나 타입 연산자는 인터페이스의 속성이 아닌 값의 집합에 적용되므로 추가적인 속성을 가지는 값도 여전히 해당 타입에 속한다.
◦
따라서 인터섹션 타입의 값은 각 타입 내의 속성을 모두 포함하는 것이 일반적인 규칙이다. 합 연산처럼 동작한다.
•
조금 더 일반적으로 공집합을 선언하는 방법은 extends 키워드를 사용하는 것이다.
◦
즉, 상속의 의미를 집합의 관점으로 생각하는 것이다.
•
extends 키워드는 제네릭 타입에서 한정자로도 사용되며 아래의 문맥에서는 ~의 부분 집합을 의미하기도 한다.
function getKey<K extends string>(val: any, key: K) { ... };
TypeScript
복사
◦
string을 상속한다는 기존의 개념에서 이해를 시도하면 조금 어렵지만 집합의 관점에서 생각해보면 쉽게 이해할 수 있다.
•
타입스크립트 용어와 집합 용어
타입스크립트 용어 | 집합 용어 |
never | 공집합 |
리터럴 타입 | 원소가 1개인 집합 |
값이 T에 할당 가능 | 값이 T의 원소 |
T1이 T2에 할당 가능 | T1이 T2의 부분 집합 |
T1이 T2를 상속 | T1이 T2의 부분 집합 |
T1 | T2 | T1과 T2의 합집합 |
T1 & T2 | T1과 T2의 교집합 |
unknown | 전체 집합 |
•
요약
◦
타입을 값의 집합으로 생각하면 이해하기 편하다. 이 집합은 유한하거나 무한하다.
◦
타입스크립트 타입은 엄격한 상속 관계가 아니라 겹쳐지는 벤다이어그램으로 표현된다. 두 타입은 서로 서브타입이 아니면서도 겹쳐질 수 있다.
◦
한 객체의 추가적인 속성이 타입 선언에 언급되지 않더라도 그 타입의 원소일 수 있다.
◦
타입 연산은 집합의 범위에 적용된다. A와 B의 &은 A의 범위와 B의 범위의 &이다. 객체 타입에서는 A & B인 값이 A와 B의 속성을 모두 가짐을 의미한다.
◦
A는 B를 상속, A는 B에 할당 가능, A는 B의 서브타입은 A는 B의 부분 집합과 같은 의미이다.
item 8. 타입 공간과 값 공간의 심벌 구분하기
•
타입스크립트의 심벌은 타입 공간이나 값 공간 중의 한 곳에 존재한다.
•
심벌은 이름이 같더라도 속하는 공간에 따라 다른 것을 나타내므로 혼란스러울 수 있다.
•
타입 선언(:) 또는 단언문(as) 다음에 나오는 심벌은 타입인 반면 = 다음에 나오는 모든 것은 값이다.
•
간단하게 각 타입 구문들이 값의 관점에서 해석되는지, 타입 관점에서 해석되는지에 따라 많은것이 달라지므로 이를 주의하자.
•
요약
◦
타입스크립트 코드를 읽을 때 타입인지 값인지를 구분하는 방법을 터득해야 한다.
◦
모든 값은 타입을 가지지만 타입은 값을 가지지 않는다. type과 interface 같은 키워드는 타입 공간에만 존재한다.
◦
class나 enum같은 키워드는 타입과 값 두 가지로 사용될 수 있다.
◦
“foo”는 문자열 리터럴이거나 문자열 리터럴 타입일 수 있다. 차이점을 알고 구별할 수 있어야 한다.
◦
typeof, this 그리고 많은 다른 연산자들과 키워드들은 타입 공간과 값 공간에서 다른 목적으로 사용된다.
item 9. 타입 단언보다는 타입 선언을 사용하기
// 타입 선언
const alice: Person = {};
// 타입 단언
const bob = {} as Person
TypeScript
복사
•
첫 번째 alice: Person은 변수에 타입 선언을 붙여서 그 값이 선언된 타입임을 명시한다.
•
두 번째 as Person은 타입 단언을 수행한다. 이 경우, 타입스크립트가 추론한 타입이 있더라도 Person 타입으로 간주한다.
•
타입 단언보다 타입 선언을 사용하는 것이 낫다.
const alice: Person = {}; // Person 유형에 필요한 name 속성이 {} 유형에 없습니다.
const bob = {} as Person // 오류 없음
TypeScript
복사
◦
타입 선언은 할당되는 값이 해당 인터페이스나 선언된 타입을 만족하는지 검사한다. 가급적이면 안전성 체크도 되는 타입 선언을 사용하는 것이 좋다.
•
타입 단언이 필요한 경우는 다음과 같다.
◦
타입 체커가 추론한 타입보다 프로그래머가 판단하는 타입이 더 정확할 때 사용한다.
◦
접미사로 !가 사용되는 경우는 null이 아니라는 단언문으로 해석된다. 접두사의 경우 부정을 나타내지만 접미사의 경우 그 의미가 다르다.
◦
타입 단언 시, 임의의 타입 간에 변환을 할 수는 없으나. A가 B의 부분 집합인 경우 타입 단언문을 이용해 변환할 수 있다.
▪
A 타입의 값을 B 타입으로 변환가능하다.
•
요약
◦
타입 단언(as Type)보다 타입 선언(: Type)을 사용해야 한다.
◦
화살표 함수의 반환 타입을 명시하는 방법을 터득해야 한다. 타입 선언을 이용해 반환 타입을 명시하자.
◦
타입스크립트보다 타입 정보를 더 잘 알고 있는 상황에서는 타입 단언문과 ! 접미사 단언문을 사용하면 된다.
item 10. 객체 래퍼 타입 피하기
•
자바스크립트에는 객체 외에도 기본형 값들에 대한 일곱 가지 타입(string, number, boolean, null, undefined, symbol, bigint)가 있다.
•
기본형들은 불변이며 메소드를 가지지 않는다는 점에서 객체와 구분된다. 그런데 다음과 같은 동작은 뭘까?
◦
‘primitive’.chatAt(3) // “m”
•
string(기본형)에는 메소드가 없지만 자바스크립트에는 String 객체 타입이 정의되어 있다. 따라서 위와 같은 연산은 다음과 같은 순서를 거쳐 실행된다.
1.
기본형을 String 객체로 래핑한다.
2.
메소드를 호출한다.
3.
래핑한 객체를 버린다.
•
다른 기본형에도 동일하게 객체 래퍼 타입이 존재한다. 이 래퍼 타입들 덕분에 기본형 값에도 메소드를 사용할 수 있고 정적 메소드도 사용할 수 있다.
•
런타임의 값은 객체가 아니고 기본형이다. 그러나 기본형 타입은 객체 래퍼에 할당가능하기 때문에 타입스크립트는 기본형 타입을 객체 래퍼에 할당하는 선언을 허용한다.
•
그러나 기본형 타입을 객체 래퍼에 할당하는 구문은 오해하기 쉽고 굳이 그렇게 할 필요도 없다. 가급적이면 기본형을 타입으로 쓰는 것이 좋다.
•
요약
◦
기본형 값에 메소드를 제공하기 위해 객체 래퍼 타입이 어떻게 사용되는지 이해해야 한다. 직접 사용하거나 인스턴스를 생성하는 것은 피해야 한다.
▪
타입스크립트 객체 래퍼 타입은 지양하고 대신 기본형 타입을 사용해야 한다.
▪
String 대신 string, Number 대신 number와 같이 말이다.
item 11. 잉여 속성 체크의 한계 인지하기
•
타입이 명시된 변수에 객체 리터럴을 할당할 때 타입스크립트는 해당 타입의 속성이 존재하는지, 그리고 그 외의 속성은 존재하지 않는지 확인한다.
interface Room {
numDoors: number;
ceilingHeightFt: number;
}
const r: Room = { numDoors: 1, ceilingHeightFt: 10, elephant: 'present' }; // 개체 리터럴은 알려진 속성만 지정할 수 있으며 'Room' 형식에 'elephant'가 없습니다.
TypeScript
복사
◦
이는 구조적 타이핑 관점에서 생각하면 아무 문제가 없어야 한다. numDoors도 있고 ceilingHeightFt도 있다.
◦
만약 객체 리터럴을 할당하지 않고 객체를 할당한다면 이는 정상적으로 동작할 것이다.
const obj = { numDoors: 1, ceilingHeightFt: 10, elephant: 'present' };
const r: Room = obj
TypeScript
복사
•
앞선 예제가 실패한 이유는 구조적 타입 시스템에서 발생할 수 있는 중요한 오류중 하나인 속성 체크 과정이 수행되었기 때문이다.
•
이러한 잉여 속성 체크는 조건에 따라 동작하지 않는다는 한계가 존재하므로 할당 가능 검사와 동일하다고 인식해버리면 구조적 타이핑에 대한 근본적인 의문이 들게될 것이다.
•
잉여 속성 체크와 할당 가능 검사가 별도의 과정이라는 것을 알아야 타입스크립트의 타입 시스템에 대한 개념을 잡을 수 있다.
•
잉여 속성 체크는 타입 단언문을 사용할 때에도 적용되지 않는다.
const o = { darkmode: true, title: 'Ski Free' } as Options
TypeScript
복사
◦
따라서 item 9에서 타입 선언을 지향하라 한 것이다.
•
요약
◦
객체 리터럴을 변수에 할당하거나 함수에 매개변수로 전달할 때 잉여 속성 체크가 수행된다.
◦
잉여 속성 체크는 오류를 찾는 효과적인 방법이지만, 타입스크립트 타입 체커가 수행하는 할당 가능 검사와 역할이 다르다.
▪
할당의 개념을 정확히 알아야 잉여 속성 체크와 할당 가능 검사를 구분할 수 있다.
◦
잉여 속성 체크에는 한계가 있다. temp 변수를 도입하면 잉여 속성 체크를 무시할 수 있다는 걸 기억하자.
item 12. 함수 표현식에 타입 적용하기
•
자바스크립트와 타입스크립트에서는 함수 문장(statement)와 함수 표현식(expression)을 다르게 인식한다.
function rollDice1(sides: number): number { ... }; // statement
const rollDice2 = function(sides: number): number { ... }; // expression
const rollDice3 = (sides: number): number => { ... }; // expression
TypeScript
복사
•
가급적이면 함수 표현식을 사용하는 것이 좋다.
◦
함수의 매개변수부터 변환값까지 전체를 함수 타입으로 선언하여 함수 표현식에 재사용할 수 있기 때문이다.
•
함수 타입의 선언은 불필요한 코드의 반복을 줄인다.
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 };
TypeScript
복사
// 함수 타입 선언
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;
TypeScript
복사
◦
이처럼 반복되는 함수 시그니처를 하나의 함수 타입으로 통합할 수 있다.
•
공통 함수 시그니처를 사용하는 것은 일종의 추상화 계층을 제공해준다. 불필요한 메소드 시그니처의 반복을 막아주고 메소드 시그니처의 가독성을 높여준다.
◦
const add: BinaryFn = (a, b) ⇒ a + b;
◦
이 함수를 사용하는 메소드에서 필요한 정보는 뭘 가지고 뭘 반환해주는 정도만 알면 된다.
◦
즉 a가 필요하고 b가 필요하다는 구현 세부 사항은 외부에서 알 필요가 없으므로 공통 함수 시그니처를 사용하면 어떻게 되겠구나의 흐름정도는 파악하기 쉬우므로 더 나은 것 같다.
•
함수의 매개변수에 타입 선언을 하는 것보다 함수 표현식 전체 타입을 정의해두는 것이 코드도 간결하고 더 안전하다.
◦
다른 함수의 시그니처와 동일한 타입을 가지는 함수를 작성하거나 동일한 타입 시그니처를 가지는 여러 함수를 정의해야할 경우, 매개변수의 타입과 반환 타입을 반복해서 작성하지 않고 함수 전체의 타입 선언을 적용하는 것이 좋다.
•
요약
◦
매개변수나 반환 값에 타입을 명시하기보다는 함수 표현식 전체에 타입 구문을 적용하는 것이 좋다.
◦
만약 같은 시그니처를 반복적으로 작성한 코드가 있다면 함수 타입을 분리해 내거나 이미 존재하는 타입을 찾아보도록 하자.
▪
라이브러리를 직접 만든다면 공통 콜백에 타입을 제공해야만 한다.
◦
다른 함수의 시그니처를 참조하려면 typeof {functionName}을 사용하면 된다.
item 13. 타입과 인터페이스의 차이점 알기
•
타입스크립트에서 타입을 정의하는 방법은 두 가지가 있다.
type TState = {
name: string;
capital: string;
}
TypeScript
복사
interface IState = { // interface의 접두사로 I를 사용하는 것은 C#에서 비롯된 관습으로 지금은 지양한다.
name: string;
capital: string;
}
TypeScript
복사
•
대부분의 경우에는 이 둘을 혼용해서 사용해도 괜찮으나 둘의 차이를 명확하게 알고 같은 상황에선 일관적인 방법으로 타입을 사용해야 한다. 인터페이스에는 보강 기법이 존재하기 때문이다.
•
유사한 점
◦
명명된 타입은 인터페이스로 정의하든 타입으로 정의하든 상태 자체에는 차이가 없다.
◦
만약 변수에 타입 선언 후, 객체 리터럴을 할당할 때, 추가적인 속성이 존재한다면 두 방법 모두 동일한 오류가 발생한다.
◦
두 방법 모두 인덱스 시그니처를 사용할 수 있다.
type TDict = { [key: string]: string }
interface IDict { [key: string]: string }
TypeScript
복사
◦
함수의 타입도 인터페이스나 타입으로 정의할 수 있다.
type TFn = (x: number) => string;
interface IFn { (x: number): string; }
TypeScript
복사
◦
제네릭도 사용 가능하다.
type TPair<T> = { first: T; second: T; }
interface IPair<T> { first: T; second: T; }
TypeScript
복사
◦
추가적인 속성의 경우도 어느것을 선택해도 별 차이가 없다.
type TFnWithProperties = { (x: number): number; props: string; }
interface IFnWithProperties { (x: number): number; props: string; }
TypeScript
복사
◦
인터페이스와 타입은 서로 확장 가능하다.
interface IStateWithPop extends TState { population: number; }
type TStateWithPop = IState & { population: number; };
TypeScript
복사
▪
IStateWithPop과 TStateWithPop은 동일하다. 다만 주의할 점은 인터페이스의 경우 유니온 타입과 같은 복잡한 타입은 확장하지 못한다는 것이다.
▪
복잡한 타입을 확장하고 싶다면 타입으로 선언하고 &을 사용해야 한다.
◦
구상 클래스를 작성할 때는 타입과 인터페이스 둘 다 사용할 수 있다.
class StateT implements TState {
name: string = '';
capital: string = '';
}
TypeScript
복사
class StateI implements IState {
name: string = '';
capital: string = '';
}
TypeScript
복사
•
차이점
◦
인터페이스엔 있으나 타입에는 없는 부분을 알아보자.
▪
속성을 추가할 때 보강 기법을 사용할 수 있다.
interface IState { name: string; capital: string; }
interface IState { popluation: number; }
const wyoming: IState = { name: 'Wyoming', capital: 'Cheyenne', popluation: 500_000 }; // 정상
TypeScript
복사
•
이 예제처럼 속성을 확장하는 것을 선언 병합이라고 한다.
•
결론
◦
복잡한 타입이라면 고민할 것도 없이 타입을 사용하면 된다.
◦
그러나 타입과 인터페이스, 두 가지 방법으로 모두 표현할 수 있는 간단한 객체 타입이라면 일관성과 보강의 관점에서 고려해 봐야 한다.
▪
타입을 정의할 때 어떤 방법을 쓰고 있었는지 일관성있게 사용하면 된다.
◦
아직 스타일이 확실하게 규정되지 않은 프로젝트라면 향후에 변동 가능성이 높은지 확인해야 봐야한다.
▪
이런 경우 보강 기법을 사용할 수 있는 인터페이스를 사용하는 것이 좋은 선택일 수 있다.
•
요약
◦
타입과 인터페이스의 차이점과 유사한 점을 이해하고 사용해야 한다.
◦
한 타입을 type과 interface 두 가지 문법을 사용해서 작성할 줄 알아야 한다.
◦
프로젝트에서 어떤 문법을 사용할지 고려할 때 한 가지 일관된 스타일을 따르는 것이 좋으며 이때 보강 기법이 필요한지 고려해야 한다.
item 14. 타입 연산과 제네릭 사용으로 반복 줄이기
•
코드의 중복을 줄이고 DRY 원칙을 지키기 위해 재사용성이 높은 코드를 메소드로 추출해낸 경험이 존재할 것이다.
•
타입의 중복을 줄이기 위해서 우린 타입 연산과 제네릭을 사용할 수 있다.
•
타입 중복이 코드 중복에 비해 흔한 이유는 공유 속성들을 제거하는 매커니즘이 기존 코드 중복을 줄이는 매커니즘과 달라 익숙하지 않기 때문이다.
◦
타입 간 매핑하는 방법을 익히면 타입 정의에서도 DRY의 장점을 적용할 수 있다.
•
반복을 줄이는 가장 간단한 방법은 타입에 이름을 붙이는 것이다.
function distance(a: {x: number, y: number}, b: {x: number, y: number}) { ... }
TypeScript
복사
interface Point { x: number; y: number; }
function distance(a: Point, b: Point) { ... }
TypeScript
복사
◦
이는 코드에서 상수를 이용해서 반복을 줄이는 기법을 동일하게 타입 시스템에 적용한 것이다.
•
이미 존재하는 타입을 확장하는 경우에는 일반적이지는 않으나 인터섹션 연산자&를 사용할 수도 있다.
◦
type PersonWithBirthDate = Person & { birth: Date };
◦
이 기법은 유니온 타입에 속성을 추가해야할 때 특히 유용하다.
▪
| 얘 말이다.
•
인덱싱을 사용한 기법도 있다.
interface State {
userId: string;
pageTitle: string;
recentFiles: string[];
pageContents: string;
}
TypeScript
복사
interface TopNavState {
userId: string;
pageTitle: string;
recentFiles: string[];
}
TypeScript
복사
◦
위 예시를 다음과 같이 변경할 수 있다.
type TopNavState {
userId: State['userId'];
pageTitle: State['pageTitle'];
recentFiles: State['recentFiles'];
}
TypeScript
복사
•
이는 매핑된 타입을 사용하면 조금 더 개선된 코드를 얻을 수 있다.
type TopNavState {
[k in 'userId' | 'pageTitle' | 'recentFiles']: State[k]
}
TypeScript
복사
◦
매핑된 타입은 배열의 필드를 순회하는 것과 같은 방식으로 이 패턴은 표준 라이브러리에서도 일반적으로 찾아볼 수 있으며 Pick이라 한다.
type Pick<T, K> = { [k in K]: T[k] };
TypeScript
복사
◦
다음과 같이 사용할 수 있다.
type TopNavState = Pick<State, 'userId' | 'pageTitle' | 'recentFiles'>;
TypeScript
복사
•
태그된 유니온에서도 타입의 중복이 발생할 수 있다.
◦
다음과 같이 단순히 태그를 붙이기 위해서 타입을 사용한다면 어떻게 될까?
interface SaveAction { type: 'save'; ... }
interface LoadAction { type: 'load'; ... }
type Action = SaveAction | LoadAction;
type ActionType = 'save' | 'load';
TypeScript
복사
◦
이를 인덱싱하면 다음과 같아질것이다.
interface SaveAction { type: ActionType['save']; ... }
interface LoadAction { type: ActionType['load']; ... }
type Action = SaveAction | LoadAction;
type ActionType = 'save' | 'load';
TypeScript
복사
◦
다음과 같이 Pick을 사용할 수 있다.
type ActionRec = Pick<Action, 'type'>;
TypeScript
복사
•
객체가 생성된 후, 업데이트가 가능한 클래스를 정의한다면 update 메소드 매개변수의 타입은 생성자와 동일한 매개변수이면서 타입 대부분이 선택적인 속성값이 된다.
interface User {
userId: number;
username: string;
}
TypeScript
복사
interface UserUpdate {
userId?: number;
username?: string;
}
TypeScript
복사
◦
이처럼 중복적으로 선언할 필요 없이, 매핑된 타입과 keyof를 사용해서 User로부터 UserUpdate를 만들수 있다.
type UserUpdate = {[k in keyof User]?: User[k]};
TypeScript
복사
◦
이 패턴 역시 아주 일반적이며 표준 라이브러리에 Partial이라는 이름으로 포함되어 있다.
•
값의 형태에 해당하는 타입을 정의하고 싶을 때도 있다.
const INIT_OPTIONS = {
width: 640,
height: 480,
color: '#00FF00',
label: 'VGA',
};
interface Option {
width: number,
height: number,
color: string,
label: string,
};
TypeScript
복사
◦
이런 경우 typeof를 사용하면 된다.
type Options = typeof INIT_OPTIONS;
Visual Basic
복사
◦
이 코드는 자바스크립트의 런타임 연산자 typeof를 사용한 것처럼 보이지만 실제로는 타입스크립트 단계에서 연산되며 훨씬 더 정확하게 타입을 표현한다.
◦
그러나 값으로부터 타입을 만들어내야하는 경우, 선언의 순서에 주의해야 한다. 타입 정의를 먼저하고 값이 그 타입에 할당 가능하다고 선언하는 것이 좋다.
•
함수나 메소드의 반환 값에 타입을 만들고 싶을 수도 있다.
function getUserInfo(userId: string) {
...
return {
userId,
name,
age,
height,
wieght,
favoriteColor,
};
}
// 추론된 반환 타입은 { userId: string, name: string, age:number ... }와 같다.
TypeScript
복사
◦
이때는 조건부 타입이 필요한데, 표준 라이브러리에는 이러한 일반적 패턴의 제네릭 타입이 이미 정의되어있다. ReturnType 제네릭이다.
▪
type UserInfo = ReturnType<typeof getUserInfo>;
•
제네릭 타입은 코드에서 중복된 부분의 메소드 추출과 유사하다. 따라서 타입에 있어서 DRY 원칙의 핵심은 제네릭이다.
◦
함수에서 매개변수로 사용할 수 있는 값을 제한하기 위해서 타입 시스템을 사용하는 것처럼 제네릭 타입에서도 타입을 제한할 수 있는 방법이 필요하다.
◦
일반적으로 제네릭 타입에서 매개변수를 제한할 수 있는 방법은 extends를 사용하는 것인데, extends를 사용하면 제네릭 매개변수가 특정 타입을 확장한다고 선언할 수 있다.
interface Name {
first: string;
last: string;
}
type DancingDuo<T extends Name> = [T, T];
const couple1: DancingDuo<Name> = [ { first: 'Fred', last: 'Astaire'}, { first: 'Ginger', last: 'Rogers'}, ] // 정상
const couple2: DancingDuo<{first: stirng}> = [ { first: 'Fred' }, { first: 'Ginger' }, ] // 오류가 발생한다. 제네릭 T는 Name 타입을 확장해야 한다.
TypeScript
복사
•
값 공간에서와 마찬가지로 반복적인 코드는 타입 공간에서도 좋지 않다.
◦
타입 공간에서 반복을 줄이려는 작업들은 익숙하지는 않지만 배워둘만한 가치가 있다. 반복하지 않도록 주의해야 한다.
•
요약
◦
DRY 원칙을 타입에도 최대한 도입해야 한다.
◦
타입에 이름을 붙여서 반복을 피하라. extends를 사용해서 인터페이스 속성의 반복을 줄여야 한다.
◦
타입들 간의 매핑을 위해 타입스크립트가 제공한 도구를 공부하면 좋다.
▪
여기에는 keyof, typeof, 인덱싱, 매핑된 타입들이 있다.
◦
제네릭 타입은 타입을 위한 함수와 같다. 타입을 반복하는 대신 제네릭 타입을 사용하여 타입들 간에 매핑을 하는 것이 좋다.
▪
제네릭 타입을 특정 타입을 확장하는 식으로 제한하고 싶다면 extends를 사용하면 된다.
◦
표준 라이브러리에 정의된 Pick, Partial, ReturnType과 같은 제네릭 타입을 자주 사용해보자.
item 15. 동적 데이터에 인덱스 시그니처 사용하기
•
타입스크립트에서는 타입에 ‘인덱스 시그니처’를 명시하여 유연하게 매핑을 표현할 수 있다.
type Rocket = {[property: string]: string};
const rocket: Rocket = {
name: 'Falcon 9',
variant: 'v1.0',
thrust: '4,940 kN',
}; // 정상
TypeScript
복사
◦
[property: string]: string이 인덱스 시그니처이며 다음 세 의미를 담고 있다.
▪
키의 이름: 키의 위치만을 표시하는 용도다. 타입 체커에서는 사용되지 않으므로 무시할 수 있는 참고 정보정도다.
▪
키의 타입: string이나 number또는 symbol의 일종이어야 하지만 보통은 string을 사용한다.
▪
값의 타입: 여기선 string으로 되어있으나 어느것이든 될 수 있다.
•
이런식으로 타입 체크를 수행하게 되면 다음과 같은 문제들이 발생할 수 있다.
◦
잘못된 이름의 키를 포함해 모든 키를 허용한다. name 대신 Name을 작성해도 유효한 타입이 된다.
◦
특정 키가 필수적으로 요구되지 않는다. {} 타입도 유효한 Rocket 타입이 된다.
◦
키마다 다른 타입을 가질 수 없다. 예를 들어 thrust는 string이 아닌 다른 타입을 가지지 못한다.
◦
타입스크립트 언어 서비스는 각 속성 값이 존재하지 않는 것으로 알고 있기 때문에 자동완성을 지원해주지 않는다.
•
가급적이면 인터페이스나 타입을 이용해 정확하게 타입을 규정하는 것이 좋으나 동적 데이터를 타이핑해야하는 경우에 인덱스 시그니처를 사용하는 것이 좋다.
◦
CSV 파일처럼 헤더 행에 열 이름이 존재하고 데이터 행을 열 이름과 값으로 매핑하는 객체로 나타내고 싶은 경우가 대표적이다.
◦
일반적인 상황에서 CSV 파일의 열 이름이 무엇인지 알고 있을 방법은 없다.
▪
알고 있다면 이는 해당 메소드가 파일에 과의존하고 있다는 증거다.
•
연관 배열의 경우, 객체에 인덱스 시그니처를 사용하는 대신 Map 타입을 사용하는 것을 고려할 수 있다.
◦
이는 프로토타입 체인과 관련된 유명한 문제를 우회하는데, 구체적인 예시는 Item 58에 존재한다.
•
동적 데이터를 다루기 위해 타입에 유연성을 제공할 수 있는 방법은 다양하다.
◦
만약 string 타입이 특정 속성의 키를 표현하기에 너무 광범위해서 인덱스 시그니처를 사용하는 데 문제가 있다면 다음 두 대안을 생각해볼 수 있다.
◦
첫 번째, Record를 사용하는 방법이다.
▪
string 타입의 부분 집합을 사용하는 방법이다.
type Vec3D = Record<'x' | 'y' | 'z', number>;
Type Vec3D = {
x: number;
y: number;
z: number;
}
TypeScript
복사
◦
두 번째, 매핑된 타입을 사용하는 방법이다.
type Vec3D = {[k in 'x' | 'y' | 'z']: number};
type ABC = {[k in 'a' | 'b' | 'c']: k extends 'b' ? string : number};
TypeScript
복사
•
요약
◦
런타임 때까지 객체의 속성을 알 수 없는 경우에만 인덱스 시그니처를 사용하도록 한다.
◦
안전한 접근을 위해 인덱스 시그니처의 값 타입에 undefined를 추가하는 것을 고려해야 한다.
◦
가능하다면 인터페이스, Record, 매핑된 타입과 같은 인덱스 시그니처보다 정확한 타입을 사용하는 것이 좋다.
item 16. number 인덱스 시그니처보다는 Array, 튜플, ArrayLike 사용하기
•
자바스크립트가 이상하게 동작하는 것 중 가장 악명이 높은 것은 암시적 타입 변환과 관련된 부분이다.
◦
다행히도 암시적 타입 강제와 관련된 문제는 대부분 ===와 !==를 이용해서 해결할 수 있다.
•
자바스크립트 객체 모델에도 이상한 부분들이 존재하며 이 중 일부는 타입스크립트 타입 시스템으로 모델링되기 때문에 이에 대한 이해가 필요하다.
◦
자바스크립트에서 객체란 키/값 쌍의 모음이다.
◦
키는 보통 문자열로 심볼일 수 있으며 값은 어떤 것이든 될 수 있다.
•
파이썬이나 자바에서 볼 수 있는 해시 가능 객체라는 표현이 자바스크립트에는 존재하지 않는다. 만약 복잡한 객체를 키로 사용하려고 하면 toString 메소드가 호출되어 객체가 문자열로 변환된다.
◦
특히 숫자는 키로 사용할 수 없다. 만약 속성 이름으로 숫자를 사용하려고 하면, 자바스크립트 런타임은 문자열로 변환한다.
•
배열은 분명히 객체다. 그렇다면 속성의 키로 숫자를 사용한다면 자바스크립트 런타임은 이를 문자열로 변환할 것이다.
◦
배열의 인덱스는 문자열로 변환되어 사용된다. 문자열 키를 이용해도 배열의 요소에 접근이 가능하다.
x = [1, 2, 3]
x[1] // 2
x['1'] // 2
TypeScript
복사
◦
Object.keys(x)를 호출하면 키가 문자열로 출력된다.
•
타입 스크립트는 이러한 혼란을 바로잡기 위해 숫자 키를 허용하고 문자열 키와 다른 것으로 인식한다.
interface Array<T> {
...
[n: number]: T;
}
TypeScript
복사
◦
런타임 상에는 ECMAScript 표준에 서술된 것처럼 문자열 키로 인식하므로 가상의 코드라고 할 수 있지만 타입 체크 시점에 오류를 잡을 수 있어 유용하다.
◦
따라서 다음 코드는 컴파일되지 않는다.
x = [1, 2, 3]
x[1] // 2
x['1'] // ~~~ 인덱스 식이 number 형식이 아니므로 요소에 암시적으로 any 형식이 있습니다.
TypeScript
복사
•
하지만 여전히 Object.keys 같은 구문은 여전히 문자열로 반환된다.
const xs = [1, 2, 3]
const keys = Object.keys(xs);
for (const key in keys) {
console.log(key);
console.log(xs[key]); // key가 문자열이므로 오류가 발생해야 하나 '실용적인 허용'으로 허용해준다고 한다.
}
TypeScript
복사
•
배열을 순회하는 코드 스타일에 대한 실용적인 허용이라고 생각하는 것이 좋다.
◦
자바스크립트에서는 흔한 일이지만 이 예제가 배열을 순회하기 좋은 방법은 아니다.
◦
인덱스에 신경 쓰지 않는다면 for-of를 사용하는 게 더 좋다.
for (const x of xs) {}
TypeScript
복사
◦
인덱스의 타입이 중요하다면 number 타입을 제공해주는 Array.prototype.forEach를 사용하면 된다.
xs.forEach((x, i) => { i, x; }
TypeScript
복사
◦
루프 중간에 멈춰야 한다면 C 스타일인 for(;;) 루프를 사용하는 것이 좋다.
for (let i = 0; i < xs.length; i++) {
const x = xs[i];
if (x < 0) break;
}
TypeScript
복사
◦
타입이 불확실하다면 대부분의 브라우저와 자바스크립트 엔진에서 for-in 루프는 for-of 또는 C 스타일 for 루프에 비해 몇 배나 느리다.
•
인덱스 시그니처가 number로 표현되어 있다면 입력한 값이 number여야 한다는 것을 의미하지만 실제 런타임에 사용되는 키는 string 타입이다.
◦
일반적으로 string 대신 number를 타입의 인덱스 시그니처로 사용하는 일은 많지않다. 속성 값을 숫자로 사용하진 않으니까..
◦
만약 숫자를 사용하여 인덱스할 항목을 지정한다면 Array 또는 튜플 타입을 대신 사용한다는 의미다.
•
어떤 길이를 가지는 배열과 비슷한 형태의 자료구조를 사용하고 싶다면 타입스크립트에 있는 ArrayLike 타입을 사용하라.
function checkedAccess<T>(xs: ArrayLike<T>, i: number): T {
if (i < xs.length) {
return xs[i];
}
throw new OutOfRangeError();
}
TypeScript
복사
•
요약
◦
배열은 객체로 키는 숫자가 아니라 문자열이다.
▪
인덱스 시그니처로 사용된 number 타입은 버그를 잡기 위한 단순 타입스크립트 코드다.
◦
인덱스 시그니처에 number를 사용하기보단 Array나 튜플, 또는 ArrayLike 타입을 사용하는 것이 좋다.
item 17. 변경 관련된 오류 방지를 위해 readonly 사용하기
•
매개변수를 readonly로 선언하면 다음과 같은 작업을 수행한다.
◦
타입스크립트는 매개변수가 함수 내에서 변경이 발생하는지 확인한다.
◦
호출하는 쪽에서는 함수가 매개변수를 변경하지 않는다는 보장을 받게 된다.
◦
호출하는 쪽에서 함수가 readonly 배열을 매개변수로 넣을 수 있다.
•
자바스크립트는 명시적으로 언급하지 않는 한 함수가 매개변수를 변경하지 않는다고 가정한다. 그러나 이러한 암묵적인 방법은 타입 체크에 문제를 일으킬 수 있다.
•
readonly는 얕게 동작한다는 것에 유의하며 사용해야 한다. 만약 객체의 readonly 배열이 존재한다면 그 객체 자체는 readonly는 아니다.
interface Outer {
inner: {
x: number;
}
}
type T = Readonly<Outer>;
TypeScript
복사
◦
중요한 점은 readonly 접근제어자는 inner에 적용되는 것이지 x는 아니라는 것이다.
•
현 시점에서 깊은 readonly 타입이 기본적으로 지원이 되지 않으므로 만약 사용을 원한다면 ts-essentials의 DeepReadonly<T>와 같은 서드파티를 사용하면 된다.
•
인덱스 시그니처에 readonly를 사용하면 객체의 속성이 변경되는 것을 방지할 수 있다.
let obj: {readonly [k: string]: number} = {};
TypeScript
복사
•
요약
◦
만약 함수가 매개변수를 수정하지 않는다면 readonly로 선언하는 것이 좋다.
▪
readonly 매개변수는 인터페이스를 명확하게 하며 매개변수가 변경되는 것을 방지한다.
◦
readonly를 사용하면 변경하면서 발생하는 오류를 방지할 수 있고 변경이 발생하는 코드도 쉽게 찾을 수 있다.
◦
const와 readonly의 차이를 이해해야 한다.
◦
readonly는 얕게 동작한다는 것을 명심해야 한다.
item 18. 매핑된 타입을 사용하여 값을 동기화하기
type ScatterProps = {
// Data
xs: number[];
ys: number[];
// Display
xRange: [number, number];
yRange: [number, number];
color: string;
// Events
onClick: (x: number, y: number, index: number) => void;
};
TypeScript
복사
•
위와 같은 코드에서 불필요한 작업을 피하기 위해 값이 변경될 때만 차트를 그리게 하고 싶다면 다음과 같이 최적화해볼 수 있다.
◦
실패에 닫힌, 보수적 접근법
const shouldUpdate = (oldProps: ScatterProps, newProps: ScatterProps) => {
let key: keyof ScatterProps;
for (key in oldProps) {
if (oldProps[key] !== newProps[key]) {
if (key !== "onClick") return true;
}
}
return false;
};
TypeScript
복사
◦
실패에 열린 접근법
const shouldUpdate = (oldProps: ScatterProps, newProps: ScatterProps) => {
return (
oldProps.xs !== newProps.xs ||
oldProps.ys !== newProps.ys ||
oldProps.xRange !== newProps.xRange ||
oldProps.yRange !== newProps.yRange ||
oldProps.color !== newProps.color
);
};
TypeScript
복사
◦
이상적인 해결법
const REQUIRES_UPDATE: { [key in keyof ScatterProps]: boolean } = {
xs: true,
ys: true,
xRange: true,
yRange: true,
color: true,
onClick: false,
};
const shouldUpdate = (oldProps: ScatterProps, newProps: ScatterProps) => {
let key: keyof ScatterProps;
for (key in oldProps) {
if (oldProps[key] !== newProps[key] && REQUIRES_UPDATE[key]) {
if (key !== "onClick") return true;
}
}
return false;
};
TypeScript
복사
▪
타입 체커가 동작하도록 개선되었으며 핵심은 매핑된 타입과 객체를 사용하는 것이다.
•
요약
◦
매핑된 타입을 사용해서 관련된 값과 타입을 동기화하도록 하자.
◦
인터페이스에 새로운 속성을 추가할 때 선택을 강제하도록 매핑된 타입을 고려해야 한다.