아이템 45. devDefendencies에 typescript와 @types 추가하기
요약
- typescript를 시스템 레벨(npm install -g typescript)로 설치하면 안 된다. typescript를 프로젝트의 devDependencies 에 포함시키고 팀원 모두가 동일한 버전을 사용하도록 해야 한다.
- @types 의존성은 dependencies가 아니라 devDependencies에 포함시켜야 한다. 런타임에 @types 가 필요한 경우라면 별도의 작업이 필요할 수 있다.
예시
//package.json
{
...
"dependencies": {
"lodash": "^4.17.21"
},
"devDependencies": {
"typescript": "^5.0.2",
"@types/lodash": "^4.14.192",
"@types/jest": "^29.5.0",
"jest": "^29.5.0"
}
}
아이템 46. 타입 선언과 관련된 세 가지 버전 이해하기
요약
- 타입 선언과 관련된 세가지 버전이 있다. 라이브러리 버전, @types 버전, typescript 버전이다. 이 세가지의 버전에 따라 타입스크립트 생태계에선 더욱 의존성이 관리가 복잡하게 되었다.
- 라이브러리를 업데이트하는 경우, 해당 @types 역시 업데이트를 하자.
- 라이브러리를 만들때 타입 선언을 관리하는 방법은 두가지가 있다.
- 방법1) 타입 선언을 DefinitelyTyped에 공개(= @types)해 라이브러리와 따로 두기
- 방법2) 타입 선언을 라이브러리에 포함시키기
- 공식적인 권장사항은 타입스크립트로 작성된 라이브러리인 경우만 타입 선언을 자체적으로 포함하는 것이다. 그 외의 경우(자바스크립트로 작성된 라이브러리)라면 타입을 DefinitelyTyped에 공개해 따로 관리하는게 좋다.
- DefinitelyTyped : 타입스크립트 커뮤니티에서 유지보수하고 있는 자바스크립트 라이브러리 타입을 정의한 모음(깃헙 리포지토리)
아이템 47. 공개 API 에 등장하는 모든 타입을 익스포트하기
요약
- 공개 메서드에 등장한 어떤 형태의 타입이든 익스포트 하자. 어차피 라이브러리 사용자가 추출할 수 있으므로 익스포트 하기 쉽게 만드는 것이 좋다. (어차피 숨기는게 거의 불가능하다.)
예시
interface SecretName {
first: string;
last: string;
}
interface SecretSanta {
name: SecretName;
gift: string;
}
export const getGift = (name: SecretName, gift: string): SecretSanta => {
//...
};
// 타입 추출하기
type MySanta = ReturnType<typeof getGift>; //SecretSanta
type MyName = Parameters<typeof getGift>[0]; //SecretName
아이템 48. API 주석에 TSDoc 사용하기
요약
- 익스포트된 함수, 클래스, 타입에 주석을 달 때는 JSDoc/TSDoc 형태를 사용하자. 편집기가 주석 정보를 표시해 준다.
- @param, @returns 구문과 문서 서식을 위해 마크 다운을 사용할 수 있다.
- TS 주석에 타입 정보를 포함하지 말자(아이템39). 타입 정보는 타입구문으로만 이해할 수 있게 하는 게 더 좋다.
예시
//js 파일에서의 JSDoc
/**
* 책 데이터를 가져오는 함수
* @param {number} bookId **도서 ID**
* @returns {title: string, author: string} 도서명, 저자
*/
function getBookData(bookId) {
//...
return {
title: '이펙티브 타입스크립트',
author: '댄 밴더캄',
publisher: '프로그래밍 인사이트'
};
}
//ts 파일에서의 TSDoc
//타입 정보 제거 -> 어차피 기본으로 보여준다.
/**
* 책 데이터를 가져오는 함수
* @param bookId **도서 ID**
* @returns 도서명, 저자
*/
function getBookData(bookId: number) {
//...
return {
title: '이펙티브 타입스크립트',
author: '댄 밴더캄'
}
}
아이템 49. 콜백에서 this에 대한 타입 제공하기
요약
- this 바인딩이 동작하는 원리를 이해해야 한다.
- 콜백 함수에서 this를 사용해야 한다면, 타입 정보를 명시해야 한다. this는 동적 스코프(호출된 방식에 따라 값이 달라진다)라 예상하기 어렵기 때문이다.
예시
//콜백 함수 첫 번째 매개변수에 있는 this는 특별하게 처리 된다.
//-> 실제로 인자로 넣을 필요는 없다. this 바인딩 체크용이다.
//콜백 함수의 매개변수에 this를 추가하면 this 바인딩을 체크할 수 있다.
function addKeyListener(
el: HTMLElement,
fn: (this: HTMLElement, e: KeyboardEvent) => void
) {
el.addEventListener("keydown", (e) => {
fn(el, e); //❌
//1개의 인수가 필요한데 2개를 가져왔습니다.
});
}
function addKeyListener2(
el: HTMLElement,
fn: (this: HTMLElement, e: KeyboardEvent) => void
) {
el.addEventListener("keydown", (e) => {
fn(e); //this 바인딩 체크해준다.
//'void' 형식의 'this' 컨텍스트를 메서드의 'HTMLElement' 형식 'this'에 할당할 수 없습니다
});
}
//콜백 함수를 call로 호출해서 해결할 수 있다.
function addKeyListener3(
el: HTMLElement,
fn: (this: HTMLElement, e: KeyboardEvent) => void
) {
el.addEventListener("keydown", (e) => fn.call(el, e));
}
아이템 50. 오버로딩 타입보다는 조건부 타입을 사용하기
요약
- 오버로딩 타입보다 조건부 타입을 사용하는 것이 좋다. 조건부 타입은 추가적인 오버로딩 없이 유니온 타입을 지원할 수 있기 때문이다.
예시
//❌ 안 좋은 예: 오버로딩
//타입스크립트는 오버로딩 타입 증에서 일치하는 타입을 찾을 때까지 순차적으로 검색한다.
{
function double(x: number | string): number | string;
function double(x: any) {
return x + x;
}
const num = double(2); // type: string | number
const str = double("x"); // type: string | number
//선언이 틀리진 않았지만 모호하다.
}
{
function double<T extends number | string>(x: T): T;
function double(x: any) {
return x + x;
}
const num = double(2); // type: 2
const str = double("x"); // type: 'x'
//타입이 과하게 구체적이다.
}
{
function double(x: number): number;
function double(x: string): string;
function double(x: any) {
return x + x;
}
const num = double(2); // type: number
const str = double("x"); // type: string
//타입이 명확해졌지만 버그가 발생한다.
function f(x: number | string) {
return double(x); //'string|number' 형식의 인수는 'string'형식의 매개변수에 할당될 수 없습니다.
}
}
//✅ 좋은 예: 조건부 타입 사용
{
function double<T extends number | string>(
x: T
): T extends string ? string : number;
function double(x: any) {
return x + x;
}
}
아이템 51. 의존성 분리를 위해 미러 타입을 사용하기
요약
- 미러링이란, 필요한 선언부만 추출하여 작성 중인 라이브러리에 넣는 것을 말한다. 구조적 타이핑을 응용한 것이다.
- 즉, 필수가 아닌 의존성을 분리할 때는 구조적 타이핑을 사용하면 된다.
- 미러 타입으로 작성해줘야할 부분이 많다면 그냥 의존성(@types)을 추가해주는 게 낫다.
예시
//CSV 파일 파싱 함수 예시
function parseCSV(contents: string | Buffer): {[column: string]: string}[] {
//...
return [{"key": "value"}];
}
//Buffer 타입 정의는 @types/node 에서 얻을 수 있다.
//그러나 그것을 위해 라이브러리에 @types/node 의존성을 추가하는 것은 비효율적이다.
//(라이브러리 사용자가 ts 를 사용하지 않거나, nodeJS 와 무관한 개발자라면 더더욱)
//Buffer 인터페이스에서 실제 필요한 부분만 떼어 내어 명시(미러링)
interface CsvBuffer {
toString(encoding: string): string;
}
function parseCSV(contents: string | CsvBuffer): {[column: string]: string}[] {
//...
return [{"key": "value"}];
}
- 구조적 타이핑이란, 어떤 타입에 들어있는 모든 요소를 가지고 있기만 하면 그 타입에 할당 가능하다는 의미이다.
interface Person {
name: string;
age: number;
}
function fn(x: Person) {
console.log(x);
}
const student = { name: "yoong", age: 30, class: '1-2' };
fn(student); //✅ student class라는 프로퍼티가 있음에도 Person로써 인정 받음(할당 가능)
const animal = { name: "monkey"};
fn(animal); //❌
//'{ name: string; }' 형식의 인수는 'Person' 형식의 매개 변수에 할당될 수 없습니다.
//-> ❗️타입이 좁아지는 건 가능! 타입이 넓어지는 건 불가능
아이템 52. 테스팅 타입의 한정에 주의하기
요약
-
타입을 테스트할 때는 특히 함수 타입의 동일성과 할당 가능성의 차이점을 알고 있어야 한다. (구조적 타이핑)
//선언된 것보다 적은 매개변수를 가진 함수를 할당하는 것은 아무런 문제가 없다. const testFn: (x: string, y: number) => any = (z: string) => 12; //정상 const testFn: (x: string) => any = () => 12; //정상
-
콜백이 있는 함수를 테스트할 때 콜백 매개변수의 추론된 타입을 체크해야 한다. this가 API의 일부분이라면 역시 테스트해야 한다.
-
타입 관련된 테스트에서 any를 주의하자. 더 엄격한 테스트를 위해 dtslint같은 도구를 활용하는 것도 좋다. (dtslint 는 할당 가능성이 아닌 심벌 타입을 추출해 글자 자체가 같은지 비교한다.)