이번 장을 공부하면..
npm
자바스크립트 라이브러리 저장소(npm 레지스트리)와, 프로젝트가 의존하고 있는 라이브러리들의 버전을 지정하는 방법(package.json)을 제공
3가지의 의존성 관리법 → package.json 파일 내 별도의 영역에 존재
dependencies
- 현재 프로젝트를 실행하는 데 필수적인 라이브러리들이 포함
- 전이 의존성 발생
→ 다른 사용자가 프로젝트를 설치한다면,dependencies
에 들어 있는 라이브러리도 함께 설치됨
devDependencies
- 프로젝트를 개발하고 테스트하는 데, 사용하지만 런타임에는 필요 없는 라이브러리
- 전이 의존성이 발생하지 않는다.
peerDependencies
- 런타임에 필요하긴 하지만, 의존성을 직접 관리 하지 않는 라이브러리
- ex) 플러그인 → 제이쿼리의 플러그인은 다양한 버전의 제이쿼리와 호환되므로 실제 프로젝트에서 선택하도록 만들 때 사용한다.
타입스크립트와 관련된 라이브러리는 일반적으로 **devDependencies
** 에 속한다.
→ TS는 개발 도구일 뿐이고 타입 정보는 런타임에 존재하지 않음
타입스크립트 프로젝트에서 고려해야 할 의존성 두 가지
- 타입스크립트를
devDependencies
로 설치해 팀원들 모두 동일한 버전의 타입스크립트를 사용할 수 있도록 하자.- 타입스크립트 컴파일러 실행
npx tsc
- DefinitelyTyped의 타입 정의들은 npm 레지스트리의 @types 스코프에 공개됨
- @types 라이브러리는 타입 정보만 포함
- @types 의존성은
devDependencies
에 있어야 함(항상 유효하지는 않음 아이템46)
→ 일반적으로 라이브러리를dependencies,
타입정보는devDependencies
설치
🎯 요약
의존성 관리 방법의 차이를 알아두고, TS와 관련된 대부분 라이브러리는 devDependencies
에 속함
→ 세가지 버전 중 하나라도 맞지 않으면 오류가 발생할 수 있다.
라이브러리는 업데이트했지만 타입 선언은 업데이트 하지 않은 경우
라이브러리보다 타입 선언의 버전이 최신인 경우
프로젝트에서 사용하는 타입스크립트 버전보다 라이브러리에서 필요로 하는 타입스크립트 버전이 최신인 경우
@types 의존성이 중복될 수 있다.
일반적으로 ts라이브러리들은 자체적으로 타입 선언을 포함(번들링)하게 된다. → index.d.ts
파일을 가리킴.
→ 그러나 이러한 번들링 방식은 부수적인 4가지 문제점을 가짐
🎯 요약
타입스크립트 사용시 라이브러리, 타입 선언, 타입스크립트 사이의 버전들을 고려해야 한다.
🎯 요약
라이브러리 제작자는 라이브러리 사용자가 타입을 사용하기 쉽게 익스포트 해야 한다.
// 인라인 주석
/** JSDoc 주석 */
/**
* This _interface_ has **three** properties:
* 1. x
* 2. y
* 3. z
*/
interface Vector3D {
x: number;
y: number;
z: number;
}
export default {};
마크 다운
🎯 요약
익스포트된 함수, 클래스, 타입에 주석을 달 때는 JSDoc/TSDoc를 사용하자.
함수 호출 방식 | this 바인딩 |
---|---|
일반 함수 호출 | 전역 객체 |
메서드 호출 | 메서드를 호출한 객체 |
생성자 함수 호출 | 생성자 함수가(미래에) 생성할 인스턴스 |
Function.prototype.apply/call/bind 메서드에 의한 간접 호출 | Function.prototype.apply/call/bind 메서드에 첫 번째 인수로 전달한 객체 |
apply/call
메서드는 함수를 호출하면서 첫 번째 인수로 전달한 특정 객체를 호출한 함수의 this
에 바인딩한다. (apply
는 배열로 묶어 전달, call
은 리스트 형식으로 전달)
bind
메서드는 메서드의 this
와 메서드 내부의 중첩 함수 또는 콜백 함수의 this
가 불일치하는 문제를 해결하기 위해 유용하게 사용된다.
작성 중인 라이브러리에 this
를 사용하는 콜백 함수가 있다면, this 바인딩 문제를 고려해야 한다.
// this 바인딩 문제는 콜백 함수의 매개변수에 this를 추가하고,
// 콜백 함수를 call로 호출해서 해결
function addKeyListener(
el: HTMLElement,
fn: (this: HTMLElement, e: KeyboardEvent) => void
) {
el.addEventListener('keydown', e => {
fn.call(el, e)
})
}
// 콜백 함수의 첫 번째 매개변수에 있는 this는 특별하게 처리 됨
// call을 제거 해 봄
function addKeyListener(
el: HTMLElement,
fn: (this: HTMLElement, e: KeyboardEvent) => void
) {
el.addEventListener("keydown", (e) => {
fn(el*,* e); //❌
//1개의 인수가 필요한데 2개를 가져왔습니다.
});
}
// 콜백 함수의 매개변수에 this를 추가하면 this 바인딩을 체크할 수 있다.
function addKeyListener(
el: HTMLElement,
fn: (this: HTMLElement, e: KeyboardEvent) => void
) {
el.addEventListener("keydown", (e) => {
fn(e); //this 바인딩 체크해준다.
//'void' 형식의 'this' 컨텍스트를
// 메서드의 'HTMLElement' 형식 'this'에 할당할 수 없습니다
});
}
this
를 참조할 수 있고 완전한 타입 안정성도 얻을 수 있다.declare let el: HTMLElement
addKeyListener(el, function (e) {
this.innerHTML // 정상, "this"는 HTMLElement 타입
})
// 콜백을 화살표 함수로 작성하고, this를 참조하면 타입스크립트가 오류를 잡아낸다.
class Foo {
registerHandler(el: HTMLElement) {
addKeyListener(el, e => {
this.innerHTML
// ~~~~~~~~~ 'Foo' 유형에 'innerHTML' 속성이 없습니다.
})
}
}
🎯 요약
this
바인딩 동작 원리를 이해하고, 콜백 함수에서 this
사용 시, 타입 정보를 명시하자.
유니온 타입 추가, 제너릭 사용, 오버로딩 사용, 조건부 타입 사용
function double(x){
return x + x;
}
// 1. 유니온 타입 추가 -> 타입이 모호함
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
// 2. 제너릭 사용 -> 타입이 너무 과하게 구체적
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"
// 3. 오버로딩 사용 -> 유니온 타입 관련해서 문제 발생
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
function f(x: number | string) {
return double(x)
// ~ Argument of type 'string | number' is not assignable
// to parameter of type 'string'
}
// 4. 조건부 타입 사용(Best)
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
// function f(x: string | number): string | number
function f(x: number | string) {
return double(x)
}
// T가 number| string라면, 조건부 타입을 다음 단계로 해석
// (number | string) extends string ? string : number
// -> (number extends string ? string : number) |
// (string extends string ? string : number)
// -> number | string
🎯 요약
오버로딩 타입보다는 조건부 타입을 사용하자.
//CSV 파일 파싱 함수 예시
function parseCSV(contents: string | Buffer): {[column: string]: string}[] {
//...
return [{"key": "value"}];
}
// Buffer 타입 정의는 @types/node 에서 얻을 수 있다.
// 그러나 라이브러리 사용자가 ts 를 사용하지 않거나, nodeJS 와 무관한 개발자라면
// 라이브러리에 @types/node 의존성을 추가하는 것은 비효율적이다.
// Buffer 인터페이스에서 실제 필요한 부분만 떼어 내어 명시(미러링)
interface CsvBuffer {
toString(encoding: string): string;
}
function parseCSV(contents: string | CsvBuffer): {[column: string]: string}[] {
//...
return [{"key": "value"}];
}
구조적 타이핑
어떤 타입에 들어있는 모든 요소를 가지고 있기만 하면 그 타입에 할당 가능하다.
- 미러 타입으로 작성해 줘야 할 부분이 많다면 그냥 의존성(@types)을 추가해주는 게 낫다.
🎯 요약
의존성 분리를 위해 미러링을 고려해 보자.
라이브러리나 프로젝트를 패키지화해서 공개를 하려면 타입 선언도 테스트를 거쳐야 한다.
→ 타입 선언을 테스트하기는 매우 어렵다..
함수를 실행만 하는 테스트 코드가 의미가 없는 것은 아니지만..
반환 타입을 체크하는 것이 훨씬 좋은 테스트 코드이다.
예시 - 유틸리티 map 함수의 타입 선언을 작성
declare function map<U, V>(array: U[], fn: (u: U) => V): V[]
// 함수를 호출하는 테스트 파일을 작성
map(['2017', '2018', '2019'], v => Number(v));
// 매개변수 오류
map2('2014', v=> Number(v));
// map 내부의 함수가 단일 값이라면, 매개변수에 대한 타입은 잡을 수 있지만,
// 반환값에 대한 체크가 누락되어있음
const lengths: number[] = map(['john', 'paul'], name => name.length);
// -> map의 반환 타입이 number[]임을 보장함
function assertType<T>(x: T) {}
assertType<number[]>(map(['john', 'paul'], name => name.length))
// 두 타입이 동일한지 체크하는 것이 아니라 할당 가능성을 체크하고 있다.
const add = (a: number, b: number) => a + b;
assertType<(a: number; b: number) => number>(add); // 정상
const double = (x: number) => 2 * x;
assertType<(a: number, b: number) => number>(double); // 정상!??
// TS의 함수는 매개변수가 더 적은 함수 타입에 할당 가능하기 때문..
// 콜백 함수에서 흔히 볼 수 있다.
map(array, (name, index, array) => { ...생략});
// 콜백함수는 name, index, array중에서 한 두개만 사용이 가능하다.
// (오히려 세가지 모두 이용하는 경우가 드물다)
Parameter
와 ReturnType
제너릭을 이용하여 매개변수 타입과 반환 타입을 분리하여 두 번 테스트 한다.const double = (x: number) => 2 * x;
let p: Parameters<typeof double> = null!;
assertType<[number, number]>(p);
// … ' [number]' 형식의 인수는 ' [number, number]'
// 형식의 매개변수에 할당될 수 없습니다.
let r: ReturnType<typeof double> = null!;
assertType<number>(r); // 정상
any
를 주의해야 한다.dstlint
같은 도구를 사용하자.DefinitelyTyped
의 타입 선언을 위한 도구이다.(string | number) ≠ (number | string)
🎯 요약
타입 선언을 테스트 하는 것은 어렵지만 해야 한다. dtlint
같은 도구를 사용하자.