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

JIY00N·2023년 8월 31일
0

TypeScript

목록 보기
7/9
post-thumbnail
post-custom-banner

6장 타입 선언과 @types

이번 장을 공부하면..

  1. 타입스크립트에서 의존성이 어떻게 동작하는지와 개념을 알 수 있다.
  2. 의존성 관리 시 발생할 수 있는 문제점과 해결 방법을 알 수 있다.
  3. 타입 선언 파일을 작성 시 도움이 된다.

아이템 45 devDependencies에 typescript와 @types 추가하기


npm

  • 자바스크립트 라이브러리 저장소(npm 레지스트리)와, 프로젝트가 의존하고 있는 라이브러리들의 버전을 지정하는 방법(package.json)을 제공

  • 3가지의 의존성 관리법 → package.json 파일 내 별도의 영역에 존재

  1. dependencies
  • 현재 프로젝트를 실행하는 데 필수적인 라이브러리들이 포함
  • 전이 의존성 발생
    → 다른 사용자가 프로젝트를 설치한다면, dependencies에 들어 있는 라이브러리도 함께 설치됨
  1. devDependencies
  • 프로젝트를 개발하고 테스트하는 데, 사용하지만 런타임에는 필요 없는 라이브러리
  • 전이 의존성이 발생하지 않는다.
  1. peerDependencies
  • 런타임에 필요하긴 하지만, 의존성을 직접 관리 하지 않는 라이브러리
  • ex) 플러그인 → 제이쿼리의 플러그인은 다양한 버전의 제이쿼리와 호환되므로 실제 프로젝트에서 선택하도록 만들 때 사용한다.
  • 타입스크립트와 관련된 라이브러리는 일반적으로 **devDependencies** 에 속한다.
    → TS는 개발 도구일 뿐이고 타입 정보는 런타임에 존재하지 않음

  • 타입스크립트 프로젝트에서 고려해야 할 의존성 두 가지

  1. 타입스크립트 자체 의존성을 고려
  • 타입스크립트를 devDependencies 로 설치해 팀원들 모두 동일한 버전의 타입스크립트를 사용할 수 있도록 하자.
  • 타입스크립트 컴파일러 실행 npx tsc
  1. 타입 의존성(@types)을 고려
  • DefinitelyTyped의 타입 정의들은 npm 레지스트리의 @types 스코프에 공개됨
  • @types 라이브러리는 타입 정보만 포함
  • @types 의존성은 devDependencies 에 있어야 함(항상 유효하지는 않음 아이템46)
    → 일반적으로 라이브러리를 dependencies, 타입정보는 devDependencies 설치

🎯 요약

의존성 관리 방법의 차이를 알아두고, TS와 관련된 대부분 라이브러리는 devDependencies 에 속함

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


  • 타입스크립트를 사용시 세가지 고려 사항
  1. 라이브러리의 버전
  2. 타입 선언(@types)의 버전
  3. 타입스크립트의 버전

→ 세가지 버전 중 하나라도 맞지 않으면 오류가 발생할 수 있다.

  • 라이브러리와 타입 정보의 버전이 별도로 관리되는 방식의 문제점
  1. 라이브러리는 업데이트했지만 타입 선언은 업데이트 하지 않은 경우

    1. 타입 선언도 업데이트 하기
    2. 타입 선언 업데이트를 못하는 경우
      1. 보강 기법을 통한 타입 정보를 프로젝트 자체에 추가
      2. 타입 선언의 업데이트를 직접 작성하고 공개하여 커뮤니티에 기여
  2. 라이브러리보다 타입 선언의 버전이 최신인 경우

    1. 라이브러리 버전을 올리거나 타입 선언의 버전 내리기
  3. 프로젝트에서 사용하는 타입스크립트 버전보다 라이브러리에서 필요로 하는 타입스크립트 버전이 최신인 경우

    1. 프로젝트의 버전을 높이거나 라이브러리 타입 선언의 버전을 원래대로 내림
  4. @types 의존성이 중복될 수 있다.

일반적으로 ts라이브러리들은 자체적으로 타입 선언을 포함(번들링)하게 된다. → index.d.ts 파일을 가리킴.
→ 그러나 이러한 번들링 방식은 부수적인 4가지 문제점을 가짐

  1. 번들된 타입 선언에 보강 기법으로 해결할 수 없는 오류가 있는 경우, 또는 공개 시점에는 잘 동작했지만 타입스크립트 버전이 올라가면서 오류가 발생하는 경우 문제
  2. 프로젝트 내의 타입 선언이 다른 라이브러리의 타입선언에 의존하면 문제 발생
  3. 프로젝트의 과거 버전에 있는 타입 선언에 문제가 있는 경우, 과거 버전으로 돌아가서 패치 업데이트를 해야 함
  4. 타입 선언의 패치 업데이트를 자주 하기는 어려운 문제
  • 공식적인 권장사항은 라이브러리가 TS로 작성된 경우만 타입 선언을 라이브러리에 포함하는 것
  • JS로 작성된 라이브러리는 타입 선언을 DefinitelyTyped에 공개하여 커뮤니티에서 관리하고 유지보수하도록 맡기는 것이 좋다.

🎯 요약

타입스크립트 사용시 라이브러리, 타입 선언, 타입스크립트 사이의 버전들을 고려해야 한다.

아이템 47 공개 API에 등장하는 모든 타입을 익스포트하기


🎯 요약

라이브러리 제작자는 라이브러리 사용자가 타입을 사용하기 쉽게 익스포트 해야 한다.

아이템 48 API 주석에 TSDoc 사용하기


  • 인라인 주석 vs JSDoc
    JSDoc 스타일 주석은 일반적으로 편집기의 툴팁 설명에 표시되지만 인라인 주석은 그렇지 않다.
// 인라인 주석
/** JSDoc 주석 */
  • 익스포트된 함수, 클래스, 타입에 주석을 달 때는 JSDoc/TSDoc 를 사용하자
    → 툴팁으로 주석 정보 파악 용이
  • 마크 다운 사용 가능
  • 주석에 타입 정보는 제외한다.
/**
 * This _interface_ has **three** properties:
 * 1. x
 * 2. y
 * 3. z
 */
interface Vector3D {
  x: number;
  y: number;
  z: number;
}

export default {};

마크 다운

🎯 요약

익스포트된 함수, 클래스, 타입에 주석을 달 때는 JSDoc/TSDoc를 사용하자.

아이템 49 콜백에서 this에 대한 타입 제공하기


this

  • 다이나믹 스코프(Dynamic scope)
  • 다이나믹 스코프의 값은 호출된 방식에 따라 달라진다.
  • this 바인딩(this가 가리키는 값)함수 호출 방식에 의해 동적으로 결정된다.
함수 호출 방식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 사용 시, 타입 정보를 명시하자.

아이템 50 오버로딩 타입보다는 조건부 타입을 사용하기


  • 예제 - double 함수에 타입 정보 추가하기

유니온 타입 추가, 제너릭 사용, 오버로딩 사용, 조건부 타입 사용

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

🎯 요약

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

아이템 51 의존성 분리를 위해 미러 타입 사용하기


  • Buffer
    바이너리 데이터를 효율적으로 다루기 위한 클래스로, 파일의 내용을 바이너리로 읽어들이거나 쓸 수 있는 기능을 제공합니다.
    Node.js 개발자만 필요하다.
//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)을 추가해주는 게 낫다.

🎯 요약

의존성 분리를 위해 미러링을 고려해 보자.

아이템 52 테스팅 타입의 함정에 주의하기


  • 라이브러리나 프로젝트를 패키지화해서 공개를 하려면 타입 선언도 테스트를 거쳐야 한다.
    → 타입 선언을 테스트하기는 매우 어렵다..

  • 함수를 실행만 하는 테스트 코드가 의미가 없는 것은 아니지만..
    반환 타입을 체크하는 것이 훨씬 좋은 테스트 코드이다.

  • 예시 - 유틸리티 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[]임을 보장함
  • 할당 가능성 체크시 주의할 점
  1. 불필요한 변수를 만들어야 한다.
  • 해결책 - 변수를 도입하는 대신 헬퍼 함수를 정의하자.
function assertType<T>(x: T) {}
assertType<number[]>(map(['john', 'paul'], name => name.length))
// 두 타입이 동일한지 체크하는 것이 아니라 할당 가능성을 체크하고 있다.
  1. 두 타입이 동일한지 체크하는 것이 아니라 할당 가능성을 체크하고 있다.
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 함수
map(array, (name, index, array) => { ...생략});
// 콜백함수는 name, index, array중에서 한 두개만 사용이 가능하다. 
// (오히려 세가지 모두 이용하는 경우가 드물다)
  • 해결책 - ParameterReturnType 제너릭을 이용하여 매개변수 타입과 반환 타입을 분리하여 두 번 테스트 한다.
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 같은 도구를 사용하자.
  • dstlint
    DefinitelyTyped의 타입 선언을 위한 도구이다.
    → 특별한 형태의 주석을 통해 동작한다.
    → 할당 가능성을 체크하는 대신 각 심벌의 타입을 추출하여 글자 자체가 같은지 비교한다. (string | number) ≠ (number | string)

🎯 요약

타입 선언을 테스트 하는 것은 어렵지만 해야 한다. dtlint 같은 도구를 사용하자.

profile
블로그 이전 했습니다. https://yoon-log.vercel.app/
post-custom-banner

0개의 댓글