자바스크립트 프로젝트에서 발견된 버그의 상당수는 타입스크립트를 사용했다면 컴파일 시점에서 미리 방지했을 수 있었을 것이다.
58. 모던 자바스크립트로 작성하기
타입스크립트는 특정 버전의 자바스크립트로 컴파일하는 기능도 갖고 있다. (즉, 트랜스파일러)
마이그레이션을 어디서부터 시작해야 할지 모르겠다면 최신 버전의 자바스크립트로 바꾸는 것부터 시작하자.
1. ECMAScript 모듈 사용하기
es6 버전부터 모던 자바스크립트라고 부른다. es6부터는 import/export를 사용하는 ECMAScript 모듈이 표준이 되었다.
ES 모듈 시스템은 모듈 단휘로 전화할 수 있게 해주기 때문에 점진적 마이그레이션이 원활해진다.
// CommonJS
// a.js
const b = require("./b");
console.log(b.name);
// b.js
const name = "Module B";
module.exports = { name };
// ES Module
// a.ts
import * as b from "./b";
console.log(b.name);
// b.ts
export const name = "Module B";
2. 프로토타입 대신 클래스 사용하기
과거에는 자바스크립트에서 프로토타입 기반의 객체 모델을 사용했다.
es6에 class 키워드를 사용하는 클래스 기반 모델이 도입되었다.
// Prototype
function Person(first, last) {
this.first = first;
this.last = last;
}
Person.prototype.getName = function () {
return this.first + " " + this.last;
};
const marie = new Person("Marie", "Curie");
const personName = marie.getName();
// Class
class Person {
first: string;
last: string;
constructor(first: string, last: string) {
this.first = first;
this.last = last;
}
getName() {
return this.first + " " + this.last;
}
}
const marie = new Person("Marie", "Curie");
const personName = marie.getName();
클래스가 훨씬 간결하고 직관적이다.
3. var 대신 let/const 사용하기
let/const는 var와 달리 블록 스코프 규칙을 가지기 때문에 스코프 문제를 피할 수 있다.
비슷한 스코프 문제는 중첩된 함수 구문에도 있다.
function foo() {
bar();
function bar() {
console.log("hello");
}
}
bar 함수 정의가 호이스팅 되기 때문에 문제없이 hello가 출력된다.
호이스팅은 실행 순서를 예상하기 어렵게 만들고 직관적이 지 않다.
여기서도 함수 표현식을 사용하여 호이스팅 문제를 피하는 것이 좋다.
4. for(;;) 대신 for-of 또는 배열 메서드 사용하기
// C 스타일의 for 루프
for (var i = 0; i < Array.length; i++) {
const el = array[i];
}
// for-of 루프
for (const el of array) {
}
// forEach 메서드
Array.forEach((el, i) => {});
for-in 문법은 몇 가지 문제점이 있기 때문에 사용하지 않는 것이 좋다.
5. 함수 표현식보다 화살표 함수 사용하기
this 키워드는 일반적인 변수들과는 다른 스코프 규칙을 가진다.
화살표 함수를 사용하면 항상 상위 스코프의 this를 유지할 수 있다.
// 함수 표현식
class Foo {
mehtod() {
console.log(this);
[1, 2].forEach(function () {
console.log(this);
});
}
}
const f = new Foo();
f.mehtod();
// strict 모드에서 Foo, undefined, undefined를 출력
// non-strict 모드에서 Foo, window, window를 출력
// 화살표 함수
class Foo {
method() {
console.log(this);
[1, 2].forEach(() => {
console.log(this);
});
}
}
const f = new Foo();
f.mehtod();
// 항상 Foo, Foo, Foo을 출력
컴파일러 옵션에 noImplicitThis(또는 strict)를 설정하면 this 바인딩 관련 오류를 표시해 준다.
6. 단축 객체 표현과 구조 분해 할당 사용하기
단축 객체 표현으로 변수와 객체 속성의 이름이 같다면 간단하게 작성할 수 있고, 객체 속성 함수를 축약해서 표현할 수 있다.
const obj = {
x, // x: x
onClick() {}, // onClick: function() {}
};
그 반대는 객체 구조 분해다.
const { props } = obj; // const props = obj.props
const { a, b } = props; // const a = props.a
const {
props: { a, b },
} = obj;
맨 마지막의 경우 a와 b는 변수로 선언되었지만 props는 변수 선언이 아니다.
구조 분해 문법에서는 기본값을 지정할 수도 있다.
let { a } = obj.props;
if (a === undefined) a = "default";
// 위와 동일
const { a = "default" } = obj.props;
배열에도 구조 분해 문법을 사용할 수 있다.
const point = [1, 2, 3];
const [x, y, z] = point;
7. 함수 매개변수 기본값 사용하기
자바스크립트에서 함수의 모든 매개변수는 선택적이며, 매개변수를 지정하지 않으면 undefined로 간주된다.
function log2(a, b) {
console.log(a, b);
}
log2(); // undefined undefined
// 옛날
function parseNum(str, base) {
base = base || 10;
}
// 모던 자바스크립트
function parseNum(str, base = 10) {}
기본값을 기반으로 타입 추론이 가능하기 때문에 매개변수에 타입 구문을 쓰지 않아도 된다.
8. 저수준 프로미스나 콜백 대신 async/await 사용하기
async와 await를 사용하면 코드가 간결해지며, 비동기 코드에 타입 정보가 전달되어 타입 추론을 가능하게 한다.
9. 연관 배열에 객체 대신 Map과 Set 사용하기
객체의 인덱스 시그니처는 편리하지만 문제점이 있다.
function countWords(text: string) {
const counts: { [word: string]: number } = {};
for (const word of text.split(/[\s,.]_/)) {
counts[word] = 1 + (counts[word] || 0);
}
return counts;
}
console.log(countWords("Objects have a constructor"));
// {
// Objects: 1,
// have: 1,
// a: 1,
// constructor: "1function Object() { [native code] }"
// }
constructor의 초깃값은 undefined가 아니라 Object.prototype에 있는 생성자 함수이다.
원치 않는 값에다 타입도 string이다.
이런 문제를 방지하려면 Map을 사용하자.
function countWords(text: string) {
const counts = new Map<string, number>();
for (const word of text.split(/[\s,.]_/)) {
counts.set(word, 1 + (counts.get(word) || 0));
}
return counts;
}
10. 타입스크립트에 use strict 넣지 않기
ES6에서 'use strict'를 넣으면 엄격 모드가 활성화된다.
그러나 타입스크립트에서 수행되는 안전성 검사가 엄격 모드보다 훨씬 더 엄격하기 때문에 'use strict'는 무의미하다.
실제로는 타입스크립트 컴파일러가 생성하는 자바스크립트 코드에서 'use strict'가 추가된다. (alwaysStrict를 설정하면)
TC39의 깃헙 저장소와 타입스크립트의 릴리스 노트를 통해 최신 기능을 확인할 수 있다.
자바스크립트 표준화 3단계 이상의 기능들은 타입스크립트 내에서 사용할 수 있다.
파이프라인과 데코레이터는 아직 3단계가 아니지만 큰 잠재력을 지니고 있다.
59. 타입스크립트 도입 전에 @ts-check와 JSDoc으로 시험해보기
@ts-check 지시자를 사용하면 타입스크립트 전환시에 어떤 문제가 발생하는지 미리 시험해 볼 수 있다.
그러나 이는 매우 느슨한 수준으로 타입 체크를 수행한다.
@ts-check 지시자를 사용하면 다음과 같은 오류를 찾아낼 수 있다.
타입 불일치나 함수의 매개변수 개수 불일치 같은 간단한 오류
선언되지 않은 전역 변수
알 수 없는 라이브러리 (서드파티 라이브러리들의 타입 선언 체크)
DOM 문제
만일 DOM 문제가 발생한 경우, 자바스크립트의 경우 단언문을 쓸 수 있다. 이 때 JSDoc으로 해결할 수 있다.
// @ts-check
const ageEl = document.getElementById("age");
ageEl.value = "12"; // 오류
// @ts-check
const ageEl = /** @type {HTMLInputElement} */ (document.getElementById("age"));
ageEl.value = "12"; // 정상
한편 @ts-check를 활성화하면 이미 존재하던 JSDoc에서 부작용이 발생하기도 한다.
이는 JSDoc이 부정확하기 때문이다. 이를 수정하면서 점진적으로 타입 정보를 추가할 수 있다.
마이그레이션의 궁극적인 목표는 타입스크립트 기반으로 전환되는 것이다.
이미 JSDoc 주석으로 타입 정보가 많이 담겨 있다면 @ts-check 지시자를 추가해서 타입 체커를 실험해 보자.
JSDoc은 타입 단언과 타입 추론을 가능하게 하지만 중간 단계일 뿐이다.
60. allowJS로 타입스크립트와 자바스크립트 같이 사용하기
allowJS 옵션은 타입스크립트 파일과 자바스크립트 파일을 서로 입포트할 수 있게 해준다.
대규모 마이그레이션 작업 시작 전, 테스트와 빌드 체인에 타입스크립트를 적용해야 한다.
이는 힘들지만 제대로 된 점진적 마이그레이션에 필수적이다.
61. 의존성 관계에 따라 모듈 단위로 전환하기
점진적 마이그레이션을 할 때는 모듈 단위로 하는 게 이상적이다.
의존성이 없는 최하단 모듈부터 작업을 시작해야 한다.
따라서 서드파티 모듈과 외부 API 호출에 대한 @types를 먼저 추가하자.
의존성 관계도의 아래쪽 첫 번째 모듈은 보통 유틸리티 모듈이다.
마이그레이션 중에는 리팩터링을 해서는 안 된다. 마이그레이션에 집중하자.
마이그레이션 중 발생할 수 있는 오류는 다음과 같다.
- 선언되지 않은 클래스 멤버
타입스크립트에서는 클래스 멤버 변수를 명시적으로 선언해야 한다. - 타입이 바뀌는 값
객체를 한꺼번에 생성하지 않으면 존재하지 않는 속성값으로 오류가 발생할 수 있다.
필요에 따라 JSDoc을 추가할 수도 있다.
마이그레이션의 마지막 단계는 테스트 코드다.
테스트 코드는 항상 의존성 관계도의 최상단에 위치한다.
62. 마이그레이션의 완성을 위해 noImplicitAny 설정하기
noImplicitAny가 설정되지 않은 상태에서는 타입 선언에서 비롯되는 실제 오류가 숨어 있을 수 있다.
class Char {
indices: number[];
getRanges() {
for (const r of this.indices) {
const low = r[0]; // any
}
}
}
이는 잘못된 타입니다. number[][] 또는 [number, number][]가 정확한 타입니다.
현재 r은 number 타입으로 추론되는데 배열 인덱스 접근에 오류가 없다. 타입 체크가 매우 허술하다.
noImplicitAny는 상당히 엄격한 설정이므로 로컬에서부터 설정하여 점진적으로 수정하자.
strictNullChecks를 설정하지 않아도 대부분의 타입 체크를 적용한 것으로 볼 수 있다.
최종적으로 가장 강력한 설정은 "strict": true이다.