
npm은 세 가지 종류의 의존성을 구분해서 관리하며, 각각의 의존성은 package.json 파일 내의 별도 영역에 들어있다.
전이(transitive)의존성타입스크립트는 개발 도구일 뿐 타입 정보는 런타임에 존재하지 않기 때문에, 타입스크립트와 관련된 라이브러리는 일반적으로
devDependencies에 속한다.
타입스크립트를 시스템 레벨로 설치하기보단 devDependencies로 관리해야하는 이유
@types 의존성, 타입스크립트는 devDendencies에 있어야 한다.타입스크립트는 알아서 의존성 문제를 해결해 주기는커녕, 의존성 관리를 오히려 복잡하게 만들기 때문에 다음 세 가지 추가사항을 고려해야 한다.
세 가지 버전 중 하나라도 맞지 않으면, 의존성과 상관없어 보이는 곳에서 엉뚱한 오류가 발생할 수 있다.
타입 선언도 업데이트하여 라이브러리와 버전을 맞추자! 타입 선언의 버전이 준비되지 않았다면 두 가지 선택지가 존재한다.
보강기법을 활용해 타입 정보를 프로젝트 자체에 추가하기- 타입 선언의 업데이트를 직접 작성하여 공개

타입 선언 중복 발생을 추적하는 방법
npm ls @types/foo
일부 라이브러리, 특히 타입스크립트로 작성된 라이브러리들은 자체적으로 타입 선언을 포함(번들링)한다.
d.ts 파일을 가리키도록 되어있다. Definitely Typed에 공개하는 것이 좋다.권장되지 않는 예시
// 인사말을 생성합니다. 결과는 보기 좋게 꾸며집니다.
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 같은 일반적 규칙을 사용하여 주석을 달아보자.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' 속성을 읽을 수 없습니다. 라는 오류가 발생한다.
이유가 뭘까?
즉 두 가지 작업을 분리했고, logSquares는 반환값이 없으므로 undefined가 할당된다.
GPT는?
- logSquares 메서드는 반환값이 없으므로 method 변수에 할당할 수 없습니다.
- method()를 호출하려고 할 때 오류가 발생합니다. 이 부분은 이전에 언급한 대로 logSquares 메서드의 반환값이 없기 때문입니다.
const c = new C();
const method = c.logSquares;
method.call(c) // 정상
call 메서드의 인자로 전달되는 c는 해당 메서드 내부에서의 this 값을 지정하기 위한 것이므로, 실제로는 c 객체의 메서드인 것처럼 작동하게 되므로 오류가 발생하지 않는다.
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의 일부가 되는 것이기 때문에 반드시 타입 선언에 포함해야 한다.
이 아이템은 잘 모르겠다...
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 형식의 매개변수에 할당될 수 없습니다.
}
오버로딩 타입의 마지막 선언까지 검색했을 때, 유니온 타입이 없으므로 에러가 발생한다.
조건부 타입을 사용하기
function double<T extends number | string>(
x: T
): T extends string ? string : number;
function double(x: any) { return x + x; }
조건부 타입은 자바스크립트의 삼항 연산자처럼 사용하면 된다.
이로 인해 앞선 모든 예제가 동작한다.
각각의 오버로딩 타입이 독립적으로 처리되는 반면, 조건부 타입은 타입 체커가 단일 표현식으로 받아들이기 때문에 유니온 문제를 해결할 수 있다.
CSV파일을 파싱하는 라이브러리를 작성한다고 가정해보자. NodeJS 사용자를 위해 매개변수에 Buffer 타입을 허용하고 @types/node를 통해 Buffer의 타입을 정의했다.
function parseCSV(contents: string | Buffer): {[column: string]: string}[] {
if (typeof contents === 'object') {
// 버퍼인 경우
return parseCSV(contents.toString('utf8'));
}
// ...
}
다음 두 그룹의 라이브러리 사용자들에게 문제가 생긴다.
Buffer타입은 NodeJs 개발자만 필요로 한다. @types/node 또한 NodeJS와 타입스크립트를 동시에 사용하는 개발자만 필요로 한다.
각자가 필요한 모듈만 사용할 수 있도록 구조적 타이핑을 적용할 수 있다. 예를 들면 @types/node에 있는 Buffer 선언을 사용하지 않고, 필요한 메서드와 속성만 별도로 작성할 수 있다.
(미러링) 을 고려해 보는 것도 좋다.프로젝트를 공개하려면 테스트 코드를 작성하는 것은 필수이며, 타입 선언 또한 테스트를 거쳐야한다.
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);
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); // 정상
위 예제는 Parameter와 ReturnType 제너릭을 이용하여 매개변수 타입과 반환 타입을 분리하여 두 번 테스트 한다.
DefinitelyTyped의 타입 선언을 위한 도구는
dtslint이다.
dtslint 같은 도구를 사용하는 것이 좋다.