[TypeScript 독학] #10 이펙티브 타입스크립트(4)

시작하며

이번에 이펙티브 타입스크립트의 7, 8 강을 끝으로 모든 내용을 정리하였다. 마지막 부분은 기존의 자바스크립트 프로젝트를 타입스크립트로 변환하는 과정에서 유의해야 할 점과 어떤 기능을 사용하여 어떤 순서로 진행해야 하는 지에 대한 내용을 중점으로 다루고 있다.



7장 코드를 작성하고 실행하기

타입스크립트 기능보다는 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"

//상수 enum은 런타임에 완전히 제거됨
let flavor = Flavor.CHOCOLATE;  // Type is Flavor
    flavor = 'strawberry';
 // ~~~~~~ Type '"strawberry"' is not assignable to type 'Flavor'

 //문자열 enum은 명목적 타이핑(타입의 이름이 같아야 할당이 허용됨)을 사용하여 타입 안전성을 제공함.
type Flavor = 'vanilla' | 'chocolate' | 'strawberry';

let flavor: Flavor = 'chocolate';  // OK
    flavor = 'mint chip';
 // ~~~~~~ Type '"mint chip"' is not assignable to type 'Flavor'

//타입스크립트와 자바스크립트의 동작이 다르기 때문에 문자열 enum 대신 리터럴 타입의 유니온을 사용하는 것이 좋음.

class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

//일반적으로 클래스르 초기화할때 속성을 할당하기 위해 생성자 함수의 매개변수를 사용함
class Person {
  constructor(public name: string) {}
}

//타입스크립트는 더 간단한 문법(매개변수 속성)으로 클래스의 프로퍼티를 초기화 할 수 있음
class Person {
  first: string;
  last: string;
  constructor(public name: string) {
    [this.first, this.last] = name.split(' ');
  }
}

//기존 생성자 함수 방식과 매개변수 속성을 혼용해서 사용하면 일관성이 없음

namespace foo {
  function bar() {}
}

//es6 이후 모듈 시스템과의 충돌을 방지하기 위해 typescript에서는 namespace를 도입함
//호환성을 위해 남아있을뿐 import와 export를 사용해야 함
// tsConfig: {"experimentalDecorators":true}

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

//데코레이터(@)는 클래스, 메서드 속성에 기능을 추가할 수 있는 문법으로 표준화가 완료되지 않아 타입스크립트에서는 사용하지 않는 것이 좋음
//데코레이터는 처음에 엥귤러 프레임워크를 지원하기 위해 추가됨

#### 객체를 순회하는 노하우
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
}

//obj내 인데스 시그니처가 없기 때문에 any로 인식됨
const obj = { /* ... */ };
// const obj: {
//     one: string;
//     two: string;
//     three: string;
// }
for (const k in obj) {  // const k: string
  // ...
}

//obj는 'one', 'two', 'three'  개의 키만 존재하지만 k는 string으로 인식하여 오류가 발생함
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
}
let k: keyof typeof obj;  // Type is "one" | "two" | "three"
for (k in obj) {
  const v = obj[k];  // OK
}

//k의 타입을 구체적으로 명시해주면 에러가 사라짐
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
  }
}
const x = {a: 'a', b: 'b', c: 2, d: new Date()};
foo(x);  // OK

//foo 함수는 a,b,c 속성 외에 할당 가능한 어떠한 값이든 매개변수로 허용하기 때문에 d 속성을 가지고 있는 x를 매개변수로 전달할 수 있음
//그러므로 k의 타입이 'one', 'two', 'three'가 아닌 string으로 유추됨
function foo(abc: ABC) {
  for (const [k, v] of Object.entries(abc)) {
    k  // Type is string
    v  // Type is any
  }
}

//키와 값을 순회하고 싶다면 Object.entries를 사용하면 됨

DOM 계층 구조 이해하기

function handleDrag(eDown: Event) {
  const targetEl = eDown.currentTarget;
  targetEl.classList.add('dragging');
// ~~~~~~~           Object is possibly 'null'.
//         ~~~~~~~~~ Property 'classList' does not exist on type 'EventTarget'
  const dragStart = [
     eDown.clientX, eDown.clientY];
        // ~~~~~~~                Property 'clientX' does not exist on 'Event'
        //                ~~~~~~~ Property 'clientY' does not exist on 'Event'
  const handleUp = (eUp: Event) => {
    targetEl.classList.remove('dragging');
//  ~~~~~~~~           Object is possibly 'null'.
//           ~~~~~~~~~ Property 'classList' does not exist on type 'EventTarget'
    targetEl.removeEventListener('mouseup', handleUp);
//  ~~~~~~~~ Object is possibly 'null'
    const dragEnd = [
       eUp.clientX, eUp.clientY];
        // ~~~~~~~                Property 'clientX' does not exist on 'Event'
        //              ~~~~~~~   Property 'clientY' does not exist on 'Event'
    console.log('dx, dy = ', [0, 1].map(i => dragEnd[i] - dragStart[i]));
  }
  targetEl.addEventListener('mouseup', handleUp);
// ~~~~~~~ Object is possibly 'null'
}

   const div = document.getElementById('surface');
   div.addEventListener('mousedown', handleDrag);
// ~~~ Object is possibly 'null'

// 구체적이지 않은 타입 지정으로 인해 이벤트 핸들러 등록 시 에러가 발생함
function handleDrag(eDown: Event) {
  const targetEl = eDown.currentTarget;
  targetEl.classList.add('dragging');
// ~~~~~~~           Object is possibly 'null'
//         ~~~~~~~~~ Property 'classList' does not exist on type 'EventTarget'
  // ...
}

//Event.currentTarget의 타입은 EventTarget || null 이고 ClassList 속성이 없기 때문에 에러가 발생함.
document.getElementsByTagName('p')[0];  // HTMLParagraphElement
document.createElement('button');  // HTMLButtonElement
document.querySelector('div');  // HTMLDivElement

//구체적인 태그의 정보를 사용하면 정확한 타입을 지정할 수 있음
document.getElementById('my-div') as HTMLDivElement;

//getElementById를 사용하는 경우 as HTMLDivElement로 타입을 단언해주어야 함
//my-div는 div 태그라는 것을 알고 있기 때문에 단언문을 사용해도 문제가 되지 않음
function addDragHandler(el: HTMLElement) {
  el.addEventListener('mousedown', eDown => {
    const dragStart = [eDown.clientX, eDown.clientY];
    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');
if (div) {
  addDragHandler(div);
}

//Event 대신 MouseEvent로 타입을 선언하여 clientX, clientY 속성을 사용할 수 있음

정보를 감추는 목적으로 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'

//pubilic, protected, private 같은 접근 제어자는 타입스크립트 키워드로 컴파일 이후 제거됨
class Diary {
  private secret = 'cheated on my English test';
}

const diary = new Diary();
(diary as any).secret  // OK

//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

//javaScript에서는 클로저를 사용하여 속성을 숨길 수 있지만, 인스턴스를 생성할 때마다 메모리가 낭비되고
//개별 인스턴스간에 해당 속성에 대한 접근이 불가능하기 때문에 불편함
declare function hash(text: string): number;
class PasswordChecker {
  private password: string;

  constructor() {
    this.password = 's3cret';
  }

  checkPassword(password: string) {
    return password === this.password;
  }
}

const checker = new PasswordChecker();
const password = (checker as any).password;

//개별 인스턴스간 접근이 가능한 비공개 속성을 설정하려면 현재 표준화가 진행 중인 비공개 필드 기능(#)을 사용할 수 있음

소스맵을 사용하여 타입스크립트 디버깅하기

// 타입스크립트 코드
function addCounter(el: HTMLElement) {
  let clickCount = 0;
  const button = document.createElement('button');
  button.textContent = 'Click me';
  button.addEventListener('click', () => {
    clickCount++;
    button.textContent = `Click me (${clickCount})`;
  });
  el.appendChild(button);
}

addCounter(document.body);
//브라우저에서 컴파일된 코드
function addCounter(el: HTMLElement) {
  let clickCount = 0;
  const triviaEl = document.createElement('p');
  const button = document.createElement('button');
  button.textContent = 'Click me';
  button.addEventListener('click', async () => {
    clickCount++;
    const response = await fetch(`http://numbersapi.com/${clickCount}`);
    const trivia = await response.text();
    triviaEl.textContent = trivia;
    button.textContent = `Click me (${clickCount})`;
  });
  el.appendChild(triviaEl);
  el.appendChild(button);
}

//타입스크립트는 런타임시 자바스크립트로 변환되기 때문에 디버깅시 어느 부분에서 오류가 발생했는지 파악하기 쉽지 않음
//컴파일 옵션으로 "sourceMap": true를 설정하면 .js.map 파일이 생성되어 소스 코드를 사용하여 디버깅을 할 수 있음.

8장 타입스크립트로 마이그레이션하기

타입스크립트 도입 전에 @ts-check와 JSDoc으로 시험해 보기

// @ts-check
const person = {first: 'Grace', last: 'Hopper'};
2 * person.first
 // ~~~~~~~~~~~~ The right-hand side of an arithmetic operation must be of type
 //              'any', 'number', 'bigint', or an enum type

//@ts-check를 사용하면 타입스크립트 전환 시 어떤 오류가 발생하는지 미리 확인할 수 있음
interface UserData {
  firstName: string;
  lastName: string;
}
declare let user: UserData;

//숨어 있는 변수를 제대로 인식할 수 있도록 별도의 타입 선언 파일을 만들어야 함interface UserData {
  firstName: string;
  lastName: string;
}
declare let user: UserData;

//숨어 있는 변수를 제대로 인식할 수 있도록 별도의 타입 선언 파일을 만들어야 함
// checkJs
// tsConfig: {"allowJs":true,"noEmit":true}
// requires node modules: @types/jquery, @types/sizzle

// @ts-check
$('#graph').style({'width': '100px', 'height': '100px'});
         // ~~~~~ Property 'style' does not exist on type 'JQuery<HTMLElement>'

//서드파티 라이브러리의 경우 @types를 설치하여 타입 선언을 해주어야 함
// @ts-check
const ageEl = /** @type {HTMLInputElement} */(document.getElementById('age'));
ageEl.value = '12';  // OK

//Dom에 접근할 때 @ts-check에서는 JSDoc을 사용하여 타입을 단언할 수 있음

의존성 관계에 따라 모듈 단위로 전환하기

// tsConfig: {"noImplicitAny":false,"strictNullChecks":false}

class Greeting {
  constructor(name) {
    this.greeting = 'Hello';
      // ~~~~~~~~ Property 'greeting' does not exist on type 'Greeting'
    this.name = name;
      // ~~~~ Property 'name' does not exist on type 'Greeting'
  }
  greet() {
    return this.greeting + ' ' + this.name;
             // ~~~~~~~~              ~~~~ Property ... does not exist
  }
}

//타입스크립트에서는 Class 멤버 변수의 타입을 명시적으로 선언해야 하고 quick fix 기능으로 간단히 수정할 수 있음
// tsConfig: {"noImplicitAny":false,"strictNullChecks":false}

const state = {};
state.name = 'New York';
   // ~~~~ Property 'name' does not exist on type '{}'
state.capital = 'Albany';
   // ~~~~~~~ Property 'capital' does not exist on type '{}'
const state = {
  name: 'New York',
  capital: 'Albany',
};  // OK

//한꺼번에 객체를 생성하면 타입 오류를 해결할 수 있음
interface State {
  name: string;
  capital: string;
}
const state = {} as State;
state.name = 'New York';  // OK
state.capital = 'Albany';  // OK

//한번에 생성하기 어려운 경우에는 단언문을 사용하면 됨
// @ts-check
/**
 * @param {number} num
 */
function double(num) {
  return 2 * num;
}

double('trouble');
    // ~~~~~~~~~ Argument of type '"trouble"' is not assignable to
    //           parameter of type 'number'

//JSDoc과 @ts-check를 사용하다가 타입스크립트로 변환하는 경우에는 num의 타입이 any로 추론되고 에러가 사라지므로 주의해야 함
function double(num: number) {
  return 2 * num;
}

double('trouble');
    // ~~~~~~~~~ Argument of type '"trouble"' is not assignable to
    //           parameter of type 'number'

//JSDoc의 타입 정보를 타입스크립트로 변환해주는 기능이 있음


마치며

코드 위주의 내용을 정리하다보니 빠진 부분이 존재하기 때문에 실제 책을 정독하면서 정리한 내용을 통해 복습하는 과정이 필요하다고 생각한다. 책의 내용이 생각보다 광범위해서 처음에 이해가 가지 않는 부분도 있었고 자바스크립트 내용이여서 빠르게 보고 넘어간 부분도 있었다. 전반적으로 타입스크립트를 제대로 활용하는 방법에 대한 케이스를 다루고 있어서 가까이 두고 필요한 내용을 찾아봐야겠다고 생각했다.

좋은 웹페이지 즐겨찾기