
6장 타입선언과 @types
item 45~52
6장 전반에선 타입스크립트에서 의존성이 어떻게 동작하는지를 설명하며 의존성의 개념을 잡고 의존성 관리에서 맞닥드릴 수 있는 문제와 해결책을 제시한다.
아이템 요약
1. 타입스크립트는 시스템 레벨로 설치하면 안된다. ts는devDependencies에 포함시켜 팀원 모두가 동일한 버전을 사용하도록 한다.
2. @types 의존성은dependencies가 아니라devDependencies에 포함시킨다.
npm(Node Package Manager)는 라이브러리 저장소(npm 레지스트리)와 프로젝트가 의존하는 있는 라이브러리들의 버전을 지정하는 방법인 package.json을 제공한다.
npm의 세가지 의존성 영역
→ ‘현재 프로젝트’ 실행에 필수적인 라이브러리들이 포함되는 영역.
런타임에 사용하는 라이브러리가 포함되어야 하고 만약 프로젝트를 다른 사용자가 설치하면 dependencies에 포함된 라이브러리도 함께 설치된다. 이 현상을 transitive dependencies(전이 의존성)라 한다
→ 런타임엔 필요없지만, 현재 프로젝트 개발과 테스트에 사용되는 라이브러리.
공개 프로젝트에서 다른 사용자가 설치할 땐 이 영역은 제외한다
→ 런타임에 필요하지만 의존성을 직접 관리하지 않는 라이브러리. ex) 플러그인
예를 들어, 제이쿼리의 플러그인은 다양한 버전의 제이쿼리랑 호환되서 실제 프로젝트에서 제이쿼리 버전을 선택하게 한다.
ts는 개발 도구이고 런타임에 타입 정보는 존재하지 않기 때문에 @types/{somelibrary} 형태의 타입스크립트 라이브러리는 devDependencies에 속한다.
모든 프로젝트에서 고려해야할 의존성 두가지
타입스크립트 자체 의존성
ts는 시스템 레벨보다 devDependencies에 넣어 install 단계에서 팀원 모두 항상 정확한 버전의 타입스크립트를 설치하도록 관리할 수 있다.
타입 의존성(@types)
@types 라이브러리는 타입 정보만 있고 구현체는 없다. 사용하려는 라이브러리에 타입 선언이 포함되어 있지 않더라도 @types 스코프에 해당 정보가 들어있다.
$ npm install react
$ npm install --save-dev @types/react
위 처럼 원본 라이브러리는 dependencies에 추가하고, @types 의존성은 devDependencies에 추가한다. 하지만 항상 유효한 방식은 아니기 때문에 다음 아이템에서 문제점을 다룬다.
아이템 요약
1. @types 의존성과 관련된 버전은 ‘라이브러리 버전’, ‘@types’ 버전, ‘타입스크립트 버전’ 세가지가 있다.
2. 라이브러리를 업데이트할 경우, 해당 라이브러리의 @types도 업데이트해야 한다.
3. 타입선언을 자체적으로 포함하는 것과 DefinitelyTyped에 공개하는 것 사이의 장단점을 이해하자
의존성 관리에서 위 세가지 버전 중 하나라도 맞지 않는다면 의존성과 상관없어 보이는 곳에서 엉뚱하게 오류가 발생할 수 있다. 오류를 파악하기 위해서 ts 라이브러리 관리 메커니즘을 이해하도록 하자.
$ npm install react
+ react@16.8.6
$ npm install --seve-dev @types/react
+ @types/react@16.8.19
리액트를 설치하는 예시를 살펴보자. 메이저 버전과 마이너 버전 16.8은 일치하지, 패치버전인 16.8.6과 16.8.19 는 일치하지 않는다.
→ 만약 리액트가 시맨틱 버전 규칙을 제대로 지킨다면 패치 버전은 공개 API의 사양을 변경하지 않아 @types/react의 16.8.19는 타입 선언을 업데이트할 필요가 없다. 하지만, 타입 선언 자체에 버그나 누락이 존재할 수 있어 라이브러리보다 더 많은 업데이트가 있다는 것이다
이 경우 실제 라이브러리랑 타입 정보 버전이 별도로 관리되면서 크게 4가지 문제가 생길 수 있다.
라이브러리만 업데이트하고 타입선언 업데이트 하지 않은 경우
→ 업데이트된 기능을 쓰려할 때마다 타입 오류 발생. 특히 하위 호환성이 깨지면 타입 체커를 통과하더라도 런타임 오류가 발생해서 일반적으로 타입 선언도 함께 업데이트한다.
다만, 타입 선언 버전 업데이트가 없을 경우엔 보강(augementation) 기법을 활용해 사용하려는 새 함수랑 메서드 타입 정보를 프로젝트에 자체 추가하거나 커뮤니티에 공유해 기여하는 방법이 있다.
라이브러리보다 타입선언이 최신 버전인 경우
→ 타입 정보가 없을 때 any나 declare module등으로 떼우다가 라이브러리나 타입선언의 새버전이 릴리즈될 때, 타입 체커는 최신 라이브러리 API 기준으로 검사하지만 런타임에 쓰이는 건 과거 버전이다.
해결책은 두 버전이 맞도록 업데이트 하거나 버전을 내리는 것 !
프로젝트 내 ts 버전보다 라이브러리에서 필요로하는 ts 버전이 더 최신인 경우
→ @types 선언 자체에서 타입 오류가 발생하게 된다.
프로젝트의 타입스크립트를 업데이트하거나 라이브러리 @types 을 내리거나 declare module 선언으로 라이브러리 타입 정보를 없애서 해결할 수 있다.
특정 버전에 대한 타입 정보 설치는 아래처럼 실행한다.
$ npm install --save-dev @types/lodash@ts3.1
라이브러리랑 타입 선언 버전 일치가 최선이지만 상황에 따라 없을 수도 있다. 유명한 라이브러리일 수록 버전 별 타입 선언이 존재할 가능성이 높다.
@types 의존성이 중복되는 경우
→ 런타임 사용 모듈은 괜찮더라도 전역 네임스페이스에서 문제가 발생할 수 있다.
서로 의존하는 @types/foo 랑 @types/bar가 있다고 하자. 만약 bar가 현재 플젝과 호환되지 않는 foo에 의존한다면 npm은 아래 폴더구조와 같이 bar의 폴더 내에 중첩으로 해당 버전을 설치해 문제를 해결하려 한다.
node_modules/
@types/
foo/
index.d.ts @1.2.3
bar/
index.d.ts
node_modules/
@types/
foo/
index.d.ts @2.3.4 //별도의 버전 설치
이 경우, 전역 네임스페이스에 타입 선언이 존재해 중복 선언 오류가 나타난다. 이런 상황에선 npm ls @types/foo 를 실행해 중복 위치를 추적하고 둘 중 하나를 업데이트해 버전이 호환되게 하자.
자체 타입 선언을 포함하는 라이브러리들의 문제
ts로 작성된 라이브러리는 대체로 자체적 번들링으로 package.json의 “types” 필드에서 ‘d.ts’ 파일을 가리켜 타입 선언을 추가한다.
이 번들링 방식은 네가지 문제점을 가지고 있다
@types 버전 선택이 불가능하기 때문에 번들된 타입 선언에 보강 기법으로 해결할 수 없는 오류가 있거나 ts버전이 올라가면서 오류가 발생한다.
→ 번들과 달리 MS에서 ts업데이트 마다 DefinitelyTyped의 모든 타입 선언을 점검해 해결한다
번들의 타입선언이 다른 라이브러리 타입 선언에 의존하는 경우
→ 공개 프로젝트를 다른 사용자가 설치할 경우 devDependencies를 설치하지 않기 때문에 오류 발생
과거 버전 타입 선언에 문제가 있다면 과거 버전으로 돌아가 수정해야 한다
타입 선언의 패치 업데이트는 유지 보수가 어렵다
결론 ) js 라이브러리 작성 시 DefinedTyped를 사용하는게 장점이 큰듯
아이템 요약
1. 공개 메서드에 등장한 어떤 형태의 타입이든 익스포트 하자. 라이브러리 사용자는 타입을 추출할 수 있다
interface SecretName {
first: string;
last: string;
}
interface SecretSanta {
name: SecretName;
gift: string;
}
export function getGift(name: SecretName, gift: string): SecretSanta {
// COMPRESS
return {
name: {
first: 'Dan',
last: 'Van',
},
gift: 'MacBook Pro',
};
// END
}
type MySanta = ReturnType<typeof getGift>; // SecretSanta
type MyName = Parameters<typeof getGift>[0]; // SecretName
‘ReturnType’ 이나 ‘Parameters’ 제너릭 타입으로 서드 파티 모듈에서 익스포트 되지않은 타입 정보를 작성해 사용할 수 있다.
아이템 요약
1. 익스포트된 함수, 클래스, 타입에 JSDoc/TSDoc 주석을 달아 편집기가 정보를 표시하게 하자
2. @param, @returns 구문과 문서 서식엔 마크다운을 쓸 수 있다.
3.주석에 타입 정보는 포함하지 말자
인라인 형태의 주석은 편집기가 툴팁 설명에 표시해주지 않는다. TS는 JSDoc 주석을 지원하므로 적극적으로 활용하자.
공개 API에 주석을 붙일 땐, @param, @returns 규칙을 사용해 JSDoc 주석을 작성하자.
/**
* Generate a greeting.
* @param name Name of the person to greet
* @param title The person's title
* @returns A greeting formatted for human consumption.
*/
function greetFullTSDoc(name: string, title: string) {
return `Hello ${title} ${name}`;
}
위 두 규칙을 사용해 주석을 달면 함수를 호출하는 매개변수에서 관련 정보를 보여준다.
타입 정의에서 TSDoc을 사용할 수도 있다.
interface Vector3D {}
/** A measurement performed at a time and place. */
interface Measurement {
/** Where was the measurement made? */
position: Vector3D;
/** When was the measurement made? In seconds since epoch. */
time: number;
/** Observed momentum */
momentum: Vector3D;
}
인터페이스에서 각 필드 위에 마우스를 올리면 TSDoc이 표시된다.
vanila extract style의 space px guide를 JSDoc 스타일의 주석으로 표시한 예시

또, TSDoc은 마크다운 형식으로 꾸밀 수 있어 기울임이나 볼드, 글머리 기호 목록등을 사용할 수 있다.
/**
* This _interface_ has **three** properties:
* 1. x
* 2. y
* 3. z
*/
interface Vector3D {
x: number;
y: number;
z: number;
}
아래는 현재 진행중인 프로젝트에서 주석을 개선한 예시인데
타입 정보가 이미 명시되어 있으므로 필요없는 @property 타입 정보 코드를 삭제했다.

아이템 요약
1. this 바인딩 동작 원리를 이해한다
2. 콜백 함수에서 this를 사용해야 한다면, 타입 정보를 명시한다.
this는 객체의 현재 인스턴스를 참조하는 ‘클래스’에서 많이 쓰인다.
// 1번 코드
class C {
vals = [1, 2, 3];
logSquares() {
for (const val of this.vals) {
console.log(val * val);
}
}
}
const c = new C();
c.logSquares();
// 2번 코드
class C {
vals = [1, 2, 3];
logSquares() {
for (const val of this.vals) {
console.log(val * val);
}
}
}
const c = new C();
const method = c.logSquares;
method();
c.prototype.logSquares를 호출하고 this의 값을 ‘c’로 바인딩한다. → call()을 사용해 명시적으로 this를 바인딩하면 해결 가능하다.
class C {
vals = [1, 2, 3];
logSquares() {
for (const val of this.vals) {
console.log(val * val);
}
}
}
const c = new C();
const method = c.logSquares;
method.call(c); // Logs the squares again
이벤트에서 this를 바인딩 하는 예제
document.querySelector('input')!.addEventListener('change', function(e) {
console.log(this); // Logs the input element on which the event fired.
});
콜백 함수에서 this를 바인딩하는 예제
declare function makeButton(props: {text: string, onClick: () => void }): void;
class ResetButton {
constructor() {
this.onClick = this.onClick.bind(this);
}
render() {
return makeButton({text: 'Reset', onClick: this.onClick});
}
onClick() {
alert(`Reset ${this}`);
}
}
onClick() {...}은 ResetButton.Prototype의 속성을 정의하며 리셋버튼의 모든 인스턴스에 공유된다. 이때, this를 생성자에서 바인딩해주면 해당 인스턴스에 onClick 인스턴스 속성이 생성되고 프로토타입 속성보다 앞에 놓여 render()의 onClick이 성공적으로 생성된 인스턴스에 바인딩된 함수를 참조한다.
화살표 함수 예제
declare function makeButton(props: {text: string, onClick: () => void }): void;
class ResetButton {
render() {
return makeButton({text: 'Reset', onClick: this.onClick});
}
onClick = () => {
alert(`Reset ${this}`); // "this" always refers to the ResetButton instance.
}
}
화살표 함수는 ResetButton이 생성될때마다 this를 바인딩한 새 함수를 생성한다
라이브러리에 this를 사용한 콜백함수가 있는 예제
declare function makeButton(props: {text: string, onClick: () => void }): void;
function addKeyListener(
el: HTMLElement,
fn: (this: HTMLElement, e: KeyboardEvent) => void
) {
el.addEventListener('keydown', e => {
fn.call(el, e);
});
}
콜백함수 매개변수에 this를 추가한 다음 call로 호출하면,
declare function makeButton(props: {text: string, onClick: () => void }): void;
function addKeyListener(
el: HTMLElement,
fn: (this: HTMLElement, e: KeyboardEvent) => void
) {
el.addEventListener('keydown', e => {
fn(e);
// ~~~~~ The 'this' context of type 'void' is not assignable
// to method's 'this' of type 'HTMLElement'
});
}
class Foo {
registerHandler(el: HTMLElement) {
addKeyListener(el, e => {
this.innerHTML;
// ~~~~~~~~~ Property 'innerHTML' does not exist on type 'Foo'
});
}
}
콜백함수에서 this를 쓰면 API의 일부가 되는 것이므로 반드시 타입 선언에 포함해라
아이템 요약
: 오버로딩 타입보다 조건부 타입을 쓰는게 좋다. 조건부 타입은 추가 오버로딩없이 유니온을 지원한다.
function double(x: number|string): number|string;
function double(x: any) { return x + x; }
const num = double(12); // string | number
const str = double('x'); // string | number
function double<T extends number|string>(x: T): T;
function double(x: any) { return x + x; }
const num = double(12); // Type is 12
const str = double('x'); // Type is "x"
string 타입을 매개변수로 넘기면 string이 반환되고 리터럴 문자열 ‘x’를 넘기면 ‘xx’가 반환되는 double함수를 만든다고 가정하자.
첫 예시는 함수에 number 타입을 매개변수로 넣고 string을 반환하는 경우를 모델링하지 못한다. 따라서 제네릭을 사용해 두번째 예시에서 모델링해보자. 너무 과한 나머지 리터럴을 ‘xx’로 반환하지 못하고 있다.
유니온 타입에서도 작동할 수 있도록 조건부 타입을 사용해 해결해 보자
조건부 타입
function double<T extends number | string>(
x: T
): T extends string ? string : number;
function double(x: any) { return x + x; }
const num = double(12); // number
const str = double('x'); // string
삼항 연산자처럼 작성할 수 있음
아이템 요약
1. 필수가 아닌 의존성을 분리할 땐 구조적 타이핑을 사용한다
2. 공개 라이브러리를 사용하는 js사용자는 @types 의존성을 갖지 않게하고 웹 개발자는 Nodejs관련 의존성을 갖지 않게 한다.
CSV 파일 내용을 매개변수로 받고 내용을 열의 이름 값으로 매핑하는 객체를 생성해 배열로 반환하는 함수를 만든다고 하자. 이때, NodeJS 사용자를 위해 매개변수에 Buffer 타입을 허용하겠다면 NodeJS 타입 선언을 devDependencies에 추가해야 한다.
function parseCSV(contents: string | Buffer): {[column: string]: string}[] {
if (typeof contents === 'object') {
// It's a buffer
return parseCSV(contents.toString('utf8'));
}
// COMPRESS
return [];
// END
}
이 함수가 포함된 라이브러리를 공개하면 다음 사용자들에게 문제가 생긴다.
따라서, 각자 필요한 모듈을 사용할 수 있도록 Buffer선언이 필요하지 않은 인터페이스로 문제를 해결할 수 있다.
interface CsvBuffer {
toString(encoding: string): string;
}
function parseCSV(contents: string | CsvBuffer): {[column: string]: string}[] {
// COMPRESS
return [];
// END
}
이를 필요한 타입 선언부만 추출하여 작성중인 라이브러리에 넣는 것 (미러링)이라 부란다.
→ NodeJS기반 ts 사용자에겐 변화가 없지만, 웹개발자나 자바스크립트 사용자에게 나은 선택지
→ 서드 파티 라이브러리 구현에 의존하는 경우에도 타입 의존성을 피하면서 미러링을 적용할 수 있다
→ 단, 타입 선언 대부분을 추출해야 한다면 @types 의존성을 추가하는 게 낫다.
아이템 요약
1. 타입을 테스트할 땐 함수 타입의 동일성(equality)와 할당 가능성(assignablity)의 차이점을 알자
2. 콜백 함수 테스트 시, 콜백 매개변수의 추론 타입을 체크해야 한다. 또한 this가 API의 일부분이라면 이 역시 테스트해야 한다.
3. 타입 테스트에서 any에 주의하고 엄격한 테스트를 위해dtslint같은 도구를 쓰자
const square = (x: number) => x * x;
declare function map<U, V>(array: U[], fn: (u: U) => V): V[];
const lengths: number[] = map(['john', 'paul'], name => name.length);
lengths 배열의 number[] 타입 선언은 불필요한 타입 선언이지만, 테스팅에서 map 함수의 반환 타입이 number[] 임을 보장한다. 여기에는 두가지 문제가 있는데 다음과 같다
불필요한 변수를 만들어야 한다
: 변수를 도입하는 대신, 헬퍼 함수를 정의한다.
function assertType<T>(x: T) {}
assertType<number[]>(map(['john', 'paul'], name => name.length));
두 타입이 동일한지 체크하는 것이 아니라, 할당 가능성을 체크한다
```tsx
function assertType<T>(x: T) {}
const add = (a: number, b: number) => a + b;
assertType<(a: number, b: number) => number>(add); // OK
const double = (x: number) => 2 * x;
assertType<(a: number, b: number) => number>(double); // OK!?
```
→ double은 매개변수 하나를 받고있지만 assertType<(a,b)>에서 타입 체크가 성공하고 있다. 이 이유는 타입 스크립트의 함수가 매개변수가 더 적은 함수 타입에 할당가능하기 때문이다
TS 함수가 더 적은 매개변수를 가진 함수 타입에 할당되는 예제
const g: (x: string) => any = () => 12; // OK
map(array, (name, index, array) => { /*...*/ }); // 세 매개변수를 다 사용하지 않아도 OK
이렇게 매개변수가 더 작은 함수타입에 할당 가능할 때 제대로 테스팅하려면 Parameters와 ReturnType 제네릭을 사용해 매개변수의 타입과 리턴 타입을 분리해 테스트 할 수 있다.
const square = (x: number) => x * x;
declare function map<U, V>(array: U[], fn: (u: U) => V): V[];
function assertType<T>(x: T) {}
const double = (x: number) => 2 * x;
let p: Parameters<typeof double> = null!;
assertType<[number, number]>(p);
// ~ Argument of type '[number]' is not
// assignable to parameter of type [number, number]
let r: ReturnType<typeof double> = null!;
assertType<number>(r); // OK
this 콜백함수 테스팅하는 예제
// tsConfig: {"noImplicitAny":false}
declare function map<U, V>(array: U[], fn: (u: U) => V): V[];
function assertType<T>(x: T) {}
const beatles = ['john', 'paul', 'george', 'ringo'];
assertType<number[]>(map(
beatles,
function(name, i, array) {
// ~~~~~~~ Argument of type '(name: any, i: any, array: any) => any' is
// not assignable to parameter of type '(u: string) => any'
assertType<string>(name);
assertType<number>(i);
assertType<string[]>(array);
assertType<string[]>(this);
// ~~~~ 'this' implicitly has type 'any'
return name.length;
}
));
map에서 세부 작동을 테스팅하기 위해 콜백에서 매개변수들의 타입과 this를 직접 입력해 테스팅한다고 가정하면, 오류가 발생한다.
declare function map<U, V>(
array: U[],
fn: (this: U[], u: U, i: number, array: U[]) => V
): V[];
→ 타입 선언에서 ‘Argument of type '(name: any, i: any, array: any) => any' is not assignable to parameter of type '(u: string) => any'’ 에러를 해결할 수 있다.
→ map(array, (name, index, array) => { /.../ }); 매개변수를 다시 선언한 것 ?
→ declare module ‘overbar’ any로 모든 걸 반환하므로 는 타입안정성을 위해 쓰지말자
타입 선언의 글자 자체가 같은지 비교하는 dtslint
declare function map<U, V>(
array: U[],
fn: (this: U[], u: U, i: number, array: U[]) => V
): V[];
const beatles = ['john', 'paul', 'george', 'ringo'];
map(beatles, function(
name, // $ExpectType string
i, // $ExpectType number
array // $ExpectType string[]
) {
this // $ExpectType string[]
return name.length;
}); // $ExpectType number[]
dtslint 는 심벌 타입의 글자 자체가 같은지 자동화해 확인한다.
[dtslint의 단점]