이펙티브 타입스크립트: 6장 타입 선언과 @types

이펙티브 타입스크립트 북스터디 6장 아이템 45-52

아이템 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 는 할당 가능성이 아닌 심벌 타입을 추출해 글자 자체가 같은지 비교한다.)

디지엠유닛원 주식회사

  • 대표이사 권혁태
  • 개인정보보호책임자 정경영
  • 사업자등록번호 252-86-01619
  • 주소
    서울특별시 금천구 가산디지털1로 83, 6층 601호(가산동, 파트너스타워)
  • 이메일 web@dgmit.com