이번 장은..
타입과는 관계 없지만, 코드를 작성하고 실행하면서 실제로 겪을 수 있는 문제들에 대해서 다룬다.
- 숫자 열거형에 0,1,2 이외의 다른 숫자가 할당되면 매우 위험하다.
- 상수 열거형은 보통 열거형과 달리 런타임에 완전히 제거된다.
preserveConstEnums
플래그를 설정한 상태의 상수 열거형은 보통의 열거형처럼 런타임 코드에 상수 열거형 정보를 유지한다.- 타입스크립트에서 문자열 열거형은 명목적 타이핑을 사용한다.
// 숫자 열거형
enum Flavor {
VANILLA = 0,
CHOCOLATE = 1,
STRAWBERRY = 2,
}
// 상수 열거형
cosnt enum Flavor {
VANILLA = 0,
CHOCOLATE = 1,
STRAWBERRY = 2,
}
// 문자열 열거형
const enum Flavor {
VANILLA = 'vanilla',
CHOCOLATE = 'chocolate',
STRAWBERRY = 'strawberry'
}
// Flavor를 매개변수로 받는 함수 가정
function scoop(flavor: Flavor){ //... }
// Flavor는 런타임 시점에는 문자열이기 때문에, 자바스크립트에서는 정상
scoop('vanilla');
// 타입스크립트에서는 에러 'vanilla' 형식은 'Flavor' 형식의 매개변수에 할당될 수 없습니다.
scoop('vanilla');
// 따라서, 타입스크립트에서는 열거형을 임포트하고 문자열 대신 사용해야 한다.
import { Flavor } from 'ice-cream';
scoop(Flavor.VANILLA); // 정상
- 열거형만큼 안전하고, 자바스크립트와 호환 가능하다.
- 편집기에서 자동완성 기능을 사용할 수 있다.
type Flavor = 'vanilla' | 'chocolate' | 'strawberry';
let flavor: Flavor = 'chocolate'; //정상
function scoop(flavor: Flavor) {
if (flavor === 'v
// 자동완성이 'vanilla'를 추천합니다.
}
- TS 컴파일은 타입제거가 이루어져서 코드가 줄지만, 매개변수 속성으로 코드가 증가
- 매개변수 속성은 런타임에는 사용되지만, TS 관점에서는 사용되지 않은 것 처럼 보임
- 매개변수 속성과 일반 속성을 섞어서 사용하면 클래스 설계가 혼란스러움
// 클래스를 초기화할 때 속성을 할당하기 위해 생성자의 매개변수를 사용
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
}
// 매개변수 속성(public name) 사용
class Person {
constructor(public name: string) {}
}
// 설계의 혼란
class Person {
first: string;
last: string;
constructor(public name: string) {
[this.first, this.last] = name.split(' ');
}
}
namespace
키워드를 추가함.namespace foo {
function bar() {}
}
/// <reference path="other.ts"/>
foo.bar();
→ 이제는 ECMAScript 2015 스타일의 모듈(import와 export
)을 사용하자.
tsconfig.json
에 experimentalDecorators
속성을 설정하고 사용해야 한다.// tsConfig: {"experimentalDecorators":true}
// 예시: 클래스의 메서드가 호출될 때마다 로그를 남기려면 logged 애너테이션을 정의할 수 있다.
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@logged
greet() {
return 'Hello, ' + this.greeting;
}
}
function logged(target: any, name: string, descriptor: PropertyDescriptor) {
const fn = target[name];
descriptor.value = function () {
console.log(`Calling ${name}`);
return fn.apply(this, arguments);
};
}
console.log(new Greeter('Dave').greet());
// Logs:
// Calling greet
// Hello, Dave
export default {};
🎯 요약
타입스크립트에서 열거형, 매개변수 속성, 트리플 슬래시 임포트, 데코레이터를 사용하지 말자
const obj = {
one: 'uno',
two: 'dos',
three: 'tres',
};
for (const k in obj) {
const v = obj[k]; // - obj 에 인덱스 시그니처가 없기 때문에 엘리먼트는 암시적으로 'any' 타입입니다.
}
// k의 타입은 string인 반면, obj 객체에는 'one', 'two', 'three' 세 개의 키만 존재한다.
// k와 obj 객체의 키 타입이 서로 다르게 추론되어 오류가 발생함
interface ABC {
a: string;
b: string;
c: number;
}
// 1. 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
}
}
// 2. Object.entries -> 일반적인 경우지만 키와 값의 타입을 다루기 까다롭다.
function foo(abc: ABC) {
for (const [k, v] of Object.entries(abc)) {
k; // Type is string
v; // Type is any
}
}
🎯 요약
객체를 순회하며 키와 값을 얻으려면, keyof
선언이나 Object.entries
를 사용하자.
function handleDrag(eDown: Event) {
const targetEl = eDown.currentTarget;
ta rgetEl.classList.add(‘dragging1);
const dragStart = [eDown-clientX, eDown.clientY];
const handleUp = (ellp: Event) => {
targetEl.classList.remove('dragging');
targetEl.removeEventListener('mouseup', handleUp);
const dragEnd = [ellp.clientX, eUp-dientY];
console.log('dx, dy =', [0, l].map(i => dragEnd[i] - dragStart[i]));
}
targetEl.addEventListener('mouseup', handleUp);
}
const div = document.getElementByld('surface');
div.addEventListener('mousedown', handleDrag);
타입 | 예시 |
---|---|
EventTarget | window, XMLHttpRequest |
Node | document, Text, Comment |
Element | HTMLElement, SVGElement |
HTMLElement | <i>, <b> |
HTMLButtonElement | <button> |
function addDragHandler(el: HTMLElement) {
// mousedown 이벤트 핸들러를 인라인 함수로 만들어 TS에게 문맥 정보 제공
el.addEventListener('mousedown', (eDown) => {
const dragStart = [eDown.clientX, eDown.clientY];
// Event 대신 MouseEvent으로 선언
const handleUp = (eUp: MouseEvent) => {
el.classList.remove('dragging');
el.removeEventListener('mouseup', handleUp);
const dragEnd = [eUp.clientX, eUp.clientY];
console.log(
'dx, dy = ',
[0, 1].map((i) => dragEnd[i] - dragStart[i])
);
};
el.addEventListener('mouseup', handleUp);
});
}
const div = document.getElementById('surface');
// #surface 엘리먼트가 없는 경우 체크
if (div) {
addDragHandler(div);
}
🎯 요약
DOM 에는 타입 계층이 존재한다.
DOM 엘리먼트와 이벤트에는 충분히 구체적인 타입 정보를 사용하거나 TS가 추론할 수 있도록 문맥 정보를 활용해야 한다.
private, protected, public
과 같은 키워드를 붙이더라도 컴파일 후에는 TS 키워드가 모두 사라진다.privat
을 사용하면 안된다.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'); // 결과는 true
장점 - Passwordchecker
생성자 외부에서 passwordHash
변수에 접근할 수 없다 -> 정보 은닉 성공
단점 - passwordHash
에 접근하는 메서드 역시 내부에 작성되어야하고, 인스턴스가 생성될때마다 메서드의 복사본이 생성됨 -> 메모리 낭비 발생
비공개 필드 기능 (#)
→ 접두사로 #
를 붙여서 타입 체크와 런타임 모두에서 비공개로 만드는 역할을 한다.
class Passwordchecker {
#passwordHash: number;
constructor(passwordHash: number) {
this.#passwordHash = passwordHash;
}
checkPassword(password: string) {
return hash(password) === this.#passwordHash;
}
}
const checker = new Passwordchecker(hash('s3cret1'));
checker.checkPassword('secret'); // 결과는 false
checker.checkPassword('s3cret'); // 결과는 true
#passwordHash
속성은 클래스 외부에서 접근할 수 없다.🎯 요약
public, protected, private
접근 제어자는 타입스크립트에서만 강제되고, 런타임에 소용이 없으므로 데이터를 감추고 싶다면, 클로저를 사용하자
🎯 요약
소스맵을 사용해서 런타임에 타입스크립트 코드를 디버깅하자.