// 인사말을 생성합니다. 결과는 보기 좋게 꾸며집니다.
function greet(name: string, title: string) {
return `Hello ${title} ${name}`;
}
사용자를 위한 문서라면 JSDoc 스타일의 주석으로 만드는 것이 좋습니다.
/** 인사말을 생성합니다. 결과는 보기 좋게 꾸며집니다. */
function greet(name: string, title: string) {
return `Hello ${title} ${name}`;
}
⭐️ 대부분의 편집기는 함수가 호출되는 곳에서 함수에 붙어 있는 JSDoc 스타일의 주석을 툴팁으로 표시해주기 때문입니다.
만약 공개 API에 주석을 붙인다면 JSDoc 형태로 작성해야 합니다.
JSDoc에는 @param과 @return 같은 일반적 규칙을 사용할 수 있습니다.
(타입스크립트 관점에서는 TSDoc 이라고 부르기도 합니다.)
/**
* 인사말을 생성합니다.
* @param name 인사할 사람의 이름
* @param title 그 사람의 칭호
* @returns 사람이 보기 좋은 형태의 인사말
*/
function greetFullTSDoc(name: string, title: string) {
return `Hello ${title} ${name}`;
}
@param과 @returns를 추가하면 함수를 호출하는 부분에서 각 매개변수와 관련된 설명을 보여줍니다.
타입 정의에 TSDoc을 사용할 수도 있습니다.
/** 특정 시간과 장소에서 수행된 측정 */
interface Measurement {
/** 어디에서 측정되었나? */
position: Vector3D;
/** 언제 측정되었나? epoch에서부터 초 단위로 */
time: number;
/** 측정된 운동량 */
momentum: Vector3D;
}
마우스를 올려 보면 필드별로 설명을 볼 수 있습니다.
TSDoc 주석은 마크다운(markdown) 형식으로 꾸며지므로 굵은 글씨, 기울임 글씨, 글머리기호 목록을 사용할 수 있습니다.
❗️ 주석은 수필처럼 장황하게 쓰지 않도록 주의하고 간단히 요점만 언급합니다. 추가로 TSDoc에서는 타입 정보를 명시하면 안 됩니다.
요약
📌 익스포트된 함수, 클래스, 타입에 주석을 달 때는 JSDoc/TSDoc 형태를 사용합시다. 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();
// 런타임에 오류가 발생
// Uncaught TypeError: undefined의 'vals' 속성을 읽을 수 없습니다.
c.logSquares()가 실제로 두가지 작업을 수행하기 때문에 문제가 발생합니다.
C.prototype.logSquares를 호출하고, 또한 this의 값을 c로 바인딩합니다.
앞의 코드에서는 logSquares의 참조 변수를 사용함으로써 두 가지 작업을 분리 했고, this의 값은 undefined로 설정됩니다.
자바스크립트에서 this 바인딩을 온전히 제어할 수 있는 방법은 call을 사용하면 명시적으로 this를 바인딩하여 문제를 해결할 수 있습니다.
const c = new C();
const method = c.logSquares;
method.call(c); // 제곱을 출력합니다.
this가 반드신 c의 인스턴스에 바인딩되어야 하는 것은 아니며, 어떤 것이든 바인딩할 수 있습니다.
심지어 DOM에서도 this를 바인딩할 수 있습니다.
이벤트 핸들러 예제.
document.querySelector('input')!.addEventListener('change', function(e){
console.log(this); // 이벤트가 발생한 input 엘리먼트를 출력합니다.
})
this 바인딩은 종종 콜백 함수에서 쓰입니다.
클래스 내에 onClick 핸들러를 정의할 경우
class ResetButton {
render() {
return makeButton({text: 'Reset', onClick: this.onClick});
}
onClick() {
alert(`Reset ${this}`);
}
}
그러나 ResetButton에서 onClick을 호출하면, this 바인딩 문제로 인해 "Reset이 정의되지 않았습니다"라는 경고가 뜹니다.
일반적인 해결책은 생성자에서 메서드에 this를 바인딩시키는 것입니다.
class ResetButton {
constructor() {
this.onClick = this.onClick.bind(this);
}
render() {
return makeButton({text: 'Reset', onClick: this.onClick});
}
onClick() {
alert(`Reset ${this}`);
}
}
조금 더 간단한 방법으로 바인딩을 해결할 수도 있습니다.
class ResetButton {
render() {
return makeButton({text: 'Reset', onClick: this.onClick});
}
onClick = () => {
alert(`Reset ${this}`); // "this"가 항상 인스턴스를 참조합니다.
}
}
화살표 함수로 바꾸면, ResetButton이 생성될 때마다 제대로 바인딩된 this를 가지는 새 함수를 생성하게 됩니다.
class ResetButton {
constructor() {
var _this = this;
this.onClick = function () {
alert("Reset" + _this);
};
}
render() {
return makeButton({ text: 'Reset', onClick: this.onClick });
}
}
❗️ 작성 중인 라이브러리에 this를 사용하는 콜백 함수가 있다면, this 바인딩 문제를 고려해야 합니다.
⭐️ 이 문제는 콜백 함수의 매개변수 this를 추가하고, 콜백 함수를 call로 호출해서 해결할 수 있습니다.
function addKeyListener(
el: HTMLElement,
fn: (this: HTMLElement, e: KeyboardEvent) => void
){
el.addEventListener('keydown', e => {
fn.call(el, e);
})
}
ex) call을 제거하고 fn을 두 개의 매개변수로 호출할 경우
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);
// ~~~~~ 'void' 형식의 'this' 컨텍스트를
// 메서드의 'HTMLElement' 형식 'this'에 할당할 수 없습니다.
})
}
콜백 함수에서 this 값을 사용해야 한다면 this는 API의 일부가 되는 것이기 때문에 반드시 타입 선언에 포함해야 합니다.
요약
📌 this 바인딩이 동작하는 원리를 이해해야 합니다.
📌 콜백 함수에서 this를 사용해야 한다면, 타입 정보를 명시해야 합니다.
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
선언문에는 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"
❗️ 타입이 너무 과하게 구체적입니다.
타입스크립트에서 함수의 구현체는 하나지만, 타입 선언은 몇 개든지 만들 수 있습니다.
function double(x: number): number;
function double(x: string): string;
function double(x: any) {return x + x;}
const num = double(12); // 타입이 number
const str = double('x'); // 타입이 string
❗️ 유니온 타입 관련해서 문제가 발생합니다.
function f(x: number|string){
return double(x);
// ~~~ 'string | number' 형식의 인수는
// 'string' 형식의 매개변수에 할당될 수 없습니다.
}
오버로딩(string|number)을 추가하여 문제를 해결할 수 있습니다.
하지만 더 나은 해결책은 조건부 타입을 사용하는 것입니다.
조건부 타입은 타입 공간의 if 구문과 같습니다.
function double<T extends number | string>(
x: T
): T extends string ? string : number;
function double(x: any) {return x + x;}
제너릭을 사용했던 예제와 비슷하지만, 반환 타입이 더 정교합니다.
조건부 타입은 자바스크립트 삼항 연산자(?:)처럼 사용하면 됩니다.
조건부 타입의 경우 앞선 모든 예제가 동작합니다.
타입스크립트는 조건부 타입을 다음 단계로 해석합니다.
(number|string) extends string ? string : number
-> (number extends string ? string : number) | (string extends string ? string : number)
-> number | string
⭐️ 오버로딩 타입이 작성하기는 쉽지만, 조건부 타입은 개별 타입의 유니온으로 일반화하기 때문에 타입이 더 정확해집니다.
요약
📌 오버로딩 타입보다 조건부 타입을 사용하는 것이 좋습니다. 조건부 타입은 추가적인 오버로딩 없이 유니온 타입을 지원할 수 있습니다.
CSV 파일의 내용을 매개변수로 받고, 열 이름을 값으로 매핑하는 객체들을 생성하여 배열로 반환합니다.
그리고 NodeJS 사용자를 위해 매개변수에 Buffer 타입을 허용합니다.
function parseCSV(contents: string | Buffer): {[column: string]: string}[] {
if (typeof contents === 'object') {
// 버퍼인 경우
return parseCSV(contents.toString('utf8'));
}
}
Buffer의 타입 정의는 NodeJS 타입 선언을 설치해서 얻을 수 있습니다.
$ npm install --save-dev @types/node
CSV 파싱 라이브러리를 공개하면 타입 선언도 포함하게 됩니다.
타입 선언이 @types/node에 의존하기 떄문에 devDependencies로 포함해야 합니다.
❗️ @types/node를 devDepenedencies로 포함할 경우 사용자들에게 문제점.
두 그룹의 사용자들은 각자의 사용하지 않는 모듈이 포함되어 있기 때문에 혼란스럽습니다.
앞선 예제의 경우 인코딩 정보를 매개변수로 받는 toString 메서드를 가지는 인터페이스를 별도로 만들어 사용하면 됩니다.
interface CsvBuffer {
toString(encoding: string): string;
}
function parseCSV(contents: string | CsvBuffer): {[column: string]: string}[] {
// ...
}
실제로 필요한 부분만을 떼어 명시했고 해당 타입이 Buffer와 호환되기 때문에 NodeJS 프로젝트에서 실제 Buffer 인스턴스로 parseCSV를 호출하는 것이 가능합니다.
parseCSV(new Buffer("column1, column2\nval1,val2", "utf-8")); // 정상
만약 작성중인 라이브러리가 의존하는 라이브러리의 구현과 무관하게 타입에만 의존한다면, 필요한 선언부만 추출하여 작성 중인 라이브러리에 넣는 것(미러링)을 고려해 보는 것도 좋습니다.
미러링 기법은 유닛 테스트와 사용 시스템 간의 의존성을 분리하는 데도 유용합니다.
❗️ 다른 라이브러리의 타입 선언의 대부분을 추출해야 한다면, 차라리 명시적으로 @types 의존성을 추가하는 게 낫습니다.
요약
📌 필수가 아닌 의존성을 분리할 때는 구조적 타이핑을 사용하면 됩니다.
📌 공개한 라이브러리를 사용하는 자바스크립트 사용자가 @types 의존성을 가지지 않게 해야 합니다. 그리고 웹 개발자가 NodeJS 관련된 의존성을 가지지 않게 해야 합니다.
유틸리티 라이브러리에서 제공하는 map 함수의 타입 선언을 작성한다고 가정.
declare function map<U, V>(array: U[], fn: (u: U) => V): V[];
// 타입 선언이 예상한 타입으로 결과를 내는지 체크
map(['2017', '2018', '2019'], v => Number(v));
이 코드에는 오류 체크를 수행하지만 허점이 존재합니다.
반환값에 대한 체크가 누락되어 있기 때문에 완전한 테스트라고 할 수 없습니다.
test('square a number', () => {
square(1);
square(2);
})
반환값에 대해서 체크하지 않기 때문에 실행의 결과에 대한 테스트는 하지 않은 게 되어 square의 구현이 잘못되어 있더라도 이 테스트를 통과하게 됩니다.
실제로 반환 타입을 체크하는 것이 훨씬 좋은 테스트 코드입니다.
반환값을 특정 타입의 변수에 할당하여 간단히 반환 타입을 체크 하는 방법.
const lengths: number[] = map(['jogn', 'paul'], name => name.length);
이 코드는 일반적으로 불필요한 타입 선언에 해당합니다. 그러나 테스트 코드 관점에서는 중요한 역할을 하고 있습니다.
테스팅을 위해 할당을 사용하는 방법에는 두 가지 근본적인 문제가 있습니다.
⭐️ 해결책
변수를 도입하는 대신 헬퍼 함수를 정의하는 것.
function assertType<T>(x: T) {}
assertType<number[]>(map(['jogn', 'paul'], name => name.length));
불필요한 변수 문제는 해결하지만 다른 문제점이 남아있습니다.
객체 타입을 체크하는 경우
const beatles = ['jogn', 'paul', 'george', 'ringo'];
assertType<{name: string}[]>(
map(beatles, name => ({
name,
inYellowSubmarine: name === 'ringo'
}))
); // 정상
반환된 배열은 {name: string}[] 에 할당 가능하지만, inYellowSubmarine 속성에 대한 부분이 체크되지 않았습니다.
assertType에 함수를 넣어 보면, 이상한 결과가 나타납니다.
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 함수의 체크가 성공한 이유는?
타입스크립트의 함수는 매개변수가 더 적은 함수 타입에 할당 가능하기 때문입니다.
const g: (x: string) => any = () => 12; // 정상
선언된 것보다 적은 매개변수를 가진 함수를 할당하는 것이 아무런 문제가 없다는 걸 보여줍니다.
이런 사례는 콜백 함수에서 흔히 볼수 있고, 타입스크립트에서는 이러한(서언보다 많은 수의 매개변수) 동작을 모델링하도록 설계되어 있습니다.
⭐️ 제대로 된 assertType 사용 방법
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); // 정상
this가 등장하는 콜백 함수의 경우는 또 다른 문제가 있습니다.
❗️ 앞서 등장했던 map에 대한 테스트는 모두 블랙박스 스타일이었습니다.
콜백 함수 내부에서 매개변수들의 타입과 this를 직접 체크해 보겠습니다.
const beatles = ['john', 'paul', 'george', 'ringo'];
assertType<number[]>(map(
beatles,
function(name, i, array) {
// ~~~~~~~ '(name: any, i: any, array: any) => any' 형식의 인수는
// '(u: string) => any' 형식의 매개변수에 할당될 수 없습니다.
assertType<string>(name);
assertType<number>(i);
assertType<string[]>(array);
assertType<string[]>(this);
// ~~~~~~ 'this'에는 암시적으로 'any' 형식이 포함됩니다.
return name.length;
}
));
다음 코드의 선언을 사용하면 타입 체크를 통과합니다.
declare function map<U, V>(
array: U[],
fn: (this: U[], u: U, i: number, array: U[]) => V
): V[];
그러나 중요한 마지막 문제가 남아 있습니다.
다음 모듈 선언은 까다로운 테스트를 통과할 수 있는 완전한 타입 선언 파일이지만, 결과적으로 좋지 않은 설계가 됩니다.
declare module 'overbar';
이 선언은 전체 모듈에 any 타입을 할당합니다.
❗️ 테스트는 통과하겠지만, 모든 타입 안전성을 포기하게 되고 해당 모듈에 속하는 모든 함수의 호출마다 암시적으로 any 타입을 반환합니다.
⭐️ 타입체커와 독립적으로 동작하는 도구를 사용해서 타입 선언을 테스트하는 방법이 권장됩니다.
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는 할당 가능성을 체크하는 대신 각 심벌의 타입을 추출하여 글자 자체가 같은지 비교합니다.
❗️단점
요약
📌 타입을 테스트할 때는 특히 함수 타입의 동일성(equality)과 할당 가능성(assignability)의 차이점을 알고 있어야 합니다.
📌 콜백이 있는 함수를 테스트할 때, 콜백 매개변수의 추론된 타입을 체크해야 합니다. 또한 this가 API의 일부분이라면 역시 테스트해야 합니다.
📌 타입 관련된 테스트에서 any를 주의해야 합니다. 더 엄격한 테스트를 위해 dtslint같은 도구를 사용하는 것이 좋습니다.