item 53. 타입스크립트 기능보다는 ECMAScript 기능 사용하기
•
타입스크립트가 태동하던 시기의 자바스크립트는 결함이 많고 개선할 부분이 많은 언어였으며 클래스, 데코레이터, 모듈 시스템같은 기능을 지원하기 위해 프레임워크나 트랜스파일러로 보완하는 것이 일반적이었다.
•
따라서 타입스크립트 초기 버전도 이런 기능들을 지원하였고 점차 자바스크립트가 개선되어 이런 기능들을 지원하자. 기존 타입스크립트 기능들과 호환성 문제를 일으켰다.
•
대부분의 타입스크립트 진영은 이를 해결하기 위해 자바스크립트의 신규 기능을 그대로 채택하고 타입스크립트 초기 버전과 호환성을 포기하는 전략을 선택했다.
•
이 전략이 세워지기 전, 이미 사용되고 있던 몇 기능이 존재하는데, 이 기능들은 타입 공간과 값 공간의 경계를 혼란스럽게 만들기 때문에 사용하지 않는 것이 좋다.
•
열거형
enum Flavor {
VANILLA = 0,
CHOCOLATE = 1,
STRAWBERRY = 2,
}
let flavor = Flavor.CHOCOLATE; // Type is Flavor
Flavor // Autocomplete shows: VANILLA, CHOCOLATE, STRAWBERRY
Flavor[0] // Value is "VANILLA"
TypeScript
복사
◦
단순히 값을 나열하는 것보다 실수가 적고 명확하기 때문에 일반적으로는 열거형을 사용하는 것이 좋다.
◦
그러나 타입스크립트에는 문제가 존재하는데, 타입스크립트의 열거형은 다음 목록처럼 상황에 따라 다르게 동작한다.
▪
숫자 열거형에 0, 1, 2 외의 다른 숫자가 할당되면 매우 위험하다.
▪
상수 열거형은 보통의 열거형과 달리 런타임에 완전히 제거된다.
•
preserverConstEnums 플래그를 설정한 상태의 상수 열거형은 보통의 열거형처럼 런타임 코드에 상수 열거형 정보를 유지한다.
▪
문자열 열거형은 런타임의 타입 안전성과 투명성을 제공한다. 그러나 타입스크립트의 다른 타입과 달리 명목적 타이핑을 사용한다.
enum Flavor {
VANILLA = 'vanilla',
CHOCOLATE = 'chocolate',
STRAWBERRY = 'strawberry',
}
let flavor = Flavor.CHOCOLATE; // Type is Flavor
flavor = 'strawberry';
// ~~~~~~ Type '"strawberry"' is not assignable to type 'Flavor'
TypeScript
복사
◦
이처럼 자바스크립트와 타입스크립트의 동작이 일관되지 않기 때문에 문자열 열거형 대신 리터럴 타입의 유니온을 사용하는 것이 좋다.
type Flavor = 'vanilla' | 'chocolate' | 'strawberry';
let flavor: Flavor = 'chocolate'; // OK
flavor = 'mint chip';
// ~~~~~~ Type '"mint chip"' is not assignable to type 'Flavor'
TypeScript
복사
•
매개변수 속성
◦
일반적으로 클래스를 초기화할 때 속성을 할당하기 위해 생성자의 매개변수를 사용한다.
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
}
TypeScript
복사
◦
타입스크립트는 이에 대해 다음처럼 더 간결한 문법을 제공한다.
class Person {
constructor(public name: string) {}
}
TypeScript
복사
◦
위 예제의 public name을 매개변수 속성이라고 부르며 멤버 변수로 name을 선언한 이전 에제와 동일하게 동작한다. 이런 매개변수 속성은 다음과 같은 문제점을 가진다.
▪
매개변수 속성이 런타임에는 실제로 사용되지만 타입 스크립트 관점에서는 사용되지 않는 것처럼 보인다.
▪
매개변수 속성과 일반 속성을 섞어서 기재하면 클래스의 설계가 혼란스러워진다.
◦
찬반이 존재하는 주제이긴 하나 가급적이면 일반 속성과 둘 중 하나를 선택해 일관성있게 사용하는 것이 좋다.
•
네임스페이스와 트리플 슬래스 임포트
◦
각 환경마다 자신만의 방식으로 모듈 시스템을 마련했는데, Node.js는 require와 module.exports를 사용한 반면 AMD는 define 함수와 콜백을 사용했다.
◦
타입스크립트 역시 자체적으로 모듈 시스템을 구축했고 module 키워드와 트리플 슬래시 임포트를 사용했다.
◦
ECMAScript 2015가 공식적으로 모듈 시스템을 도입하고서는 충돌을 피하기 위해 module과 같은 기능을 하는 namespace 키워드를 추가했다.
namespace foo {
function bar() {}
}
/// <reference path="other.ts"/>
foo.bar();
TypeScript
복사
◦
트리플 슬래시 임포트와 module 키워드는 호환성을 위해 남아있을 뿐이니 가급적이면 ECMAScript 2015 스타일의 모듈을 사용하는 것이 좋다.
•
데코레이터
◦
데코레이터는 클래스, 메소드, 속성에 어노테이션을 붙이거나 기능을 추가하는 데 사용할 수 있다.
◦
예를 들어 클래스의 메소드가 호출될 때마다 로그를 남기려면 다음과 같이 logged 어노테이션을 정의하여 사용할 수 있다.
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@logged
greet() {
return "Hello, " + this.greeting;
}
}
function logged(target: any, name: string) {
const fn = target[name];
target[name] = function() {
console.log(`Calling ${name}`);
return fn.apply(this, arguments);
};
return target;
}
console.log(new Greeter('Dave').greet());
// Logs:
// Calling greet
// Hello, Dave
TypeScript
복사
◦
데코레이터는 앵귤러 프레임워크를 지원하기 위해 추가되었으며 tsconfig.json에 experimentalDecorators 속성을 설정하고 사용해야 한다.
◦
현재까지도 표준화가 완료되지 않았으므로 사용 중인 데코레이터가 비표준으로 바귀거나 호환성이 깨질 가능성이 있다.
•
요약
◦
일반적으로 타입스크립트 코드에서 모든 타입 정보를 제거하면 자바스크립트가 되지만 열거형, 매개변수 속성, 모듈 시스템, 데코레이터는 타입 정보를 제거한다고 자바스크립트가 되진 않는다.
◦
타입스크립트의 역할을 명확하게 하려면 열거형, 매개변수 속성, 모듈 시스템, 데코레이터는 사용하지 않는 것이 좋다.
item 54. 객체를 순회하는 노하우
•
다음 코드는 정상적으로 실행되지만 편집기에서는 오류가 발생하는데, 원인이 무엇일까?
const obj = {
one: 'uno',
two: 'dos',
three: 'tres',
};
for (const k in obj) {
const v = obj[k];
// ~~~~~~ Element implicitly has an 'any' type
// because type ... has no index signature
}
TypeScript
복사
◦
상수 k의 타입이 string으로 추론되는 반면, obj의 키는 one, two, three 세 개의 키만 존재하므로 k와 obj 객체의 키 타입이 서로 다르게 추론되어 발생하는 오류다.
◦
해당 오류는 다음과 같이 k의 타입을 구체적으로 유니온 타입을 이용해 명시해주면 해결된다.
let k: keyof typeof obj; // Type is "one" | "two" | "three"
for (k in obj) {
const v = obj[k]; // OK
}
TypeScript
복사
•
그렇다면 k 타입이 “one” | “two” | “three”가 아닌 string으로 추론된 이유는 무엇일까?
interface ABC {
a: string;
b: string;
c: number;
}
function foo(abc: ABC) {
for (const k in abc) { // const k: string
const v = abc[k];
// ~~~~~~ Element implicitly has an 'any' type
// because type 'ABC' has no index signature
}
}
TypeScript
복사
◦
앞선 예제와 같은 오류이므로 let k: keyof ABC 같은 선언을 통해 오류를 제거할 수 있다.
◦
foo 함수는 ABC 타입에 할당 가능한 어떠한 값이든 매개변수로 허용하기 때문에 a, b, c 속성 외에 d를 추가로 가지는 다음 x 객체로 호출이 가능하다.
const x = {a: 'a', b: 'b', c: 2, d: new Date()};
foo(x); // OK
TypeScript
복사
◦
즉, ABC 타입에 할당 가능한 객체는 ABC의 속성 외에 다른 속성이 존재할 수 있기 때문에 타입스크립트는 ABC 타입의 키를 string 타입으로 선택하게 된다.
•
keyof 키워드를 사용한 해결법은 또 다른 문제점을 내포하고 있다.
function foo(abc: ABC) {
let k: keyof ABC;
for (k in abc) { // let k: "a" | "b" | "c"
const v = abc[k]; // Type is string | number
}
}
TypeScript
복사
◦
k가 “a” | “b” | “c” 타입으로 한정되어 문제가 된 것처럼 v도 string | number 타입으로 한정되어 범위가 너무 좁아 문제가 된다.
◦
d: new Date()와 같은 이전 예제처럼 d 속성은 어떠한 타입도 될 수 있기 때문에 v가 string | number로 추론된 것은 런타임의 동작을 예상하기 어렵게 만든다.
•
타입 문제를 신경쓰지 않고 단지 객체의 키와 값을 순회하고 싶다면 Object.entries를 사용하면 된다.
function foo(abc: ABC) {
for (const [k, v] of Object.entries(abc)) {
k // Type is string
v // Type is any
}
}
TypeScript
복사
•
객체를 순회하며 키와 값을 얻으려면 (let k: keyof T) 같은 keyof 선언이나 Object.entries를 사용하면 된다.
◦
keyof 선언은 상수이거나 추가적인 키없이 정확한 타입을 원하는 경우에 적합하다.
◦
Object.entries는 더욱 일반적으로 사용되지만 키와 값의 타입을 다루기 까다롭다.
•
요약
◦
객체를 순회할 때, 키가 어떤 타입인지 정확히 파악하고 있다면 let k: keyof T와 for-in 루프를 사용하자. 함수의 매개변수로 쓰이는 객체에는 추가적인 속성이 있을 수 있음을 명심하자.
◦
객체를 순회하며 키와 값을 얻는 가장 일반적인 방법은 Object.entries를 사용하는 것이다.
item 55. DOM 계층 구조 이해하기
•
브라우저 관련 내용이라 생략하였습니다.
item 56. 정보를 감추는 목적으로 private 사용하지 않기
•
자바스크립트는 클래스에 비공개 속성을 만들 수 없다. 다만 비공개 속성임을 나타내기 위해 언더스코어를 접두사로 붙이던 것이 관례로 인정될 뿐이었다.
◦
단순히 비공개라고 표시한 것뿐이지 일반적인 속성과 동일하게 클래스 외부에 공개되어 있다는 점을 주의해야 한다.
•
타입스크립트에는 public, protected, private 접근 제어자를 사용해서 공개 규칙을 강제할 수 있는 것으로 오해할 수 있다.
class Diary {
private secret = 'cheated on my English test';
}
const diary = new Diary();
diary.secret
// ~~~~~~ Property 'secret' is private and only
// accessible within class 'Diary'
TypeScript
복사
•
public, protected, private같은 접근 제어자는 타입스크립트 키워드이기 때문에 컴파일 이후에는 제거된다.
◦
위 예제 코드가 컴파일되면 다음 예제의 자바스크립트 코드로 변환된다.
class Diary {
constructor() {
this.secret = 'cheated on my English test';
}
}
const diary = new Diary();
diary.secret;
TypeScript
복사
◦
private 키워드는 사라졌고 secret은 일반적인 속성이므로 접근할 수 있다. 심지어 단언문을 사용하면 타입스크립트 상태에서도 private 속성에 접근할 수 있다.
•
정보를 감추기 위해서 private을 사용해서는 안 된다.
•
자바스크립트에서 정보를 숨기기 위해 가장 효과적인 방법은 클로저를 사용하는 것이다. 다음 코드처럼 생성자에서 클로저를 만들어낼 수 있다.
declare function hash(text: string): number;
class PasswordChecker {
checkPassword: (password: string) => boolean;
constructor(passwordHash: number) {
this.checkPassword = (password: string) => {
return hash(password) === passwordHash;
}
}
}
const checker = new PasswordChecker(hash('s3cret'));
checker.checkPassword('s3cret'); // Returns true
TypeScript
복사
•
다른 선택지로 현재 표준화가 진행 중인 비공개 필드 기능을 사용할 수 있다.
◦
접두사로 #를 붙여서 타입 체크와 런타임 모두에서 비공개로 만드는 역할을 한다.
class PasswordChecker {
#passwordHash: number;
constructor(passwordHash: number) {
this.#passwordHash = passwordHash;
}
checkPassword(password: string) {
return hash(password) === this.#passwordHash;
}
}
const checker = new PasswordChecker(hash('s3cret'));
checker.checkPassword('secret'); // Returns false
checker.checkPassword('s3cret'); // Returns true
TypeScript
복사
•
요약
◦
public, protected, private 접근 제어자는 타입 시스템에서만 강제될 뿐으로 런타임에는 소용이 없으며 단언문을 통해 우회할 수 있다. 접근 제어자로 캡슐화를 달성하려해선 안 된다.
◦
확실히 데이터를 감추고 싶다면 클로저나 # 접두사를 사용하자.
item 57. 소스맵을 사용하여 타입스크립트 디버깅하기
•
엄밀히 말해 타입스크립트를 실행한다는 것은 타입스크립트 컴파일러가 생성한 자바스크립트 코드를 실행한다는 것이다.
◦
이렇게 변환된 자바스크립트 코드는 복잡해 디버깅하기 매우 어렵기 때문에 이를 해결하고 브라우저 제조사들은 서로 협력하여 소스맵이라는 해결책을 내놓았다.
◦
소스맵은 변환된 코드의 위치와 심벌들을 원본 코드의 원래 위치와 심벌들로 매핑한다.
•
NodeJS 프로그램의 디버깅에도 소스맵을 사용할 수 있는데, 보통 편집기가 자동 인식하거나 NodeJS 프로세스를 브라우저 디버거와 연결한다.
•
타입 체커가 코드를 실행하기 전 많은 오류를 잡을 수 있지만 디버거를 대체할 순 없다. 소스맵을 사용하여 제대로 된 타입스크립트 디버깅 환경을 구축하라.
•
요약
◦
원본 코드가 아닌 변환된 자바스크립트 코드를 디버깅하지 말라. 소스맵을 사용해서 런타임에 타입스크립트 코드를 디버깅하자.
◦
소스맵이 최종적으로 변환된 코드에 완전히 잘 매핑되어 있는지 확인하라.
◦
소스맵에 원본 코드가 그대로 포함되도록 설정되어 있을 수 있으니 배포가 필요한 경우, 공개되지 않도록 설정을 확인하라.