이펙티브 타입스크립트 6장

정태호·2023년 8월 28일
0

타입스크립트

목록 보기
11/13
post-thumbnail

타입 선언과 @types

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

npm은 세 가지 종류의 의존성을 구분해서 관리하며, 각각의 의존성은 package.json 파일 내의 별도 영역에 들어있다.

dependencies

  • 현재 프로젝트를 실행하는 데 필수적인 라이브러리들이 포함된다.
  • 런타임에도 사용
  • 다른 사용자가 해당 프로젝트를 설치한다면 이 안에 들어있는 라이브러리들도 함께 설치된다. 전이(transitive)의존성

devDependencies

  • 현재 프로젝트를 개발하고 테스트하는 데 사용
  • 런타임에는 필요없는 라이브러리들이 포함
  • devDependencies에 포함된 라이브러리들은 설치되지 않음

peerDependencies

  • 런타임에 필요하긴 하지만, 의존성을 직접 관리하지 않는 라이브러리들이 포함
  • 일반적으로 사용되지 않음

타입스크립트는 개발 도구일 뿐 타입 정보는 런타임에 존재하지 않기 때문에, 타입스크립트와 관련된 라이브러리는 일반적으로 devDependencies에 속한다.

타입스크립트를 시스템 레벨로 설치하기보단 devDependencies로 관리해야하는 이유

  • 팀원들과 동일한 타입스크립트 버전 보장
  • 프로젝트 셋업 시 단계 단축

요약

  • @types 의존성, 타입스크립트는 devDendencies에 있어야 한다.

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

타입스크립트는 알아서 의존성 문제를 해결해 주기는커녕, 의존성 관리를 오히려 복잡하게 만들기 때문에 다음 세 가지 추가사항을 고려해야 한다.

  • 라이브러리의 버전
  • 타입 선언(@types)의 버전
  • 타입스크립트의 버전

세 가지 버전 중 하나라도 맞지 않으면, 의존성과 상관없어 보이는 곳에서 엉뚱한 오류가 발생할 수 있다.

라이브러리와 타입 정보의 버전이 별도로 관리되는 방식의 문제점

라이브러리를 업데이트했지만 타입 선언은 업데이트하지 않은 경우

  • 라이브러리 업데이트와 관련된 새로운 기능을 사용하려 할 때 타입 오류 발생

타입 선언도 업데이트하여 라이브러리와 버전을 맞추자! 타입 선언의 버전이 준비되지 않았다면 두 가지 선택지가 존재한다.

  • 보강기법을 활용해 타입 정보를 프로젝트 자체에 추가하기
  • 타입 선언의 업데이트를 직접 작성하여 공개

라이브러리보다 타입 선언 버전이 최신인 경우

  • 타입 체커는 최신 API를 기준으로 코드를 검사하게 되지만 런타임에는 과거 버전이 쓰임
  • 해결책은 버전 맞추기

프로젝트에서 사용하는 타입스크립트 버전보다 라이브러리에서 필요로 하는 타입스크립트 버전이 최신인 경우

  • @types 선언 자체에서 타입 오류가 발생하게 됨
  • 해결책은 타입스크립트 버전을 올리거나 라이브러리의 타입 선언 버전을 내리기 또는 라이브러리의 타입 정보를 없애기

@types 의존성 중복

  • 런타임에 사용되는 모듈이라면 괜찮지만, 전역 네임스페이스에 있는 타입 선언 모듈이라면 문제가 발생한다.
    • 중복된 선언, 또는 선언이 병합될 수 없습니다!

타입 선언 중복 발생을 추적하는 방법

npm ls @types/foo

일부 라이브러리, 특히 타입스크립트로 작성된 라이브러리들은 자체적으로 타입 선언을 포함(번들링)한다.

  • package.json의 "types" 필드에서 d.ts 파일을 가리키도록 되어있다.

요약

  • 라이브러리와 @types 버전을 맞추자!
  • 타입스크립트로 작성된 라이브러리라면 타입 선언을 자체적으로 포함하고, 자바스크립트로 작성된 라이브러리라면 타입 선언을 Definitely Typed에 공개하는 것이 좋다.

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

요약

  • 공개 메서드에 등장한 어떤 형태의 타입이든 export 하자. 어차피 사용자가 추출할 수 있으므로, export 하기 쉽게 만드는 것이 좋다.

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

권장되지 않는 예시

// 인사말을 생성합니다. 결과는 보기 좋게 꾸며집니다.
function greet(name: string, title: string){
  return `Hello ${title} ${name}`
}

권장되는 예시

/** 인사말을 생성합니다. 결과는 보기 좋게 꾸며집니다. */
function greetJSDoc(name: string, title: string){
  return `Hello ${title} ${name}`
}

대부분의 편집기는 함수가 호출되는 곳에서 함수에 붙어있는 JSDoc 스타일의 주석을 툴팁으로 표시해준다. 인라인은 X

요약

  • 타입스크립트가 JSDoc(TSDoc) 스타일을 지원하기 때문에 @param과 @returns 같은 일반적 규칙을 사용하여 주석을 달아보자.
  • 타입스크립트에서는 타입 정보가 코드에 있기 때문에 TSDoc에서는 타입 정보를 명시하지 말자.

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

this는 다이나믹 스코프이기 때문에 정의된 방식이 아니라 호출된 방식 에 따라 달라진다.

class C {
    vals = [1, 2, 3];
    logSquares() {
        for (const val of this.vals) {
        	console.log(val * val);
        }
    }
}
const c = new C();
c.logSquares();

코드를 실행하면 다음처럼 출력된다.
1
4
9

logSquares를 외부 변수에 넣고 호출해보자.

const c = new C();
const method = c.logSquares;
method();

코드의 출력 결과는 undefined의 'vals' 속성을 읽을 수 없습니다. 라는 오류가 발생한다.

이유가 뭘까?

  • c.logSquares()가 실제로 두 가지 작업을 수행하기 때문이다.
  1. C.prototype.logSquares 호출
  2. this의 값을 c로 바인딩함

즉 두 가지 작업을 분리했고, logSquares는 반환값이 없으므로 undefined가 할당된다.

GPT는?

  • logSquares 메서드는 반환값이 없으므로 method 변수에 할당할 수 없습니다.
  • method()를 호출하려고 할 때 오류가 발생합니다. 이 부분은 이전에 언급한 대로 logSquares 메서드의 반환값이 없기 때문입니다.

this 바인딩

call 사용

const c = new C();
const method = c.logSquares;
method.call(c) // 정상

call 메서드의 인자로 전달되는 c는 해당 메서드 내부에서의 this 값을 지정하기 위한 것이므로, 실제로는 c 객체의 메서드인 것처럼 작동하게 되므로 오류가 발생하지 않는다.

this 바인딩을 콜백 함수에서 쓰는 예시

class ResetButton {
    render() {
    	return makeButton({text: 'Reset', onClick: this.onClick});
    }
    onClick() {
    	alert(`Reset ${this}`);
    }
}

ResetButton에서 onClick을 호출하면 this바인딩 문제로 인해 Reset이 정의되지 않았습니다. 라는 경고가 뜬다.

알맞게 사용한 예시

function addKeyListener2(
    el: HTMLElement,
    fn: (this: HTMLElement, e: KeyboardEvent) => void
    ) {
        el.addEventListener('keydown', e => {
        fn.call(el, e);
    })
}

콜백 함수의 매개변수에 this를 추가하면 this 바인딩이 제대로 체크기되기 때문에 실수를 방지할 수 있다.

function addKeyListener(
el: HTMLElement,
fn: (this: HTMLElement, e: KeyboardEvent) => void
) {
    el.addEventListener('keydown', e => {
    fn(el, e);
    // ~ 1개의 인수가 필요한데 2개를 가져왔습니다.
    });
}

참고

콜백 함수에서 this 값을 사용한다면 this는 API의 일부가 되는 것이기 때문에 반드시 타입 선언에 포함해야 한다.

요약

  • this 바인딩이 동작하는 원리를 이해해야 한다.
  • 콜백 함수에서 this를 사용한다면, 타입 정보를 반드시 명시해야 한다.

이 아이템은 잘 모르겠다...


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

function double(x) {
	return x + x;
}

double 함수에는 string 또는 number 타입의 매개변수가 들어올 수 있으므로 함수 오버로딩 개념을 사용해 타입 정보를 추가해보자.

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

제너릭을 이용하면?

function double<T extends number|string>(x: T): T;
function double(x: any) { return x + x; }

const num = double(12); // 타입이 12
const str = double('x'); // 타입이 'x'

이렇게 사용하게 되면 타입을 너무 구체적으로 반환하게 된다.

타입스크립트는 오버로딩 타입 중에서 일치하는 타입을 찾을 때까지 순차적으로 검색한다.

function double(x: number): number;
function double(x: string): string;
function double(x: any) { return x + x; }

const num = double(12); // 타입이 12
const str = double('x'); // 타입이 'x'
function f(x: number|string) {
  return double(x); // error 
  // string|number 형식의 인수는 string 형식의 매개변수에 할당될 수 없습니다.
}

오버로딩 타입의 마지막 선언까지 검색했을 때, 유니온 타입이 없으므로 에러가 발생한다.

가장 좋은 해결책은?

조건부 타입을 사용하기

  • 타입 공간의 if 구문과 같다.
function double<T extends number | string>(
	x: T
): T extends string ? string : number;
function double(x: any) { return x + x; }

조건부 타입은 자바스크립트의 삼항 연산자처럼 사용하면 된다.

  • T가 string의 부분 집합이면 반환 타입은 string
  • 그 외에는 반환 타입이 number

이로 인해 앞선 모든 예제가 동작한다.

각각의 오버로딩 타입이 독립적으로 처리되는 반면, 조건부 타입은 타입 체커가 단일 표현식으로 받아들이기 때문에 유니온 문제를 해결할 수 있다.

요약

  • 오버로딩 타입보다 조건부 타입을 사용하는 것이 좋다. 조건부 타입은 추가적인 오버로딩 없이 유니온 타입을 지원한다.

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

CSV파일을 파싱하는 라이브러리를 작성한다고 가정해보자. NodeJS 사용자를 위해 매개변수에 Buffer 타입을 허용하고 @types/node를 통해 Buffer의 타입을 정의했다.

function parseCSV(contents: string | Buffer): {[column: string]: string}[] {
    if (typeof contents === 'object') {
        // 버퍼인 경우
        return parseCSV(contents.toString('utf8'));
    }
// ...
}

다음 두 그룹의 라이브러리 사용자들에게 문제가 생긴다.

  • @types와 무관한 자바스크립트 개발자
  • NodeJS와 무관한 타입스크립트 개발자

Buffer타입은 NodeJs 개발자만 필요로 한다. @types/node 또한 NodeJS와 타입스크립트를 동시에 사용하는 개발자만 필요로 한다.

각자가 필요한 모듈만 사용할 수 있도록 구조적 타이핑을 적용할 수 있다. 예를 들면 @types/node에 있는 Buffer 선언을 사용하지 않고, 필요한 메서드와 속성만 별도로 작성할 수 있다.

요약

  • 작성 중인 라이브러리가 구현과 무관하게 타입에 의존한다면, 필요한 선언부만 추출하여 작성중인 라이브러리에 넣는 것(미러링) 을 고려해 보는 것도 좋다.

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

프로젝트를 공개하려면 테스트 코드를 작성하는 것은 필수이며, 타입 선언 또한 테스트를 거쳐야한다.

dtslint 또는 타입 시스템 외부에서 타입을 검사하는 유사한 도구를 사용하는 것이 안전하고 간단하다.

타입선언이 예상한 타입으로 결과를 내는지 테스트 해보기 위해선 함수를 호출하는 테스트 파일을 작성해야한다.

잘못된 예시

declare function map<U, V>(array: U[], fn: (u: U) => V): V[];

map(['2017', '2018', '2019'], v=> Number(v));

// 매개변수 오류
map2('2023', v=> Number(v));

map내부의 함수가 단일값 이라면 매개변수에 대한 타입은 잡을 수 있지만 반환값에 대한 체크가 누락되어있다.

반환 타입을 체크하는 것이 훨씬 좋은 테스트 코드이다.

특정 타입의 변수에 할당하여 체크

const lengths: number[] = map(['john', 'paul'], name => name.length);

할당을 사용하는 방법의 두 가지 문제점

  • 불필요한 변수를 만들어야 한다. -> 위 코드의 lengths
  • 두 타입이 동일한지 체크하는 것이 아닌 할당 가능성을 체크하고 있다.
const n = 12;
assertType<number>(n); // 정상

n 심벌을 조사해 보면, 타입이 실제로 12다. 12는 number의 서브타입이고 할당 가능성 체크를 통과한다.

객체의 타입을 체크하는 경우 문제가 발생한다.

const beatles = ["john", "paul", "george", "ringo"];
assertType<{ name: string }[]>(
  map(beatles, (name) => ({
    name,
    inYellowSubmarine: name === "ringo",
  }))
); // 정상

반환된 배열은 {name: string}[]에 할당 가능하지만, inYellowSubmarine 속성에 대한 부분이 체크되지 않았다.

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); // 정상!?

타입스크립트의 함수는 매개변수가 더 적은 함수 타입에 할당 가능하기 때문에 위 코드에 double 함수의 체크가 성공한다..

lodash의 map함수 예시

map(array, (name, index, array) => { ...생략});

콜백함수는 name, index, array중에서 한 두개만 보통 사용한다. (세 개를 모두 사용하는 경우는 드물다)

만약 매개변수의 수가 맞지 않는 경우까지 체크한다면 매우 많은 곳에서 타입스크립트 + 콜백함수 타입 오류가 발생할 것 이다.

해결 방법

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); // 정상

위 예제는 ParameterReturnType 제너릭을 이용하여 매개변수 타입과 반환 타입을 분리하여 두 번 테스트 한다.

DefinitelyTyped의 타입 선언을 위한 도구는 dtslint 이다.

요약

  • 타입 선언을 테스트 하는 것은 어렵지만 반드시 해야하는 작업이다.
  • 콜백이 있는 함수를 테스트할 때, 콜백 매개변수의 추론된 타입을 체크해야 한다.
  • 엄격한 테스트를 위해 dtslint 같은 도구를 사용하는 것이 좋다.
profile
주니어 프론트엔드 개발자가 되고 싶습니다!

0개의 댓글