January 6, 2022 • ☕️☕️ 10 min read
타입 선언과 @types
npm의 의존성 구분
타입스크립트는 개발 도구일 뿐이고 타입 정보는 런타임에 존재하지 않기 때문에, 타입스크립트와 관련된 라이브러리는 일반적으로 devDependencies에 속한다
타입스크립트 프로젝트에서 고려해야 할 의존성
npm install
시 팀원들 모두 항상 정확한 버전의 타입스크립트 설치 가능@types
)을 고려DefinitelyTyped
에서 라이브러리에 대한 타입 정보를 얻을 수 있다@types
라이브러리는 타입 정보만 포함하고 있으며 구현체는 포함하지 않는다@types
의존성은 devDependencies에 있어야 한다타입스크립트 사용 시 고려해야 할 사항
@types
)의 버전타입스크립트에서 의존성을 사용하는 방식
실제 라이브러리와 타입 정보의 버전이 별도로 관리되는 방식의 문제점
declare module
선언으로 라이브러리의 타입 정보를 없애 버린다@types
의존성이 중복되는 경우ex) @types/bar
가 현재 호환되지 않는 버전의 @types/foo
에 의존하는 경우
일부 라이브러리는 자체적으로 타입 선언을 포함(번들링)한다.
package.json
의 types
필드가 .d.ts
파일을 가리키도록 되어 있다@types
의 버전 선택 불가능DefinitelyTyped
에 타입 선언을 공개하여 타입 선언을 @types
로 분리한다잘 작성된 타입 선언은 라이브러리를 올바르게 사용하는 방법에 도움이 되며 생산성을 크게 향상시킨다
라이브러리 공개 시, 타입 선언을 자체적으로 포함하는 것과 타입 정보만 분리하여 DefinitelyTyped
에 공개하는 것의 장단점을 비교해 보자
라이브러리가 타입스크립트로 작성된 경우만 타입 선언을 라이브러리에 포함하는 것을 권장한다
라이브러리 제작자는 프로젝트 초기에 타입 익스포트부터 작성해야 한다
타입을 익스포트하지 않았을 경우
interface SecretName {
first: string;
last: string;
}
interface SecretSanta {
name: SecretName;
gift: string;
}
export function getGift(name: SecretName, gift: string): SecretSanta {
// ...
}
SecretName
또는 SecretSanta
를 직접 임포트할 수 없고, getGift
만 임포트할 수 있다Parameters
와 ReturnType
을 이용해 추출하기
type MySanta = ReturnType<typeof getGift>; // SecretSanta
type MyName = Parameters<typeof getGift>[0]; // SecretName
→ 사용자가 추출하기 전에 공개 메서드에 사용된 타입은 익스포트하자!
함수 주석에 // ...
대신 JSDoc 스타일의 /** ... **/
을 사용하면 대부분의 편집기는 함수 사용부에서 주석을 툴팁으로 표시해 준다
타입스크립트 관점의 TSDoc
/**
* Generate a greeting
* @param name Name of the person to greet
* @param title ...
* returns ...
*/
function greetFullTSDoc(name: string, title: string) {
return `Hello ${title} ${name}`;
}
타입 정의에 TSDoc 사용하기
/** 특정 시간과 장소에서 수행된 측정 */
interface Measurement {
/** 어디에서 측정되었나? */
position: Vector3D;
/** 언제 측정되었나? */
time: number;
/** 측정된 운동량 */
momentum: Vector3D;
}
→ Measurement
객체의 각 필드에 마우스를 올려 보면 필드별로 설명을 볼 수 있다
😮 주의! 타입스크립트에서는 타입 정보가 코드에 있기 때문에 TSDoc에서는 타입 정보를 명시하면 안 된다
자바스크립트에서 this는 다이나믹 스코프
타입스크립트는 자바스크립트의 this 바인딩을 그대로 모델링한다
this를 사용하는 콜백 함수에서 this 바인딩 문제 해결하기
콜백 함수의 매개변수에 this를 추가하고, 콜백 함수를 call
로 호출하는 방법
function addKeyListener(
el: HTMLElement,
fn: (this: HTMLElement, e: KeyboardEvent) => void
) {
el.addEventListener("keydown", (e) => {
fn.call(el, e);
});
}
call
을 사용해야 한다만약 라이브러리 사용자가 콜백을 화살표 함수로 작성하고 this를 참조하려고 하면 타입스크립트가 문제를 잡아낸다
class Foo {
registerHandler(el: HTMLElement) {
addKeyListener(el, (e) => {
this.innerHTML; // 🚨 'Foo' 유형에 'innerHTML' 속성이 없습니다
});
}
}
콜백 함수에서 this 값을 사용해야 한다면 this는 API의 일부가 되는 것이기 때문에 반드시 타입 선언에 포함해야 한다
두 가지 타입의 매개변수를 받는 함수
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
number
타입을 매개변수로 넣고 string
타입을 반환하는 경우도 포함되어 있다→ 제네릭을 사용하여 동작을 모델링할 수 있다
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' (😮 string을 원하고 있다.)
→ 타입이 너무 과하게 구체적인 문제
조건부 타입
타입 공간의 if 구문
function double<T extends number | string>(
x: T
): T extends string ? string : number;
function double(x: any) {
return x + x;
}
개별 타입의 유니온으로 일반화하기 때문에 타입이 더 정확해진다
각각이 독립적으로 처리되는 타입 오버로딩과 달리, 조건부 타입은 타입 체커가 단일 표현식으로 받아들이기 때문에 유니온 문제를 해결할 수 있다
CSV 파일을 파싱하는 라이브러리 작성 시 NodeJS 사용자를 위해 매개변수에 Buffer
타입을 허용하는 경우
Buffer
타입 정의를 위해 @types/node
패키지 필요각자가 필요한 모듈만 사용할 수 있도록 구조적 타이핑 적용하기
interface CsvBuffer {
toString(encoding: string): string;
}
function parseCSV(
contents: string | CsvBuffer
): { [column: string]: string }[] {
// ...
}
CsvBuffer
가 Buffer
타입과 호환되기 때문에 NodeJS 프로젝트에서도 사용 가능
parseCSV(new Buffer("column1, column2\nval2,val2", "utf-8"));
미러링: 작성 중인 라이브러리가 의존하는 라이브러리의 구현과 무관하게 타입에만 의존한다면, 필요한 선언부만 추출하여 작성 중인 라이브러리에 넣는 것
다른 라이브러리의 타입이 아닌 구현에 의존하는 경우에도 동일한 기법을 적용할 수 있고 타입 의존성을 피할 수 있다
→ 유닛 테스트와 상용 시스템 간의 의존성을 분리하는 데도 유용하다
타입 선언 테스트하기
유틸리티 라이브러리에서 제공하는 map
함수의 타입 작성하기
declare function map<U, V>(array: U[], fn: (u: U) => V): V[];
단순히 함수를 호출하는 테스트만으로는 반환값에 대한 체크가 누락될 수 있다 (’실행’에서의 오류만 검사한다)
반환값을 특정 타입의 변수에 할당하여 간단히 반환 타입을 체크할 수 있는 방법
const lengths: number[] = map(["john", "paul"], (name) => name.length);
number[]
타입 선언은 map
함수의 반환 타입이 number[]
임을 보장한다그러나 테스팅을 위해 할당을 사용하는 방법에는 두 가지 문제가 있다
일반적인 해결책은 변수 도입 대신 헬퍼 함수를 정의하는 것이다
function assertType<T>(x: T) {}
assertType<number[]>(map(["john", "paul"], (name) => name.length));
객체의 타입을 체크하는 경우
const beatles = ["john", "paul", "george", "ringo"];
assertType<{ name: string }[]>(
map(beatles, (name) => ({
name,
inYellowSubmarine: name === "ringo",
}))
); // 정상
{name: string}[]
에 할당 가능하지만, inYellowSubmarine
속성에 대한 부분이 체크되지 않았다타입스크립트의 함수는 매개변수가 더 적은 함수 타입에 할당 가능하다는 문제
const double = (x: number) => 2 * x;
assertType<(a: number, b: number) => number>(double); // 정상?!
Parameters
와 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); // 정상
map
의 콜백 함수에서 사용하게 되는 this 값에 대한 타입 선언 테스트
declare function map<U, V>(
array: U[],
fn: (this: U[], u: U, i: number, array: U[]) => V
): V[];
타입 시스템 내에서 암시적 any
타입을 발견하기 위해 DefinitelyTyped의 타입 선언을 위한 도구 dtslint
사용하기
const beatles = ["john", "paul", "george", "ringo"];
map(
beatles,
function (
name, // $ExpectType string
i, // $ExpectType number
array // $ExpectType string[]
) {
this; // $ExpectType string[]
return name.length; // $ExpectType number[]
}
);
dtslint
는 할당 가능성을 체크하는 대신 각 심벌의 타입을 추출하여 글자 자체가 같은지 비교한다