Search
Duplicate
🏓

6장. 타입 선언과 @types

타입스크립트에서 의존성이 어떻게 동작하는지 설명하여 의존성에 대한 개념을 잡을 수 있게 한다.
의존성 관리를 하다가 맞닥뜨릴 수 있는 몇 가지 문제를 보여주고 해결하는 방법을 찾아본다.

item 45. devDependencies에 typescript와 @types 추가하기

npm은 자바스크립트 라이브러리 저장소와 프로젝트가 의존하고 있는 라이브러리들의 버전을 지정하는 방법을 제공한다.
npm은 세 가지 종류의 의존성을 구분해서 관리한다.
dependencies
현재 프로젝트를 실행하는 데 필수적인 라이브러리들이 포함된다.
프로젝트의 런타임에 lodash가 사용된다면 dependencies에 포함되어야 하며 lodash에 들어 있는 라이브러리들도 함께 설치된다.
이러한 현상을 전이 의존성이라고 한다.
devDependencies
현재 프로젝트를 개발하고 테스트하는 데 사용되지만 런타임에는 필요 없는 라이브러리들이 포함된다.
예를 들어, 프로젝트에서 사용 중인 테스트 프레임워크가 devDependencies에 포함될 수 있는 라이브러리다.
peerDependencies
런타임에 필요하긴 하지만 의존성을 직접 관리하지 않는 라이브러리들이다.
단적인 예로 플러그인을 들 수 있다.
세 가지 의존성 중에서 dependencies와 devDependencies가 주로 사용되며 라이브러리를 추가할 때 어떤 종류의 의존성을 사용해야하는지 이해하고 있어야 한다.
타입스크립트는 개발 도구일 뿐 타입 정보는 런타임에 존재하지 않기 때문이며 타입스크립트와 관련된 라이브러리는 일반적으로 devDependencies에 속한다.
모든 타입스크립트 프로젝트에서는 공통적으로 고려해야할 의존성 두 가지를 살펴보겠다.
타입스크립트 자체 의존성을 고려해야 한다. 타입스크립트를 시스템 레벨로 설치할 수 있지만 다음 두 이유로 인해 추천하진 않는다.
팀원들 모두가 항상 동일한 버전을 사용할거란 보장이 없다.
프로젝트 환경 설정 시 별도의 단계가 추가된다.
타입 의존성을 고려해야 한다.
사용하려는 라이브러리에 타입 선언이 존재하지 않아도 타입 정보를 얻을 수 있다.
@types/jquery에는 jquery의 타입 정의가 있고 @types/lodash에는 lodash의 타입 정의가 있다.
요약
타입스크립트를 시스템 레벨로 설치해선 안 된다. 타입스크립트를 프로젝트의 devDependencies에 포함시키고 팀원 모두가 동일한 버전을 사용하도록 해야 한다.
@types 의존성은 dependencies가 아니라 devDependencies에 포함시켜야 한다. 런타임에 @types가 필요한 경우, 별도의 작업이 필요할 수 있다.

item 46. 타입 선언과 관련된 세 가지 버전 이해하기

의존성 관리는 그 자체로도 충분히 복잡한 작업이므로 라이브러리의 전이적 의존성이 호환되는지 깊게 생각하지 않았을 것이다.
타입스크립트를 사용하면 다음 세 가지 사항을 추가로 고려해야 하며 의존성 문제를 알아서 해결해주지 않고 오히려 더 복잡하게 만든다.
라이브러리의 버전
타입 선언(@types)의 버전
타입스크립트의 버전
세 버전 중 하나라도 맞지 않으면 의존성과 상관이 없어보이는 곳에서 오류가 발생할 수 있다.
이런 오류들의 원인을 파악하고 고치기 위해서는 타입스크립트 라이브러리 관리의 메커니즘을 이해하고 있어야 한다.
타입스크립트에서 의존성을 사용하는 방식은 다음과 같은데, 특정 라이브러리를 dependencies로 설치하고, 타입 정보는 devDependencies로 설치한다.
$ npm install react + react@16.8.6 $ npm install --save-dev @types/react + @types/react@16.8.19
Shell
복사
위 예시의 경우 메이저 버전과 마이너 버전이 일치하지만 패치 버전은 일치하지 않는다.
물론 시맨틱 버전 규칙을 적절히 잘 지키고 있다면 두 버전이 일치하므로 큰 문제가 없겠지만 타입 선언 자체에도 버그나 누락이 존재할 수 있으므로 불일치가 발생할 가능성은 얼마든지 있다.
이처럼 실제 라이브러리와 타입 정보의 버전이 별도로 관리되는 방식은 다음 네 가지 문제점을 야기한다.
라이브러리를 업데이트했지만 타입 선언은 업데이트하지 않는 경우
이런 경우, 업데이트된 기능을 사용하려할 때마다 타입 오류가 발생하게 된다.
하위 호환성이 깨지는 변경이 업데이트에서 발생했다면 코드가 타입 체커를 통과하더라도 런타임에 오류가 발생할 수 있다.
해결책으로 타입 선언도 업데이트하는 방법이 있으며 만약 타입 선언 버전이 준비되어있지 않다면 다음 두 가지 해결책을 추가로 고려해볼 수 있다.
보강 기법을 활용하여 사용하려는 새 함수와 메소드의 타입 정보를 프로젝트 자체에 추가해두는 방법
타입 선언의 업데이트를 직접 작성하고 공개하여 커뮤니티에 기여하는 방법
라이브러리보다 타입 선언의 버전이 최신인 경우
보통 타입 정보없이 라이브러리를 사용해 오다가 타입 선언을 설치하려고 할 때 발생한다.
타입 체커는 최신 API를 기준으로 코드를 검사하게 되지만 런타임에 실제로 사용하는 것은 과거 버전으로 불일치 문제가 발생한다.
해결책은 라이브러리와 타입 선언의 버전이 맞도록 라이브러리 버전을 올리거나 타입 선언의 버전을 낮추는 것이다.
프로젝트에서 사용하는 타입스크립트 버전보다 라이브러리에서 필요로 하는 타입스크립트 버전이 최신인 경우
일반적으로 로대시, 리액트, 람다같은 유명 자바스크립트 라이브러리의 타입 정보를 더 정확하게 표현하기 위해 타입 시스템이 개선되고 버전이 상승한다.
이들의 최신 버전을 사용하려면 당연히 타입스크립트 최신 버전을 사용해야 한다.
현재 프로젝트보다 라이브러리에게 필요한 타입스크립트 버전이 높은 상황이라면 @types 선언 자체에서 타입 오류가 발생하게 된다.
이 오류를 해결하려면 타입스크립트 버전을 올리거나 타입 선언의 버전을 원래대로 내리면 된다.
타입스크립트의 특정 버전에 대한 타입 정보를 설치하려면 다음과 같이 작성하면 된다.
$ npm install --save-dev @types/lodash@ts3.1
라이브러리와 타입 선언의 버전을 일치시키는 것이 최선이겠지만 상황에 따라 해당 버전의 타입 정보가 존재하지 않을 수 있다. 그러나 유명한 라이브러리라면 존재할 가능성이 크다.
@types 의존성이 중복되는 경우
만약 @types/foo@tpyes/bar에 의존하고 있는 상황에서 @types/bar가 현재 프로젝트와 호환되지 않는 @types/foo 버전에 의존한다면 npm은 중첩된 폴더에 별도로 해당 버전을 설치하여 문제를 해결하려 시도한다.
런타임에 사용되는 모듈이라면 괜찮을 수 있지만 전역 네임스페이스에 있는 타입 선언 모듈이라면 문제가 발생한다.
이런 경우 보통 @types/foo을 업데이트하거나 @tpyes/bar를 업데이트해서 서로 버전이 호환되도록 한다.
이처럼 @types가 전이 의존성을 가지도록 만드는 것은 종종 문제를 일으키기도 한다.
일반적으로 타입스크립트로 작성된 라이브러리들은 자체적으로 타입 선언을 포함하고 있다.
자체적인 타입 선언은 보통 package.jsontypes 필드에서 .d.ts를 가리키도록 되어 있다.
라이브러리가 타입 선언을 포함하는 경우, 다음과 같은 네 가지 문제점을 가지고 있다.
포함된 타입 선언에 보강 기법으로 해결할 수 없는 오류가 있는 경우, 공개 시점에 잘 동작했지만 타입스크립트 버전이 올라가면서 오류가 발생하는 경우
이 경우, 타입 선언이 포함되어 있으므로 @types의 버전 선택이 불가능하다..
프로젝트 내의 타입 선언이 다른 라이브러리의 타입 선언에 의존하는 경우
보통은 의존성이 devDependencies에 존재하는데, 프로젝트를 배포하는 경우, 해당 의존성은 공개되지 않아 프로젝트를 다운받는 다른 사용자들은 타입 오류를 맞이하게 된다.
프로젝트의 과거 버전에 있는 타입 선언에 문제가 있는 경우
과거 버전으로 돌아가서 패치 업데이트를 수행해야 한다.
타입 선언의 패치 업데이트를 자주 하기 어렵다.
앞서 살펴보면 react 예제에서 라이브러리 자체보다 타입 선언에 대한 패치 업데이트가 13번이나 더 발생했다.
독립적이기 때문에 개별로 업데이트가 어렵지 않다.
잘 작성된 타입 선언은 라이브러리를 올바르게 사용하는데 도움을 주며 생산성 역시 크게 향상시킨다.
요약
@types 의존성과 관련된 세 가지 버전이 있다.
라이브러리 버전, @types 버전, 타입스크립트 버전
라이브러리를 업데이트하는 경우, 해당 타입 선언도 업데이트해야 한다.
타입 선언을 라이브러리에 포함하는 경우와 DefinitelyTyped에 공개하는 것 사이의 장단점을 이해해야 한다.
타입스크립트로 작성된 라이브러리라면 타입 선언을 자체적으로 포함하는 것이 낫다.
자바스크립트로 작성된 라이브러리라면 타입 선언을 DefinitelyType에 공개하는 것이 낫다.

item 47. 공개 API에 등장하는 모든 타입을 익스포트하기

서드파티의 모듈에서 익스포트되지 않은 타입 정보가 필요한 경우, 타입 간의 매핑을 해주는 도구를 사용하면 된다.
interface SecretName { first: string; last: string; } interface SecretSanta { name: SecretName; gift: string; } export function getGift(name: SecretName, gift: string): SecretSanta { // ... }
TypeScript
복사
위와 같이 어떤 타입을 숨기고 싶어서 익스포트하지 않은 경우, 해당 라이브러리 사용자는 SecretName 또는 SecretSanta를 직접 임포트할 수 없고 getGift만 임포트 가능하다.
그러타 타입들은 export된 함수 시그니처에 등장하므로 추출해낼 수 있는데 다음처럼 ParametersReturnType 제네릭 타입을 사용하면 된다.
type MySanta = ReturnType<typeof getGift>; // SecretSanta type MyName = Parameters<typeof getGift>[0]; // SecretName
TypeScript
복사
공개 메소드에 등장한 어떤 형태의 타입이든 익스포트하는 것이 사용자에게 더 편의성을 제공해준다.

item 48. API 주석에 TSDoc 사용하기

다음처럼 인사말을 생성하는 타입스크립트 함수가 있다고 해보자.
// Generate a greeting. Result is formatted for display. function greet(name: string, title: string) { return `Hello ${title} ${name}`; }
TypeScript
복사
주석을 통해 함수가 어떤 기능을 수행하는지 쉽게 알 순 있다. 하지만 사용자를 위한 문서라면 JSDoc 스타일의 주석으로 만드는 것이 좋다.
/** Generate a greeting. Result is formatted for display. */ function greetJSDoc(name: string, title: string) { return `Hello ${title} ${name}`; }
TypeScript
복사
이렇게 대부분의 편집기는 함수가 호출되는 곳에서 함수에 붙어 있는 JSDoc 스타일의 주석을 툴팁으로 표시해주기 때문이다.
외에도 @param@returns와 같은 일반적인 규칙을 사용할 수도 있다.
타입스크립트 관점에서는 TSDoc이라고 부르기도 한다.
/** * Generate a greeting. * @param name Name of the person to greet * @param salutation The person's title * @returns A greeting formatted for human consumption. */ function greetFullTSDoc(name: string, title: string) { return `Hello ${title} ${name}`; }
TypeScript
복사
타입 정의에 TSDoc을 사용할 수도 있다.
/** 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; }
TypeScript
복사
TSDoc은 마크다운 형식을 지원하므로 굵기, 기울임 등이 가능하다.
훌륭한 주석은 산문 형식이 아니다. 간단히 요점만 언급하자.
JSDoc에도 타입 정보를 명시하는 규칙이 존재하지만 타입스크립트에서는 타입 정보가 코드에 존재하기 때문에 TSDoc을 작성하는 경우, 타입 정보를 기재해서는 안 된다.
요약
export된 함수, 클래스, 타입에 주석을 달아야할 때 JSDoc/TSDoc 형태를 사용하자.
편집기가 호출되는 곳에서 주석 정보를 툴팁으로 제공한다.
@param, @returns 구문을 사용할 수 있으며 또한 문서 서식을 위해 마크다운을 사용할 수 있다.
TSDoc의 경우, 주석에 타입 정보를 포함하면 안 된다.

item 49. 콜백에서 this에 대한 타입 제공하기

let이나 const로 선언된 변수는 블록, 정적 범위인 반면 this는 동적 범위로 정의된 방식이 아닌 호출된 방식에 따라서 달라진다.
var 키워드는 함수 범위로 생성된 함수에서만 사용할 수 있으며 함수 내부에서 생성되지 않은 경우엔 전역 범위를 가지게 된다.
렉시컬 스코프는 정적 스코프라고도 하며 중첩된 함수 그룹에서 내부 함수가 상위 범위의 변수 및 기타 리소스에 액세스할 수 있음을 의미한다.
함수 선언 시점에 상위 범위가 결정되어 해당 변수에 접근가능하다.
this는 전형적으로 객체의 현재 인스턴스를 참조하는 클래스에서 가장 많이 사용된다.
class C { vals = [1, 2, 3]; logSquares() { for (const val of this.vals) { console.log(val * val); } } } const c = new C(); c.logSquares(); // output 1 4 9
TypeScript
복사
만약 logSquares를 외부 변수에 추가하고 호출하면 어떻게 될까
const c = new C(); const method = c.logSquares; // Uncaught TypeError: Cannot read property 'vals' of undefined method();
TypeScript
복사
c.logSquares()가 실제로는 두 가지 작업을 수행하기 때문에 문제가 발생한다. C.prototype.logSquares를 호출하고 this의 값을 c로 바인딩한다.
이 코드에서 logSqaures의 참조 변수를 사용하므로 두 작업을 분리하였고 this의 값은 undefined로 설정된다.
⇒ 근데 이런식이면 static을 사용하는게 더 나을것 같은데..
콜백 함수에서 this 값을 사용해야한다면 this는 API의 일부로써 명시되어야 하므로 타입 선언에 반드시 포함되어야 한다.
Node에서 thismodule.exports인데, 파일을 모듈로 사용할 수 있게 해주는 객체다.
요약
자바스크립트에서 this 바인딩이 동작하는 원리를 이해해야 한다.
콜백 함수에서 this 매개변수를 사용해야 한다면 타입 정보를 명시해야 한다.

item 50. 오버로딩 타입보다는 조건부 타입을 사용하기

다음 함수에 타입 정보를 추가해보자.
function double(x) { return x + x; }
TypeScript
복사
이 함수에는 string 또는 number 타입의 매개변수가 들어올 수 있으므로 다음과 같이 유니온 타입(string | number)을 추가할 수 있다.
이는 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"
TypeScript
복사
이 경우 타입이 과하게 구체적이다.
여러 타입 선언으로 분리하는 방법도 있다.
function double(x: number): number; function double(x: string): string; function double(x: any) { return x + x; } const num = double(12); // Type is number const str = double('x'); // Type is string
TypeScript
복사
함수 타입이 명확해졌지만 문제가 발생한다. string이나 number 타입의 값으로는 문제없이 동작하지만 유니온 타입 관련해서 문제가 발생한다.
오버로딩 타입을 하나 더 추가해서 문제를 해결할 수도 있지만 가장 좋은 해결책은 조건부 타입을 사용하는 것이다.
조건부 타입은 타입 공간의 if 구문과 유사하다.
function double<T extends number | string>( x: T ): T extends string ? string : number; function double(x: any) { return x + x; }
TypeScript
복사
이는 제네릭을 사용한 예제와 유사하지만 반환 타입이 덜 구체적이고 정교하게 추론된다.
조건부 타입은 자바스크립트의 삼항 연산자처럼 사용하면 된다.
const num = double(12); // number const str = double('x'); // string // function f(x: string | number): string | number function f(x: number|string) { return double(x); }
TypeScript
복사
유니온에 조건부 타입을 적용하면 조건부 타입의 유니온으로 분리되기 대문에 number|string의 경우에도 동작한다.
만약 Tnumber|string이면 타입스크립트는 조건부 타입을 다음 단계로 해석한다.
(number|string) extends string ? string : number -> (number extends string ? string : number) | (string extends string ? string : number) -> number | string
TypeScript
복사
오버로딩 타입이 작성하기는 쉽지만 조건부 타입은 개별 타입의 유니온으로 일반화하기 때문에 타입이 더 정확하게 추론된다.
오버로딩의 경우, 타입이 독립적으로 처리된다.
조건부 타입은 타입 체커가 단일 표현식으로 받아들이기 때문에 유니온 문제를 해결할 수 있다.
오버로딩 타입을 사용하고 있다면 조건부 타입을 사용해서 개선할 수 있을지 검토해보는 것도 좋다.
요약
오버로딩 타입보다 조건부 타입을 사용하는 것이 좋다. 조건부 타입은 추가적인 오버로딩 구문없이 유니온 타입을 지원할 수 있다.

item 51. 의존성 분리를 위해 미러 타입 사용하기

CSV 파일을 파싱하는 기능을 제공하는 프로젝트를 배포한다고 가정해보자.
이때 반환값을 특정 NodeJS에 의존하는 Buffer로 선언했다면 이는 devDependencies에 포함되고 이는 다음 두 그룹의 사용자에게 문제를 야기한다.
@types와 무관한 자바스크립트 개발자
NodeJS와 무관한 타입스크립트 개발자
두 그룹은 이 라이브러리를 이용하기 위해 다른 라이브러리를 사용하거나 타입 선언을 사용해야하므로 혼란스러울 것이다.
이를 해결하기 위해 구조적 타이핑을 사용하면 된다.
interface CsvBuffer { toString(encoding?: string): string; } function parseCSV(contents: string | CsvBuffer): {[column: string]: string}[] { // ... }
TypeScript
복사
위와 같이 @types/node에 있는 Buffer 선언을 사용하지 않고 필요한 메소드와 속성만 따로 작성하여 대체할 수 있다.
요약
필수가 아닌 의존성을 분리하는 경우, 구조작 타이핑을 사용하라.
공개한 라이브러리를 사요하는 자바스크립트 사용자가 @types 의존성을 가지지 않게 하며 웹 개발자가 NodeJS 의존성을 가지지 않게 해야 한다.

item 52. 테스팅 타입의 함정에 주의하기

타입스크립트의 타입은 동적이기 때문에 라이브러리의 기능을 제공할 때, 충분한 테스트를 통해 의도된 타입을 제공하는지 확인해야 한다.
일반적으로 변수를 도입하는 대신 헬퍼 함수를 정의하여 이를 수행할 수 있다.
function assertType<T>(x: T) {} assertType<number[]>(map(['john', 'paul'], name => name.length));
TypeScript
복사
다만 해당 코드는 두 타입이 동일한지 체크하는 것이 아니라 할당 가능성을 체크하고 있다. 따라서 다음과 같은 경우도 정상이다.
const n = 12; assertType<number>(n); // OK
TypeScript
복사
그러나 객체의 타입을 체크하는 경우 다음과 같이 문제가 발생할 수 있다.
const beatles = ['john', 'paul', 'george', 'ringo']; assertType<{name: string}[]>( map(beatles, name => ({ name, inYellowSubmarine: name === 'ringo' }))); // OK
TypeScript
복사
이 코드에서 map은 {name: string, inYellowSubmarine: boolean} 객체의 배열을 반환하는데, 이는 {name: string}[]에 할당가능하지만 inYellowSubmarine 속성에 대한 부분은 체크되지 않는다.
ParametersReturnType 제네릭 타입을 사용하여 함수의 매개변수 타입과 반환 타입만 분리하여 테스트하는 것이 좋다.
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
TypeScript
복사
요약
타입을 테스트할 때는 함수 타입의 동일성(equality)과 할당 가능성(assignability)의 차이점을 알고 있어야 한다.
콜백이 있는 함수를 테스트할 때, 콜백 매개변수의 추론된 타입을 체크해야 한다. 또한 this가 API의 일부분이라면 역시 테스트해야 한다.
타입 관련된 테스트에서 any를 주의해야 한다. 더 엄격한 테스트를 위해 dtslint 같은 도구를 사용하는 것이 좋다.